devrites 6.0.1 → 6.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +15 -64
  3. package/SECURITY.md +7 -11
  4. package/bin/devrites.mjs +0 -8
  5. package/docs/adr/0034-workflow-artifact-byte-corrections-get-code-review.md +60 -0
  6. package/docs/adr/README.md +2 -1
  7. package/docs/architecture.md +2 -5
  8. package/docs/cli.md +4 -11
  9. package/docs/command-map.md +1 -2
  10. package/docs/engine/commands.md +6 -6
  11. package/docs/release.md +5 -6
  12. package/engine/help.go +12 -7
  13. package/engine/help_test.go +1 -0
  14. package/engine/internal/gate/readiness_binding.go +19 -0
  15. package/engine/internal/lib/cli_observe.go +16 -0
  16. package/engine/internal/lib/context.go +191 -19
  17. package/engine/internal/lib/context_next_test.go +186 -0
  18. package/engine/internal/lib/dispatch.go +146 -11
  19. package/engine/internal/lib/drift.go +10 -2
  20. package/engine/internal/lib/drift_test.go +29 -0
  21. package/engine/internal/lib/slicecheck.go +19 -0
  22. package/engine/internal/lib/slicecheck_test.go +41 -0
  23. package/engine/internal/lib/windows.go +109 -13
  24. package/engine/internal/lib/windows_test.go +85 -0
  25. package/engine/internal/release/release.go +3 -13
  26. package/engine/internal/release/release_test.go +6 -197
  27. package/engine/main.go +2 -2
  28. package/install.sh +1 -116
  29. package/pack/.claude/agents/devrites-slice-wright.md +3 -1
  30. package/pack/.claude/skills/devrites-debug-recovery/reference/hypotheses.md +6 -0
  31. package/pack/.claude/skills/devrites-lib/reference/parallel-dispatch.md +1 -0
  32. package/pack/.claude/skills/devrites-lib/reference/standards/README.md +1 -1
  33. package/pack/.claude/skills/devrites-lib/reference/standards/afk-hitl.md +2 -0
  34. package/pack/.claude/skills/devrites-lib/reference/standards/code-review.md +3 -0
  35. package/pack/.claude/skills/devrites-lib/reference/standards/glossary.md +1 -1
  36. package/pack/.claude/skills/devrites-lib/reference/standards/security.md +1 -0
  37. package/pack/.claude/skills/devrites-lib/reference/standards/windows.md +11 -3
  38. package/pack/.claude/skills/devrites-lib/reference/workspace-artifact-schema.md +5 -2
  39. package/pack/.claude/skills/rite-build/SKILL.md +1 -1
  40. package/pack/.claude/skills/rite-build/reference/one-slice-cycle.md +3 -2
  41. package/pack/.claude/skills/rite-build/reference/parallel-batch.md +5 -3
  42. package/pack/.claude/skills/rite-build/reference/phase-contract.md +5 -3
  43. package/pack/.claude/skills/rite-build/reference/spec-drift-guard.md +3 -1
  44. package/pack/.claude/skills/rite-build/reference/wright-dispatch.md +23 -9
  45. package/pack/.claude/skills/rite-define/SKILL.md +2 -1
  46. package/pack/.claude/skills/rite-plan/SKILL.md +2 -1
  47. package/pack/.claude/skills/rite-pressure-test/SKILL.md +6 -3
  48. package/pack/.claude/skills/rite-vet/SKILL.md +9 -1
  49. package/pack/.claude/skills/rite-vet/reference/review-axes.md +6 -2
  50. package/pack/generated/claude/agents/devrites-slice-wright.md +3 -1
  51. package/pack/generated/claude/skills/devrites-debug-recovery/reference/hypotheses.md +6 -0
  52. package/pack/generated/claude/skills/devrites-lib/reference/parallel-dispatch.md +1 -0
  53. package/pack/generated/claude/skills/devrites-lib/reference/standards/README.md +1 -1
  54. package/pack/generated/claude/skills/devrites-lib/reference/standards/afk-hitl.md +2 -0
  55. package/pack/generated/claude/skills/devrites-lib/reference/standards/code-review.md +3 -0
  56. package/pack/generated/claude/skills/devrites-lib/reference/standards/glossary.md +1 -1
  57. package/pack/generated/claude/skills/devrites-lib/reference/standards/security.md +1 -0
  58. package/pack/generated/claude/skills/devrites-lib/reference/standards/windows.md +11 -3
  59. package/pack/generated/claude/skills/devrites-lib/reference/workspace-artifact-schema.md +5 -2
  60. package/pack/generated/claude/skills/rite-build/SKILL.md +1 -1
  61. package/pack/generated/claude/skills/rite-build/reference/one-slice-cycle.md +3 -2
  62. package/pack/generated/claude/skills/rite-build/reference/parallel-batch.md +5 -3
  63. package/pack/generated/claude/skills/rite-build/reference/phase-contract.md +5 -3
  64. package/pack/generated/claude/skills/rite-build/reference/spec-drift-guard.md +3 -1
  65. package/pack/generated/claude/skills/rite-build/reference/wright-dispatch.md +23 -9
  66. package/pack/generated/claude/skills/rite-define/SKILL.md +2 -1
  67. package/pack/generated/claude/skills/rite-plan/SKILL.md +2 -1
  68. package/pack/generated/claude/skills/rite-pressure-test/SKILL.md +6 -3
  69. package/pack/generated/claude/skills/rite-vet/SKILL.md +9 -1
  70. package/pack/generated/claude/skills/rite-vet/reference/review-axes.md +6 -2
  71. package/pack/generated/codex/agents/devrites-slice-wright.toml +3 -1
  72. package/pack/generated/codex/skills/devrites-debug-recovery/reference/hypotheses.md +6 -0
  73. package/pack/generated/codex/skills/devrites-lib/reference/parallel-dispatch.md +1 -0
  74. package/pack/generated/codex/skills/devrites-lib/reference/standards/README.md +1 -1
  75. package/pack/generated/codex/skills/devrites-lib/reference/standards/afk-hitl.md +2 -0
  76. package/pack/generated/codex/skills/devrites-lib/reference/standards/code-review.md +3 -0
  77. package/pack/generated/codex/skills/devrites-lib/reference/standards/glossary.md +1 -1
  78. package/pack/generated/codex/skills/devrites-lib/reference/standards/security.md +1 -0
  79. package/pack/generated/codex/skills/devrites-lib/reference/standards/windows.md +11 -3
  80. package/pack/generated/codex/skills/devrites-lib/reference/workspace-artifact-schema.md +5 -2
  81. package/pack/generated/codex/skills/rite-build/SKILL.md +1 -1
  82. package/pack/generated/codex/skills/rite-build/reference/one-slice-cycle.md +3 -2
  83. package/pack/generated/codex/skills/rite-build/reference/parallel-batch.md +5 -3
  84. package/pack/generated/codex/skills/rite-build/reference/phase-contract.md +5 -3
  85. package/pack/generated/codex/skills/rite-build/reference/spec-drift-guard.md +3 -1
  86. package/pack/generated/codex/skills/rite-build/reference/wright-dispatch.md +23 -9
  87. package/pack/generated/codex/skills/rite-define/SKILL.md +2 -1
  88. package/pack/generated/codex/skills/rite-plan/SKILL.md +2 -1
  89. package/pack/generated/codex/skills/rite-pressure-test/SKILL.md +6 -3
  90. package/pack/generated/codex/skills/rite-vet/SKILL.md +9 -1
  91. package/pack/generated/codex/skills/rite-vet/reference/review-axes.md +6 -2
  92. package/pack/generated/devin/agents/devrites-slice-wright.md +3 -1
  93. package/pack/generated/devin/skills/devrites-debug-recovery/reference/hypotheses.md +6 -0
  94. package/pack/generated/devin/skills/devrites-lib/reference/parallel-dispatch.md +1 -0
  95. package/pack/generated/devin/skills/devrites-lib/reference/standards/README.md +1 -1
  96. package/pack/generated/devin/skills/devrites-lib/reference/standards/afk-hitl.md +2 -0
  97. package/pack/generated/devin/skills/devrites-lib/reference/standards/code-review.md +3 -0
  98. package/pack/generated/devin/skills/devrites-lib/reference/standards/glossary.md +1 -1
  99. package/pack/generated/devin/skills/devrites-lib/reference/standards/security.md +1 -0
  100. package/pack/generated/devin/skills/devrites-lib/reference/standards/windows.md +11 -3
  101. package/pack/generated/devin/skills/devrites-lib/reference/workspace-artifact-schema.md +5 -2
  102. package/pack/generated/devin/skills/rite-build/SKILL.md +1 -1
  103. package/pack/generated/devin/skills/rite-build/reference/one-slice-cycle.md +3 -2
  104. package/pack/generated/devin/skills/rite-build/reference/parallel-batch.md +5 -3
  105. package/pack/generated/devin/skills/rite-build/reference/phase-contract.md +5 -3
  106. package/pack/generated/devin/skills/rite-build/reference/spec-drift-guard.md +3 -1
  107. package/pack/generated/devin/skills/rite-build/reference/wright-dispatch.md +23 -9
  108. package/pack/generated/devin/skills/rite-define/SKILL.md +2 -1
  109. package/pack/generated/devin/skills/rite-plan/SKILL.md +2 -1
  110. package/pack/generated/devin/skills/rite-pressure-test/SKILL.md +6 -3
  111. package/pack/generated/devin/skills/rite-vet/SKILL.md +9 -1
  112. package/pack/generated/devin/skills/rite-vet/reference/review-axes.md +6 -2
  113. package/pack/generated/omp/agents/devrites-slice-wright.md +3 -1
  114. package/pack/generated/omp/skills/devrites-debug-recovery/reference/hypotheses.md +6 -0
  115. package/pack/generated/omp/skills/devrites-lib/reference/parallel-dispatch.md +1 -0
  116. package/pack/generated/omp/skills/devrites-lib/reference/standards/README.md +1 -1
  117. package/pack/generated/omp/skills/devrites-lib/reference/standards/afk-hitl.md +2 -0
  118. package/pack/generated/omp/skills/devrites-lib/reference/standards/code-review.md +3 -0
  119. package/pack/generated/omp/skills/devrites-lib/reference/standards/glossary.md +1 -1
  120. package/pack/generated/omp/skills/devrites-lib/reference/standards/security.md +1 -0
  121. package/pack/generated/omp/skills/devrites-lib/reference/standards/windows.md +11 -3
  122. package/pack/generated/omp/skills/devrites-lib/reference/workspace-artifact-schema.md +5 -2
  123. package/pack/generated/omp/skills/rite-build/SKILL.md +1 -1
  124. package/pack/generated/omp/skills/rite-build/reference/one-slice-cycle.md +3 -2
  125. package/pack/generated/omp/skills/rite-build/reference/parallel-batch.md +5 -3
  126. package/pack/generated/omp/skills/rite-build/reference/phase-contract.md +5 -3
  127. package/pack/generated/omp/skills/rite-build/reference/spec-drift-guard.md +3 -1
  128. package/pack/generated/omp/skills/rite-build/reference/wright-dispatch.md +23 -9
  129. package/pack/generated/omp/skills/rite-define/SKILL.md +2 -1
  130. package/pack/generated/omp/skills/rite-plan/SKILL.md +2 -1
  131. package/pack/generated/omp/skills/rite-pressure-test/SKILL.md +6 -3
  132. package/pack/generated/omp/skills/rite-vet/SKILL.md +9 -1
  133. package/pack/generated/omp/skills/rite-vet/reference/review-axes.md +6 -2
  134. package/pack/generated/pi/agents/devrites-slice-wright.md +3 -1
  135. package/pack/generated/pi/skills/devrites-debug-recovery/reference/hypotheses.md +6 -0
  136. package/pack/generated/pi/skills/devrites-lib/reference/parallel-dispatch.md +1 -0
  137. package/pack/generated/pi/skills/devrites-lib/reference/standards/README.md +1 -1
  138. package/pack/generated/pi/skills/devrites-lib/reference/standards/afk-hitl.md +2 -0
  139. package/pack/generated/pi/skills/devrites-lib/reference/standards/code-review.md +3 -0
  140. package/pack/generated/pi/skills/devrites-lib/reference/standards/glossary.md +1 -1
  141. package/pack/generated/pi/skills/devrites-lib/reference/standards/security.md +1 -0
  142. package/pack/generated/pi/skills/devrites-lib/reference/standards/windows.md +11 -3
  143. package/pack/generated/pi/skills/devrites-lib/reference/workspace-artifact-schema.md +5 -2
  144. package/pack/generated/pi/skills/rite-build/SKILL.md +1 -1
  145. package/pack/generated/pi/skills/rite-build/reference/one-slice-cycle.md +3 -2
  146. package/pack/generated/pi/skills/rite-build/reference/parallel-batch.md +5 -3
  147. package/pack/generated/pi/skills/rite-build/reference/phase-contract.md +5 -3
  148. package/pack/generated/pi/skills/rite-build/reference/spec-drift-guard.md +3 -1
  149. package/pack/generated/pi/skills/rite-build/reference/wright-dispatch.md +23 -9
  150. package/pack/generated/pi/skills/rite-define/SKILL.md +2 -1
  151. package/pack/generated/pi/skills/rite-plan/SKILL.md +2 -1
  152. package/pack/generated/pi/skills/rite-pressure-test/SKILL.md +6 -3
  153. package/pack/generated/pi/skills/rite-vet/SKILL.md +9 -1
  154. package/pack/generated/pi/skills/rite-vet/reference/review-axes.md +6 -2
  155. package/package.json +1 -1
  156. package/scripts/install-lib.sh +0 -16
@@ -14,6 +14,8 @@ import (
14
14
  "strings"
15
15
 
16
16
  "github.com/devrites/devrites/internal/devritespaths"
17
+ "github.com/devrites/devrites/internal/markdowntext"
18
+ "github.com/devrites/devrites/internal/state"
17
19
  )
18
20
 
19
21
  // loadsManifest is the machine-readable read-set a skill declares in an HTML
@@ -24,29 +26,35 @@ import (
24
26
  // role key is present its list replaces "workspace" entirely (an empty list
25
27
  // means the role needs no workspace artifacts). "agents" maps a --role name to
26
28
  // an agents-root-relative path when it deviates from the devrites-<role>
27
- // convention.
29
+ // convention. "rootOnly" lists always/trigger paths that only the controlling
30
+ // root executes; a --role bundle names them instead of inlining them.
28
31
  type loadsManifest struct {
29
32
  Always []string `json:"always"`
30
33
  Triggers map[string][]string `json:"triggers"`
31
34
  Workspace []string `json:"workspace"`
32
35
  WorkspaceByRole map[string][]string `json:"workspaceByRole"`
33
36
  Agents map[string]string `json:"agents"`
37
+ RootOnly []string `json:"rootOnly"`
34
38
  }
35
39
 
36
40
  const loadsMarker = "<!-- loads:"
37
41
 
38
- const contextUsage = `usage: devrites-engine context [slug] (--phase <p> | --skill <name>) [--role <r>] [--trigger a,b] [--skills-root <dir>] [--out <path>]
42
+ const contextUsage = `usage: devrites-engine context [slug] (--phase <p> | --skill <name>) [--role <r>] [--slice <SLICE-###>] [--trigger a,b] [--skills-root <dir>] [--out <path>]
39
43
 
40
44
  Emits one deduplicated context bundle for a phase or standalone skill
41
45
  (+ optional dispatch role) into .devrites/work/<slug>/ctx/ so an agent
42
46
  reads a single file. With --skill, slug may be omitted; then --out is
43
- required and no workspace section is included.
47
+ required and no workspace section is included. --slice scopes the
48
+ workspace section to one slice: tasks.md contributes that slice's block,
49
+ and an artifact over its line/byte budget becomes a by-reference index
50
+ (path, SHA-256, headings, and line hits for every ID the block cites).
51
+ Repeat --slice for a recheck that spans several slices.
44
52
  `
45
53
 
46
54
  // RunContext emits one deduplicated context bundle for a phase (+ optional
47
55
  // dispatch role) so an agent reads a single file instead of selecting files.
48
56
  //
49
- // context <slug> --phase <p> [--role <r>] [--trigger a,b] [--skills-root <dir>] [--out <path>]
57
+ // context <slug> --phase <p> [--role <r>] [--slice <SLICE-###>] [--trigger a,b] [--skills-root <dir>] [--out <path>]
50
58
  func RunContext(root string, args []string, stdout, stderr io.Writer) int {
51
59
  opts, code := parseContextArgs(args, stderr)
52
60
  if code != 0 {
@@ -82,6 +90,27 @@ func RunContext(root string, args []string, stdout, stderr io.Writer) int {
82
90
  return 2
83
91
  }
84
92
 
93
+ var sliceBlock string
94
+ var sliceIDs []citedID
95
+ if opts.slice != "" {
96
+ raw, err := os.ReadFile(filepath.Join(featureDir, "tasks.md")) // #nosec G304 -- featureDir is the validated feature workspace
97
+ if err != nil {
98
+ fmt.Fprintf(stderr, "context: %v\n", err)
99
+ return 2
100
+ }
101
+ var blocks []string
102
+ for _, id := range opts.slices {
103
+ block, ok := state.ExtractTaskSlice(raw, id)
104
+ if !ok {
105
+ fmt.Fprintf(stdout, "context: BLOCKED: %s not found in tasks.md\n", id)
106
+ return 3
107
+ }
108
+ blocks = append(blocks, block)
109
+ }
110
+ sliceBlock = strings.Join(blocks, "\n")
111
+ sliceIDs = citedIDs(sliceBlock)
112
+ }
113
+
85
114
  var files []string
86
115
  var missingRequired []string
87
116
  var autoTriggers []string
@@ -124,12 +153,31 @@ func RunContext(root string, args []string, stdout, stderr io.Writer) int {
124
153
 
125
154
  seen := map[string]bool{}
126
155
  loaded := 0
156
+ // The role contract leads the bundle so the agent reads its rules before
157
+ // reference text.
158
+ if opts.role != "" {
159
+ rel := roleAgentPath(manifest, opts.role)
160
+ if writeContextEntry(&bundle, filepath.Join(skillsRoot, "..", "agents", filepath.FromSlash(rel)),
161
+ "agents/"+rel, seen, true) {
162
+ loaded++
163
+ } else {
164
+ // The agent contract is a pack file, not a workspace artifact: a
165
+ // miss means a typo'd role or a pack bug, and a dispatch without
166
+ // its contract must fail loudly.
167
+ missingRequired = append(missingRequired, "agents/"+rel)
168
+ }
169
+ }
170
+ var rootOnly []string
127
171
  for _, rel := range files {
128
172
  abs := filepath.Join(skillsRoot, filepath.FromSlash(rel))
129
173
  if seen[abs] {
130
174
  continue
131
175
  }
132
176
  seen[abs] = true
177
+ if opts.role != "" && slices.Contains(manifest.RootOnly, rel) {
178
+ rootOnly = append(rootOnly, rel)
179
+ continue
180
+ }
133
181
  data, err := os.ReadFile(abs) // #nosec G304 -- abs is resolved from the validated skills root
134
182
  fmt.Fprintf(&bundle, "===== %s =====\n", rel)
135
183
  if err != nil {
@@ -144,34 +192,37 @@ func RunContext(root string, args []string, stdout, stderr io.Writer) int {
144
192
  bundle.WriteByte('\n')
145
193
  loaded++
146
194
  }
195
+ if len(rootOnly) > 0 {
196
+ fmt.Fprintf(&bundle, "===== root-only (not inlined for role %s) =====\n%s\n\n", opts.role, strings.Join(rootOnly, "\n"))
197
+ }
147
198
 
199
+ whole, byReference := 0, 0
148
200
  workspaceList, workspaceScoped := workspaceReadSet(manifest, opts.role)
149
201
  if hasManifest && featureDir != "" {
150
202
  if workspaceScoped && len(workspaceList) == 0 {
151
203
  bundle.WriteString("===== workspace =====\nskipped: role-scoped read-set declares no workspace artifacts\n\n")
152
204
  }
153
205
  for _, rel := range workspaceList {
154
- if writeContextEntry(&bundle, filepath.Join(featureDir, filepath.FromSlash(rel)),
155
- "workspace/"+rel, seen, false) {
206
+ abs := filepath.Join(featureDir, filepath.FromSlash(rel))
207
+ if opts.slice == "" {
208
+ if writeContextEntry(&bundle, abs, "workspace/"+rel, seen, false) {
209
+ loaded++
210
+ }
211
+ continue
212
+ }
213
+ ok, byRef := writeSliceEntry(&bundle, filepath.Dir(root), abs, "workspace/"+rel, opts.slice, sliceBlock, sliceIDs, seen)
214
+ if ok {
156
215
  loaded++
157
216
  }
217
+ if byRef {
218
+ byReference++
219
+ } else if ok {
220
+ whole++
221
+ }
158
222
  }
159
223
  } else if hasManifest && len(workspaceList) > 0 {
160
224
  bundle.WriteString("===== workspace =====\nskipped: no feature workspace (slug omitted)\n\n")
161
225
  }
162
- if opts.role != "" {
163
- rel := roleAgentPath(manifest, opts.role)
164
- if writeContextEntry(&bundle, filepath.Join(skillsRoot, "..", "agents", filepath.FromSlash(rel)),
165
- "agents/"+rel, seen, true) {
166
- loaded++
167
- } else {
168
- // The agent contract is a pack file, not a workspace artifact: a
169
- // miss means a typo'd role or a pack bug, and a dispatch without
170
- // its contract must fail loudly.
171
- missingRequired = append(missingRequired, "agents/"+rel)
172
- }
173
- }
174
-
175
226
  out := opts.out
176
227
  if out == "" {
177
228
  out = filepath.Join(featureDir, "ctx", bundleName(opts))
@@ -216,6 +267,9 @@ func RunContext(root string, args []string, stdout, stderr io.Writer) int {
216
267
  fmt.Fprintf(stdout, ", MISSING: %s", strings.Join(missingRequired, ","))
217
268
  }
218
269
  fmt.Fprintln(stdout, ")")
270
+ if opts.slice != "" {
271
+ fmt.Fprintf(stdout, "scope: slice %s (%d whole, %d by-reference)\n", opts.slice, whole, byReference)
272
+ }
219
273
  if workspaceScoped {
220
274
  fmt.Fprintf(stdout, "workspace: role-scoped for %s (%d of %d declared artifacts)\n",
221
275
  opts.role, len(workspaceList), len(manifest.Workspace))
@@ -280,6 +334,110 @@ func expandIncludes(abs string, data []byte) []byte {
280
334
  })
281
335
  }
282
336
 
337
+ // writeSliceEntry writes one workspace artifact of a --slice bundle: tasks.md
338
+ // as the slice block plus a by-reference index, any other canonical artifact
339
+ // over its line/byte budget as a by-reference index, everything else whole.
340
+ func writeSliceEntry(bundle *strings.Builder, project, abs, label, slice, block string, ids []citedID, seen map[string]bool) (loaded, byRef bool) {
341
+ if seen[abs] {
342
+ return false, false
343
+ }
344
+ data, err := os.ReadFile(abs) // #nosec G304 -- abs is resolved from the validated feature workspace
345
+ name := filepath.Base(abs)
346
+ if err == nil && name == "tasks.md" {
347
+ seen[abs] = true
348
+ fmt.Fprintf(bundle, "===== %s (%s block) =====\n%s\n", label, slice, block)
349
+ } else if status, ok := state.ArtifactBudget(name, data); err != nil || !ok || !status.Over {
350
+ return writeContextEntry(bundle, abs, label, seen, false), false
351
+ } else {
352
+ seen[abs] = true
353
+ }
354
+ writeAnchorIndex(bundle, project, abs, label, data, ids)
355
+ return true, true
356
+ }
357
+
358
+ var (
359
+ citedIDRE = regexp.MustCompile(`\b[A-Z]{1,6}-\d{2,}(?:-[A-Z][A-Z0-9]*)*\b`)
360
+ anchorHeadingRE = regexp.MustCompile(`^#{1,3}\s`)
361
+ )
362
+
363
+ // maxIDHits bounds the line anchors listed per ID; the live file holds the rest.
364
+ const maxIDHits = 20
365
+
366
+ // citedID is one ID the slice block cites with its word-bounded matcher.
367
+ type citedID struct {
368
+ id string
369
+ re *regexp.Regexp
370
+ }
371
+
372
+ // citedIDs returns each distinct ID the slice block cites (AC-###, DEC-###,
373
+ // T-###, PF-##, SLICE-###, ...).
374
+ func citedIDs(block string) []citedID {
375
+ var out []citedID
376
+ seen := map[string]bool{}
377
+ for _, id := range citedIDRE.FindAllString(block, -1) {
378
+ if !seen[id] {
379
+ seen[id] = true
380
+ out = append(out, citedID{id, regexp.MustCompile(`\b` + regexp.QuoteMeta(id) + `\b`)})
381
+ }
382
+ }
383
+ return out
384
+ }
385
+
386
+ // writeAnchorIndex emits a by-reference entry: identity, heading anchors, and
387
+ // line hits for each cited ID, so the agent reads owning sections from the
388
+ // live file instead of the whole artifact.
389
+ func writeAnchorIndex(bundle *strings.Builder, project, abs, label string, data []byte, ids []citedID) {
390
+ rel, err := filepath.Rel(project, abs)
391
+ if err != nil {
392
+ rel = abs
393
+ }
394
+ lines := strings.Split(strings.ReplaceAll(string(data), "\r\n", "\n"), "\n")
395
+ structural := lines
396
+ if masked, err := markdowntext.Structural(data); err == nil {
397
+ structural = strings.Split(strings.ReplaceAll(string(masked), "\r\n", "\n"), "\n")
398
+ }
399
+ fmt.Fprintf(bundle, "===== %s (by reference) =====\n", label)
400
+ fmt.Fprintf(bundle, "path: %s sha256: %x bytes: %d lines: %d\n", filepath.ToSlash(rel), sha256.Sum256(data), len(data), len(lines))
401
+ bundle.WriteString("Not inlined. Read owning sections from the live file by line anchor; nothing is omitted there.\n")
402
+ bundle.WriteString("headings:\n")
403
+ for i, line := range structural {
404
+ if anchorHeadingRE.MatchString(line) {
405
+ fmt.Fprintf(bundle, "- L%d %s\n", i+1, clipRunes(lines[i], 120))
406
+ }
407
+ }
408
+ if len(ids) > 0 {
409
+ bundle.WriteString("id hits (IDs cited by the slice block):\n")
410
+ }
411
+ for _, cited := range ids {
412
+ var at []string
413
+ total := 0
414
+ for i, line := range lines {
415
+ if cited.re.MatchString(line) {
416
+ total++
417
+ if len(at) < maxIDHits {
418
+ at = append(at, fmt.Sprintf("L%d", i+1))
419
+ }
420
+ }
421
+ }
422
+ hits := "none"
423
+ if total > 0 {
424
+ hits = strings.Join(at, " ")
425
+ if total > len(at) {
426
+ hits += fmt.Sprintf(" (+%d more)", total-len(at))
427
+ }
428
+ }
429
+ fmt.Fprintf(bundle, "- %s: %s\n", cited.id, hits)
430
+ }
431
+ bundle.WriteByte('\n')
432
+ }
433
+
434
+ func clipRunes(s string, n int) string {
435
+ if r := []rune(s); len(r) > n {
436
+ return string(r[:n]) + "…"
437
+ }
438
+ return s
439
+ }
440
+
283
441
  func writeContextEntry(bundle *strings.Builder, abs, label string, seen map[string]bool, expand bool) bool {
284
442
  if seen[abs] {
285
443
  return false
@@ -308,6 +466,8 @@ type contextOpts struct {
308
466
  phase string
309
467
  skill string
310
468
  role string
469
+ slice string // display label: slices joined with "+"
470
+ slices []string
311
471
  triggers []string
312
472
  skillsRoot string
313
473
  out string
@@ -325,6 +485,8 @@ func parseContextArgs(args []string, stderr io.Writer) (contextOpts, int) {
325
485
  opts.skill = value
326
486
  case "--role":
327
487
  opts.role = value
488
+ case "--slice":
489
+ opts.slices = append(opts.slices, value)
328
490
  case "--trigger", "--triggers":
329
491
  for _, name := range strings.Split(value, ",") {
330
492
  if name = strings.TrimSpace(name); name != "" {
@@ -353,6 +515,13 @@ func parseContextArgs(args []string, stderr io.Writer) (contextOpts, int) {
353
515
  fmt.Fprintf(stderr, "context: --skill must be a directory name, not a path\n")
354
516
  return opts, 2
355
517
  }
518
+ for _, id := range opts.slices {
519
+ if !state.ValidSliceID(id) || len(positional) == 0 {
520
+ fmt.Fprintf(stderr, "context: --slice needs a slug and a SLICE-### id, got %q\n", id)
521
+ return opts, 2
522
+ }
523
+ }
524
+ opts.slice = strings.Join(opts.slices, "+")
356
525
  if len(positional) == 1 {
357
526
  opts.slug = positional[0]
358
527
  } else if opts.out == "" {
@@ -429,6 +598,9 @@ func bundleName(opts contextOpts) string {
429
598
  if opts.role != "" {
430
599
  name += "-" + opts.role
431
600
  }
601
+ if opts.slice != "" {
602
+ name += "." + opts.slice
603
+ }
432
604
  return name + ".bundle.md"
433
605
  }
434
606
 
@@ -672,3 +672,189 @@ func TestContextWorkspaceIncludeMarkerStaysLiteral(t *testing.T) {
672
672
  t.Fatalf("workspace include marker not kept literal:\n%s", text)
673
673
  }
674
674
  }
675
+
676
+ func TestContextSliceScopesWorkspaceAndRole(t *testing.T) {
677
+ root, featureDir := newWorkspace(t, "build")
678
+ skills := newSkillsRoot(t)
679
+ writeFile(t, filepath.Join(skills, "rite-build", "reference", "root-procedure.md"), "# root procedure body\n")
680
+ writeFile(t, filepath.Join(skills, "rite-build", "SKILL.md"), "---\nname: rite-build\n---\n<!-- loads: {\"always\":[\"devrites-lib/reference/standards/core.md\",\"rite-build/reference/root-procedure.md\"],\"rootOnly\":[\"rite-build/reference/root-procedure.md\"],\"workspace\":[\"tasks.md\",\"decisions.md\",\"spec.md\"]} -->\n# build\n")
681
+ writeFile(t, filepath.Join(featureDir, "tasks.md"), "# Tasks\n\n## SLICE-001 First\nGoal: first body\n\n## SLICE-002 Second\nGoal: second body\nSatisfies: AC-001\nDecisions: DEC-010\ndepends_on: [SLICE-001]\n")
682
+ var decisions strings.Builder
683
+ decisions.WriteString("# Decisions\n")
684
+ for i := 0; i < 250; i++ {
685
+ decisions.WriteString("filler decision row\n")
686
+ }
687
+ decisions.WriteString("## DEC-010 Use the store\nDEC-0100 is a different id\n")
688
+ writeFile(t, filepath.Join(featureDir, "decisions.md"), decisions.String())
689
+ writeFile(t, filepath.Join(featureDir, "spec.md"), "# spec\nAC-001 small spec body\n")
690
+
691
+ stdout, stderr := &bytes.Buffer{}, &bytes.Buffer{}
692
+ args := []string{"feat", "--phase", "build", "--role", "slice-wright", "--slice", "SLICE-002", "--skills-root", skills}
693
+ if code := RunContext(root, args, stdout, stderr); code != 0 {
694
+ t.Fatalf("code=%d\n%s%s", code, stdout, stderr)
695
+ }
696
+ if !strings.Contains(stdout.String(), "scope: slice SLICE-002 (1 whole, 2 by-reference)") {
697
+ t.Fatalf("scope line missing:\n%s", stdout)
698
+ }
699
+ raw, err := os.ReadFile(filepath.Join(featureDir, "ctx", "build-slice-wright.SLICE-002.bundle.md"))
700
+ if err != nil {
701
+ t.Fatalf("bundle: %v", err)
702
+ }
703
+ text := string(raw)
704
+ for _, want := range []string{
705
+ "Goal: second body",
706
+ "workspace/decisions.md (by reference)",
707
+ "/work/feat/decisions.md sha256: ",
708
+ "- L252 ## DEC-010 Use the store",
709
+ "- DEC-010: L252\n",
710
+ "- SLICE-001: L3 L10\n",
711
+ "- AC-001: L8\n",
712
+ "small spec body",
713
+ "===== root-only (not inlined for role slice-wright) =====\nrite-build/reference/root-procedure.md",
714
+ } {
715
+ if !strings.Contains(text, want) {
716
+ t.Fatalf("bundle missing %q:\n%s", want, text)
717
+ }
718
+ }
719
+ for _, absent := range []string{"Goal: first body", "filler decision row", "root procedure body"} {
720
+ if strings.Contains(text, absent) {
721
+ t.Fatalf("bundle inlined %q:\n%s", absent, text)
722
+ }
723
+ }
724
+ if strings.Index(text, "# wright") > strings.Index(text, "# core") {
725
+ t.Fatalf("role contract must lead the bundle:\n%s", text)
726
+ }
727
+
728
+ // Without --role the root still gets its procedure inline.
729
+ stdout.Reset()
730
+ if code := RunContext(root, []string{"feat", "--phase", "build", "--skills-root", skills}, stdout, stderr); code != 0 {
731
+ t.Fatalf("phase bundle code=%d\n%s", code, stderr)
732
+ }
733
+ phase, err := os.ReadFile(filepath.Join(featureDir, "ctx", "build.bundle.md"))
734
+ if err != nil || !strings.Contains(string(phase), "root procedure body") || !strings.Contains(string(phase), "Goal: first body") {
735
+ t.Fatalf("phase bundle must stay whole: err=%v\n%s", err, phase)
736
+ }
737
+ }
738
+
739
+ func TestContextSliceRejectsBadOrUnknownID(t *testing.T) {
740
+ root, featureDir := newWorkspace(t, "build")
741
+ skills := newSkillsRoot(t)
742
+ writeFile(t, filepath.Join(featureDir, "tasks.md"), "# Tasks\n\n## SLICE-001 First\nGoal: x\n")
743
+ stdout, stderr := &bytes.Buffer{}, &bytes.Buffer{}
744
+ if code := RunContext(root, []string{"feat", "--phase", "build", "--slice", "slice-1", "--skills-root", skills}, stdout, stderr); code != 2 {
745
+ t.Fatalf("malformed id: code=%d want 2\n%s", code, stderr)
746
+ }
747
+ if code := RunContext(root, []string{"--skill", "rite-build", "--slice", "SLICE-001", "--out", "x.md", "--skills-root", skills}, stdout, stderr); code != 2 {
748
+ t.Fatalf("slice without slug: code=%d want 2\n%s", code, stderr)
749
+ }
750
+ stdout.Reset()
751
+ if code := RunContext(root, []string{"feat", "--phase", "build", "--slice", "SLICE-009", "--skills-root", skills}, stdout, stderr); code != 3 || !strings.Contains(stdout.String(), "BLOCKED: SLICE-009 not found") {
752
+ t.Fatalf("unknown slice: code=%d\n%s", code, stdout)
753
+ }
754
+ }
755
+
756
+ func TestDispatchWrightRoundCap(t *testing.T) {
757
+ root, featureDir := newWorkspace(t, "build")
758
+ run := func(args ...string) (int, string) {
759
+ stdout, stderr := &bytes.Buffer{}, &bytes.Buffer{}
760
+ code := RunDispatch(root, append([]string{"feat"}, args...), stdout, stderr)
761
+ return code, stdout.String() + stderr.String()
762
+ }
763
+ open := func(wave string, extra ...string) (int, string) {
764
+ return run(append([]string{"open", "--phase", "build", "--wave", wave, "--role", "slice-wright", "--slice", "SLICE-001"}, extra...)...)
765
+ }
766
+ if code, out := run("open", "--phase", "build", "--wave", "w0", "--role", "slice-wright"); code != 2 || !strings.Contains(out, "needs --slice") {
767
+ t.Fatalf("missing --slice: code=%d\n%s", code, out)
768
+ }
769
+ if code, out := run("open", "--phase", "vet", "--wave", "v1", "--role", "plan-reviewer"); code != 0 {
770
+ t.Fatalf("non-build wave must not need --slice: code=%d\n%s", code, out)
771
+ }
772
+ for i := 1; i <= wrightRoundsPerReview; i++ {
773
+ code, out := open("r" + string(rune('0'+i)))
774
+ if code != 0 || !strings.Contains(out, "SLICE-001 wright round "+string(rune('0'+i))) {
775
+ t.Fatalf("round %d: code=%d\n%s", i, code, out)
776
+ }
777
+ }
778
+ if code, out := run("abandon", "--wave", "r5", "--reason", "orphaned"); !strings.Contains(out, "wave r5 abandoned") {
779
+ t.Fatalf("abandon: code=%d\n%s", code, out)
780
+ }
781
+ if code, out := open("r5b"); code != 0 || !strings.Contains(out, "wright round 5") {
782
+ t.Fatalf("abandoned wave must not count: code=%d\n%s", code, out)
783
+ }
784
+ if code, out := open("r6"); code != 3 || !strings.Contains(out, "BLOCKED: SLICE-001 reached 5 wright rounds") {
785
+ t.Fatalf("round 6 must block: code=%d\n%s", code, out)
786
+ }
787
+ writeFile(t, filepath.Join(featureDir, "drift.md"), "# Drift\n\n## D-010 — SLICE-002 unrelated\n\n## D-011 — SLICE-001 reslice review: no smaller AC-preserving split\n")
788
+ if code, out := open("r6", "--reslice-review", "D-099"); code != 3 {
789
+ t.Fatalf("unknown drift id: code=%d\n%s", code, out)
790
+ }
791
+ if code, out := open("r6", "--reslice-review", "D-010"); code != 3 || !strings.Contains(out, "naming SLICE-001") {
792
+ t.Fatalf("drift heading for another slice: code=%d\n%s", code, out)
793
+ }
794
+ if code, out := open("r6", "--reslice-review", "D-011"); code != 0 || !strings.Contains(out, "wright round 6") {
795
+ t.Fatalf("valid review must admit: code=%d\n%s", code, out)
796
+ }
797
+ for i := 7; i <= 2*wrightRoundsPerReview; i++ {
798
+ if code, out := open("r" + string(rune('a'+i))); code != 0 {
799
+ t.Fatalf("round %d inside the reviewed window: code=%d\n%s", i, code, out)
800
+ }
801
+ }
802
+ if code, out := open("r11", "--reslice-review", "D-011"); code != 3 || !strings.Contains(out, "already admitted") {
803
+ t.Fatalf("reused review: code=%d\n%s", code, out)
804
+ }
805
+ if code, out := run("open", "--phase", "build", "--wave", "s2", "--role", "slice-wright", "--slice", "SLICE-002", "--reslice-review", "D-010"); code != 2 {
806
+ t.Fatalf("review before the cap must be refused: code=%d\n%s", code, out)
807
+ }
808
+ }
809
+
810
+ func TestDispatchVetRoundsCountSeparately(t *testing.T) {
811
+ root, featureDir := newWorkspace(t, "build")
812
+ run := func(args ...string) (int, string) {
813
+ stdout, stderr := &bytes.Buffer{}, &bytes.Buffer{}
814
+ code := RunDispatch(root, append([]string{"feat"}, args...), stdout, stderr)
815
+ return code, stdout.String() + stderr.String()
816
+ }
817
+ for i := 0; i < 7; i++ {
818
+ if code, out := run("open", "--phase", "vet", "--wave", "full"+string(rune('a'+i)), "--role", "plan-reviewer"); code != 0 {
819
+ t.Fatalf("full Vet without --slice stays uncapped: code=%d\n%s", code, out)
820
+ }
821
+ }
822
+ if code, out := run("open", "--phase", "build", "--wave", "w1", "--role", "slice-wright", "--slice", "SLICE-001"); code != 0 || !strings.Contains(out, "wright round 1") {
823
+ t.Fatalf("wright round must not see vet waves: code=%d\n%s", code, out)
824
+ }
825
+ for i := 1; i <= wrightRoundsPerReview; i++ {
826
+ if code, out := run("open", "--phase", "vet", "--wave", "v"+string(rune('0'+i)), "--role", "plan-reviewer", "--slice", "SLICE-001"); code != 0 || !strings.Contains(out, "vet round "+string(rune('0'+i))) {
827
+ t.Fatalf("vet round %d: code=%d\n%s", i, code, out)
828
+ }
829
+ }
830
+ if code, out := run("open", "--phase", "vet", "--wave", "v6", "--role", "plan-reviewer", "--slice", "SLICE-001"); code != 3 || !strings.Contains(out, "reached 5 vet rounds") {
831
+ t.Fatalf("vet round 6 must block: code=%d\n%s", code, out)
832
+ }
833
+ writeFile(t, filepath.Join(featureDir, "drift.md"), "## D-001 — SLICE-001 reslice review: proof seam already minimal\n")
834
+ if code, out := run("open", "--phase", "vet", "--wave", "v6", "--role", "plan-reviewer", "--slice", "SLICE-001", "--reslice-review", "D-001"); code != 0 {
835
+ t.Fatalf("reviewed vet round 6: code=%d\n%s", code, out)
836
+ }
837
+ if code, out := run("open", "--phase", "plan", "--wave", "p1", "--role", "plan-drafter", "--slice", "SLICE-001"); code != 2 {
838
+ t.Fatalf("--slice on a plan wave: code=%d\n%s", code, out)
839
+ }
840
+ }
841
+
842
+ func TestContextRepeatedSliceUnion(t *testing.T) {
843
+ root, featureDir := newWorkspace(t, "build")
844
+ skills := newSkillsRoot(t)
845
+ writeFile(t, filepath.Join(skills, "rite-build", "SKILL.md"), "---\nname: rite-build\n---\n<!-- loads: {\"always\":[\"devrites-lib/reference/standards/core.md\"],\"workspace\":[\"tasks.md\"]} -->\n# build\n")
846
+ writeFile(t, filepath.Join(featureDir, "tasks.md"), "# Tasks\n\n## SLICE-001 A\nGoal: alpha body\n\n## SLICE-002 B\nGoal: beta body\n\n## SLICE-003 C\nGoal: gamma body\n")
847
+ stdout, stderr := &bytes.Buffer{}, &bytes.Buffer{}
848
+ args := []string{"feat", "--phase", "build", "--slice", "SLICE-001", "--slice", "SLICE-003", "--skills-root", skills}
849
+ if code := RunContext(root, args, stdout, stderr); code != 0 {
850
+ t.Fatalf("code=%d\n%s%s", code, stdout, stderr)
851
+ }
852
+ raw, err := os.ReadFile(filepath.Join(featureDir, "ctx", "build.SLICE-001+SLICE-003.bundle.md"))
853
+ if err != nil {
854
+ t.Fatal(err)
855
+ }
856
+ text := string(raw)
857
+ if !strings.Contains(text, "alpha body") || !strings.Contains(text, "gamma body") || strings.Contains(text, "beta body") {
858
+ t.Fatalf("union of slice blocks wrong:\n%s", text)
859
+ }
860
+ }