devrites 7.3.0 → 7.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +19 -3
  3. package/bin/devrites.mjs +31 -5
  4. package/docs/engine/commands.md +41 -3
  5. package/docs/release.md +5 -2
  6. package/engine/Makefile +2 -2
  7. package/engine/internal/acceptance/newfinal_origins.go +39 -0
  8. package/engine/internal/acceptance/preconditions.go +17 -11
  9. package/engine/internal/acceptance/transcript.go +23 -4
  10. package/engine/internal/gate/gate.go +8 -6
  11. package/engine/internal/gate/readiness_binding.go +54 -24
  12. package/engine/internal/install/apply.go +36 -21
  13. package/engine/internal/install/binary.go +67 -9
  14. package/engine/internal/install/install.go +63 -21
  15. package/engine/internal/install/manifest.go +4 -5
  16. package/engine/internal/install/merge.go +89 -33
  17. package/engine/internal/install/preflight.go +60 -19
  18. package/engine/internal/install/uninstall.go +19 -9
  19. package/engine/internal/install/update.go +0 -4
  20. package/engine/internal/lib/claims.go +170 -4
  21. package/engine/internal/lib/claims_holder.go +13 -16
  22. package/engine/internal/lib/closeout.go +52 -28
  23. package/engine/internal/lib/context_io.go +88 -4
  24. package/engine/internal/lib/continuation.go +111 -24
  25. package/engine/internal/lib/dispatch.go +157 -24
  26. package/engine/internal/lib/final_prerequisites.go +137 -0
  27. package/engine/internal/lib/manifest_union.go +62 -25
  28. package/engine/internal/lib/next.go +10 -10
  29. package/engine/internal/lib/packet.go +32 -8
  30. package/engine/internal/lib/packet_integrity.go +194 -2
  31. package/engine/internal/lib/resolve.go +128 -142
  32. package/engine/internal/lib/treebaseline.go +0 -1
  33. package/engine/internal/markdowntext/markdowntext.go +110 -31
  34. package/engine/internal/overhaul/records/check.go +45 -5
  35. package/engine/internal/overhaul/records/consistency.go +34 -8
  36. package/engine/internal/overhaul/records/run.go +55 -11
  37. package/engine/internal/parallel/landing.go +17 -0
  38. package/engine/internal/state/acceptance.go +35 -5
  39. package/engine/internal/state/feature.go +3 -2
  40. package/engine/internal/state/observation.go +50 -3
  41. package/engine/internal/state/write.go +20 -44
  42. package/engine/internal/workflow/commands.go +25 -3
  43. package/engine/main.go +19 -0
  44. package/pack/.claude/skills/devrites-lib/reference/parallel-dispatch.md +41 -39
  45. package/pack/.claude/skills/devrites-lib/reference/parallel-workspaces.md +46 -12
  46. package/pack/.claude/skills/overhaul/SKILL.md +8 -4
  47. package/pack/.claude/skills/overhaul/references/lifecycle.md +13 -6
  48. package/pack/.claude/skills/overhaul/references/orchestration.md +32 -21
  49. package/pack/.claude/skills/overhaul/references/records.md +23 -8
  50. package/pack/.claude/skills/overhaul/references/scope-and-safety.md +7 -2
  51. package/pack/.claude/skills/overhaul/references/scoring.md +7 -0
  52. package/pack/.claude/skills/rite-autocomplete/SKILL.md +9 -10
  53. package/pack/.claude/skills/rite-autocomplete/reference/loop.md +8 -1
  54. package/pack/.claude/skills/rite-build/reference/wright-dispatch.md +17 -17
  55. package/pack/.claude/skills/rite-clarify/SKILL.md +25 -25
  56. package/pack/.claude/skills/rite-prove/SKILL.md +15 -7
  57. package/pack/.claude/skills/rite-review/SKILL.md +7 -0
  58. package/pack/.claude/skills/rite-vet/SKILL.md +10 -1
  59. package/pack/generated/claude/skills/devrites-lib/reference/parallel-dispatch.md +41 -39
  60. package/pack/generated/claude/skills/devrites-lib/reference/parallel-workspaces.md +46 -12
  61. package/pack/generated/claude/skills/overhaul/SKILL.md +8 -4
  62. package/pack/generated/claude/skills/overhaul/references/lifecycle.md +13 -6
  63. package/pack/generated/claude/skills/overhaul/references/orchestration.md +32 -21
  64. package/pack/generated/claude/skills/overhaul/references/records.md +23 -8
  65. package/pack/generated/claude/skills/overhaul/references/scope-and-safety.md +7 -2
  66. package/pack/generated/claude/skills/overhaul/references/scoring.md +7 -0
  67. package/pack/generated/claude/skills/rite-autocomplete/SKILL.md +9 -10
  68. package/pack/generated/claude/skills/rite-autocomplete/reference/loop.md +8 -1
  69. package/pack/generated/claude/skills/rite-build/reference/wright-dispatch.md +17 -17
  70. package/pack/generated/claude/skills/rite-clarify/SKILL.md +25 -25
  71. package/pack/generated/claude/skills/rite-prove/SKILL.md +15 -7
  72. package/pack/generated/claude/skills/rite-review/SKILL.md +7 -0
  73. package/pack/generated/claude/skills/rite-vet/SKILL.md +10 -1
  74. package/pack/generated/codex/skills/devrites-lib/reference/parallel-dispatch.md +41 -39
  75. package/pack/generated/codex/skills/devrites-lib/reference/parallel-workspaces.md +46 -12
  76. package/pack/generated/codex/skills/overhaul/SKILL.md +8 -4
  77. package/pack/generated/codex/skills/overhaul/references/lifecycle.md +13 -6
  78. package/pack/generated/codex/skills/overhaul/references/orchestration.md +32 -21
  79. package/pack/generated/codex/skills/overhaul/references/records.md +23 -8
  80. package/pack/generated/codex/skills/overhaul/references/scope-and-safety.md +7 -2
  81. package/pack/generated/codex/skills/overhaul/references/scoring.md +7 -0
  82. package/pack/generated/codex/skills/rite-autocomplete/SKILL.md +9 -10
  83. package/pack/generated/codex/skills/rite-autocomplete/reference/loop.md +8 -1
  84. package/pack/generated/codex/skills/rite-build/reference/wright-dispatch.md +17 -17
  85. package/pack/generated/codex/skills/rite-clarify/SKILL.md +25 -25
  86. package/pack/generated/codex/skills/rite-prove/SKILL.md +15 -7
  87. package/pack/generated/codex/skills/rite-review/SKILL.md +7 -0
  88. package/pack/generated/codex/skills/rite-vet/SKILL.md +10 -1
  89. package/pack/generated/devin/skills/devrites-lib/reference/parallel-dispatch.md +41 -39
  90. package/pack/generated/devin/skills/devrites-lib/reference/parallel-workspaces.md +46 -12
  91. package/pack/generated/devin/skills/overhaul/SKILL.md +8 -4
  92. package/pack/generated/devin/skills/overhaul/references/lifecycle.md +13 -6
  93. package/pack/generated/devin/skills/overhaul/references/orchestration.md +32 -21
  94. package/pack/generated/devin/skills/overhaul/references/records.md +23 -8
  95. package/pack/generated/devin/skills/overhaul/references/scope-and-safety.md +7 -2
  96. package/pack/generated/devin/skills/overhaul/references/scoring.md +7 -0
  97. package/pack/generated/devin/skills/rite-autocomplete/SKILL.md +9 -10
  98. package/pack/generated/devin/skills/rite-autocomplete/reference/loop.md +8 -1
  99. package/pack/generated/devin/skills/rite-build/reference/wright-dispatch.md +17 -17
  100. package/pack/generated/devin/skills/rite-clarify/SKILL.md +25 -25
  101. package/pack/generated/devin/skills/rite-prove/SKILL.md +15 -7
  102. package/pack/generated/devin/skills/rite-review/SKILL.md +7 -0
  103. package/pack/generated/devin/skills/rite-vet/SKILL.md +10 -1
  104. package/pack/generated/dsh/runtime/cordis.patch.yml +28 -14
  105. package/pack/generated/dsh/skills/devrites-lib/reference/parallel-dispatch.md +41 -39
  106. package/pack/generated/dsh/skills/devrites-lib/reference/parallel-workspaces.md +46 -12
  107. package/pack/generated/dsh/skills/overhaul/SKILL.md +8 -4
  108. package/pack/generated/dsh/skills/overhaul/references/lifecycle.md +13 -6
  109. package/pack/generated/dsh/skills/overhaul/references/orchestration.md +32 -21
  110. package/pack/generated/dsh/skills/overhaul/references/records.md +23 -8
  111. package/pack/generated/dsh/skills/overhaul/references/scope-and-safety.md +7 -2
  112. package/pack/generated/dsh/skills/overhaul/references/scoring.md +6 -0
  113. package/pack/generated/dsh/skills/rite-autocomplete/SKILL.md +9 -10
  114. package/pack/generated/dsh/skills/rite-autocomplete/reference/loop.md +8 -1
  115. package/pack/generated/dsh/skills/rite-build/reference/wright-dispatch.md +17 -17
  116. package/pack/generated/dsh/skills/rite-clarify/SKILL.md +25 -25
  117. package/pack/generated/dsh/skills/rite-prove/SKILL.md +15 -7
  118. package/pack/generated/dsh/skills/rite-review/SKILL.md +7 -0
  119. package/pack/generated/dsh/skills/rite-vet/SKILL.md +10 -1
  120. package/pack/generated/omp/skills/devrites-lib/reference/parallel-dispatch.md +41 -39
  121. package/pack/generated/omp/skills/devrites-lib/reference/parallel-workspaces.md +46 -12
  122. package/pack/generated/omp/skills/overhaul/SKILL.md +8 -4
  123. package/pack/generated/omp/skills/overhaul/references/lifecycle.md +13 -6
  124. package/pack/generated/omp/skills/overhaul/references/orchestration.md +32 -21
  125. package/pack/generated/omp/skills/overhaul/references/records.md +23 -8
  126. package/pack/generated/omp/skills/overhaul/references/scope-and-safety.md +7 -2
  127. package/pack/generated/omp/skills/overhaul/references/scoring.md +7 -0
  128. package/pack/generated/omp/skills/rite-autocomplete/SKILL.md +9 -10
  129. package/pack/generated/omp/skills/rite-autocomplete/reference/loop.md +8 -1
  130. package/pack/generated/omp/skills/rite-build/reference/wright-dispatch.md +17 -17
  131. package/pack/generated/omp/skills/rite-clarify/SKILL.md +25 -25
  132. package/pack/generated/omp/skills/rite-prove/SKILL.md +15 -7
  133. package/pack/generated/omp/skills/rite-review/SKILL.md +7 -0
  134. package/pack/generated/omp/skills/rite-vet/SKILL.md +10 -1
  135. package/pack/generated/pi/skills/devrites-lib/reference/parallel-dispatch.md +41 -39
  136. package/pack/generated/pi/skills/devrites-lib/reference/parallel-workspaces.md +46 -12
  137. package/pack/generated/pi/skills/overhaul/SKILL.md +8 -4
  138. package/pack/generated/pi/skills/overhaul/references/lifecycle.md +13 -6
  139. package/pack/generated/pi/skills/overhaul/references/orchestration.md +32 -21
  140. package/pack/generated/pi/skills/overhaul/references/records.md +23 -8
  141. package/pack/generated/pi/skills/overhaul/references/scope-and-safety.md +7 -2
  142. package/pack/generated/pi/skills/overhaul/references/scoring.md +7 -0
  143. package/pack/generated/pi/skills/rite-autocomplete/SKILL.md +9 -10
  144. package/pack/generated/pi/skills/rite-autocomplete/reference/loop.md +8 -1
  145. package/pack/generated/pi/skills/rite-build/reference/wright-dispatch.md +17 -17
  146. package/pack/generated/pi/skills/rite-clarify/SKILL.md +25 -25
  147. package/pack/generated/pi/skills/rite-prove/SKILL.md +15 -7
  148. package/pack/generated/pi/skills/rite-review/SKILL.md +7 -0
  149. package/pack/generated/pi/skills/rite-vet/SKILL.md +10 -1
  150. package/package.json +1 -1
  151. package/scripts/dsh-generate.py +1 -1
  152. package/scripts/install-lib.sh +29 -0
  153. package/uninstall.sh +1 -0
@@ -7,6 +7,7 @@ import (
7
7
  "os"
8
8
  "path/filepath"
9
9
  "sort"
10
+ "strconv"
10
11
  "strings"
11
12
 
12
13
  "github.com/devrites/devrites/internal/devritespaths"
@@ -134,9 +135,9 @@ func mergeManifestLocked(root, slug, workspace string, args []string, stdout, st
134
135
  return 0
135
136
  }
136
137
 
137
- // explicitManifests parses the caller-ordered predecessor list. When the
138
- // workspace carries a sequence_parent cursor, the last predecessor must be the
139
- // recorded parent; a mismatch means the list does not describe this chain.
138
+ // explicitManifests parses the caller-ordered predecessor list. A numbered
139
+ // workspace or one recording a parent requires the complete recorded chain;
140
+ // legacy workspaces without cursors retain caller-supplied ordering.
140
141
  func explicitManifests(root, slug string, predecessors []string) ([][]candidateRow, error) {
141
142
  seen := make(map[string]struct{}, len(predecessors))
142
143
  for _, pred := range predecessors {
@@ -148,8 +149,31 @@ func explicitManifests(root, slug string, predecessors []string) ([][]candidateR
148
149
  }
149
150
  seen[pred] = struct{}{}
150
151
  }
151
- if parent := sequenceParent(root, slug); parent != "" && parent != predecessors[len(predecessors)-1] {
152
- return nil, fmt.Errorf("%s records sequence_parent %q; pass the full ordered predecessor list ending with it (or no list to walk the recorded chain)", slug, parent)
152
+ dir, err := devritespaths.ExistingFeatureDirChecked(root, slug)
153
+ if err != nil {
154
+ return nil, err
155
+ }
156
+ raw, err := readBoundedRegularFile(filepath.Join(dir, "state.md"), maxCandidateManifestBytes)
157
+ if err != nil {
158
+ return nil, fmt.Errorf("%s state.md: %w", slug, err)
159
+ }
160
+ lines := strings.Split(string(raw), "\n")
161
+ parent, _ := state.CursorField(lines, state.CursorSequenceParent)
162
+ _, numbered := state.CursorField(lines, state.CursorSequencePosition)
163
+ if parent != "" || numbered {
164
+ chain, manifests, err := walkSequenceChain(root, slug, dir)
165
+ if err != nil {
166
+ return nil, err
167
+ }
168
+ if len(chain) != len(predecessors) {
169
+ return nil, fmt.Errorf("%s requires the complete ordered sequence_parent chain", slug)
170
+ }
171
+ for i, pred := range predecessors {
172
+ if pred != chain[i] {
173
+ return nil, fmt.Errorf("%s requires the complete ordered sequence_parent chain", slug)
174
+ }
175
+ }
176
+ return manifests, nil
153
177
  }
154
178
  manifests := make([][]candidateRow, 0, len(predecessors))
155
179
  for _, pred := range predecessors {
@@ -162,26 +186,52 @@ func explicitManifests(root, slug string, predecessors []string) ([][]candidateR
162
186
  return manifests, nil
163
187
  }
164
188
 
165
- // walkSequenceChain follows the target workspace's sequence_parent links to the
166
- // chain head, returning predecessors oldest-first with their manifests. Every
167
- // link must resolve to a live or archived workspace carrying the cursor field;
168
- // a broken link refuses the merge rather than producing a partial union.
189
+ // walkSequenceChain follows sequence_parent links, returning predecessors
190
+ // oldest-first. Numbered chains must descend consecutively to position 1;
191
+ // fully unnumbered legacy chains may end at any parentless workspace. Broken
192
+ // links or mixed numbering refuse the merge instead of producing a partial union.
169
193
  func walkSequenceChain(root, slug, workspace string) ([]string, [][]candidateRow, error) {
170
194
  visited := map[string]struct{}{slug: {}}
171
195
  chain := []string{}
172
196
  manifests := [][]candidateRow{}
173
197
  dir := workspace
198
+ numbered := false
199
+ var expectedPosition uint64
174
200
  for {
175
201
  raw, err := readBoundedRegularFile(filepath.Join(dir, "state.md"), maxCandidateManifestBytes)
176
202
  if err != nil {
177
203
  return nil, nil, fmt.Errorf("%s state.md: %w", filepath.Base(dir), err)
178
204
  }
179
- parent, _ := state.CursorField(strings.Split(string(raw), "\n"), state.CursorSequenceParent)
205
+ lines := strings.Split(string(raw), "\n")
206
+ parent, _ := state.CursorField(lines, state.CursorSequenceParent)
207
+ positionText, hasPosition := state.CursorField(lines, state.CursorSequencePosition)
208
+ if len(chain) == 0 {
209
+ numbered = hasPosition
210
+ }
211
+ if hasPosition != numbered {
212
+ return nil, nil, fmt.Errorf("%s mixes numbered and unnumbered sequence positions", filepath.Base(dir))
213
+ }
214
+ if numbered {
215
+ position, err := strconv.ParseUint(positionText, 10, 64)
216
+ if err != nil || position == 0 || strconv.FormatUint(position, 10) != positionText {
217
+ return nil, nil, fmt.Errorf("%s sequence_position %q must be a positive canonical decimal", filepath.Base(dir), positionText)
218
+ }
219
+ if len(chain) > 0 && position != expectedPosition {
220
+ return nil, nil, fmt.Errorf("%s sequence_position %d; expected %d", filepath.Base(dir), position, expectedPosition)
221
+ }
222
+ if parent == "" && position != 1 {
223
+ return nil, nil, fmt.Errorf("%s sequence_position %d has no sequence_parent; only position 1 may end the chain", filepath.Base(dir), position)
224
+ }
225
+ if parent != "" && position == 1 {
226
+ return nil, nil, fmt.Errorf("%s sequence_position 1 must not have a sequence_parent", filepath.Base(dir))
227
+ }
228
+ expectedPosition = position - 1
229
+ }
180
230
  if parent == "" {
181
231
  break
182
232
  }
183
233
  if len(chain) == maxSequenceChain {
184
- return nil, nil, fmt.Errorf("sequence chain longer than %d; pass an explicit ordered predecessor list", maxSequenceChain)
234
+ return nil, nil, fmt.Errorf("sequence chain longer than %d", maxSequenceChain)
185
235
  }
186
236
  if _, dup := visited[parent]; dup {
187
237
  return nil, nil, fmt.Errorf("sequence_parent cycle at %q", parent)
@@ -201,7 +251,7 @@ func walkSequenceChain(root, slug, workspace string) ([]string, [][]candidateRow
201
251
  chain = append(chain, parent)
202
252
  manifests = append(manifests, rows)
203
253
  }
204
- if len(chain) == 0 {
254
+ if len(chain) == 0 && !numbered {
205
255
  return nil, nil, fmt.Errorf("%s has no sequence_parent; pass an explicit ordered predecessor list", slug)
206
256
  }
207
257
  for i, j := 0, len(chain)-1; i < j; i, j = i+1, j-1 {
@@ -211,19 +261,6 @@ func walkSequenceChain(root, slug, workspace string) ([]string, [][]candidateRow
211
261
  return chain, manifests, nil
212
262
  }
213
263
 
214
- func sequenceParent(root, slug string) string {
215
- dir, err := devritespaths.ExistingFeatureDirChecked(root, slug)
216
- if err != nil {
217
- return ""
218
- }
219
- raw, err := readBoundedRegularFile(filepath.Join(dir, "state.md"), maxCandidateManifestBytes)
220
- if err != nil {
221
- return ""
222
- }
223
- parent, _ := state.CursorField(strings.Split(string(raw), "\n"), state.CursorSequenceParent)
224
- return parent
225
- }
226
-
227
264
  // VerifyReleaseUnion enforces the release-milestone contract at the candidate
228
265
  // and seal gates: a workspace whose cursor declares `sequence_role: release`
229
266
  // must carry a candidate manifest covering every predecessor in its recorded
@@ -16,11 +16,11 @@ import (
16
16
  "github.com/devrites/devrites/internal/workflow"
17
17
  )
18
18
 
19
- // RunNext prints the minimal remaining lifecycle path: the first phase whose
20
- // required artifacts are not all present, plus advisory skips for phases whose
21
- // remaining work is vacuous (e.g. clarify with zero open questions). Advisory
22
- // only — the rite still owns the decision to advance — except the root-edit
23
- // tripwire, which exits 3 while an unattributed product edit is in the tree.
19
+ // RunNext prints the minimal remaining lifecycle path using artifact presence
20
+ // where it proves completion, retaining phases that add no required artifact.
21
+ // Advisory only — the rite still owns the decision to advance — except the
22
+ // root-edit tripwire, which exits 3 while an unattributed product edit is in the
23
+ // tree. Observation never records lifecycle metrics.
24
24
  func RunNext(root string, args []string, stdout, stderr io.Writer) int {
25
25
  slug, code, err := ActiveSlug(root, args)
26
26
  if err != nil {
@@ -55,6 +55,7 @@ func RunNext(root string, args []string, stdout, stderr io.Writer) int {
55
55
  return 2
56
56
  }
57
57
 
58
+ fmt.Fprintf(stdout, "source: %s\n", slug)
58
59
  fmt.Fprintf(stdout, "current: %s\n", report.Phase)
59
60
  if report.Complete() {
60
61
  fmt.Fprintln(stdout, "current-status: complete")
@@ -69,7 +70,6 @@ func RunNext(root string, args []string, stdout, stderr io.Writer) int {
69
70
  fmt.Fprintf(stdout, "blocked: tree baseline: %v\n", err)
70
71
  exit = 3
71
72
  } else if len(flagged) > 0 {
72
- recordUnattributed(root, slug, string(report.Phase), flagged)
73
73
  fmt.Fprintf(stdout, "blocked: %s\n", unattributedMessage(slug, flagged))
74
74
  exit = 3
75
75
  }
@@ -94,7 +94,7 @@ func RunNext(root string, args []string, stdout, stderr io.Writer) int {
94
94
  }
95
95
  var missing []string
96
96
  if i == currentIdx {
97
- if report.Complete() {
97
+ if report.Complete() && (currentIdx == 0 || addsArtifact(policies[currentIdx-1], policy) || policy.Target == state.PhaseConverge) {
98
98
  continue
99
99
  }
100
100
  missing = report.MissingFiles
@@ -114,7 +114,7 @@ func RunNext(root string, args []string, stdout, stderr io.Writer) int {
114
114
  }
115
115
  }
116
116
  fmt.Fprintf(stdout, "next-phase: %s\n", policy.Target)
117
- fmt.Fprintf(stdout, "next-command: %s\n", renderNextCommand(policy))
117
+ fmt.Fprintf(stdout, "next-command: %s\n", renderNextCommand(policy, slug))
118
118
  if policy.BlocksOpenQuestions && openQuestions > 0 && policy.Target != state.PhaseClarify {
119
119
  fmt.Fprintf(stdout, "blocked: %d open question(s) gate this phase\n", openQuestions)
120
120
  }
@@ -153,12 +153,12 @@ func printPlanBatch(stdout io.Writer, root, slug string) {
153
153
  fmt.Fprintf(stdout, "select-command: %s\n", planSelectCommand(filepath.Dir(root), slug, capN))
154
154
  }
155
155
 
156
- func renderNextCommand(policy state.PhasePolicy) string {
156
+ func renderNextCommand(policy state.PhasePolicy, slug string) string {
157
157
  verb := policy.ResumeVerb
158
158
  if verb == "" {
159
159
  return "(no resume command)"
160
160
  }
161
- return workflow.ForVerb(verb).Claude
161
+ return workflow.ForVerb(verb).SourceInvocation(slug)
162
162
  }
163
163
 
164
164
  func printSkips(stdout io.Writer, skips []string) {
@@ -1,6 +1,7 @@
1
1
  package lib
2
2
 
3
3
  import (
4
+ "crypto/sha256"
4
5
  "errors"
5
6
  "fmt"
6
7
  "io"
@@ -25,11 +26,14 @@ check, and each --checks gates.md gate's CHECK, which must be an approved
25
26
  test-plan.md Build-entry preflight row; a native-worktree pilot keeps its own
26
27
  contract. A reviewer
27
28
  role (*-reviewer, *-auditor, test-analyst) writes packets/<w>.<role>.md with
28
- its Outcome/Counts return head. Read first names the role's context bundle
29
- (run context first; --recheck names the build --recheck bundle) and
30
- packets/<w>.diff when candidate freeze wrote one; Candidate copies
31
- packets/<w>.sha256 verbatim, or only its header rows when the per-file rows
32
- would overflow the packet (required at review and seal). --focus (relative
29
+ its Outcome/Counts return head. Read first names an immutable wave/role copy
30
+ of the role's context bundle, bound by its actual SHA256 in the packet
31
+ (run context first; --recheck copies the build --recheck bundle), and
32
+ packets/<w>.diff when candidate freeze wrote one. The context path names the
33
+ selected workspace, including authorized overrides outside the project.
34
+ Candidate copies packets/<w>.sha256 verbatim, or only its header rows when the per-file rows
35
+ would overflow the packet (required at review and seal). A wave/role cannot
36
+ be re-rendered with a different context read-set. --focus (relative
33
37
  to the workspace) holds one "- <ID> <path:line> <text>" row per line, text up
34
38
  to 240 characters, and any other non-blank line is refused: open findings
35
39
  for a reviewer, defects and quoted carry-forward constraints for a wright,
@@ -108,13 +112,22 @@ func renderPacketLocked(root, slug, workspace, wave, phase, role, slice, checks,
108
112
  }
109
113
  rel := workspaceRel(root, workspace)
110
114
  bundle := "ctx/" + bundleName(contextOpts{phase: phase, role: role, slice: slice, recheck: recheck})
111
- if info, err := os.Stat(filepath.Join(workspace, filepath.FromSlash(bundle))); err != nil || !info.Mode().IsRegular() {
115
+ context, err := (&contextSources{dir: workspace}).read(bundle)
116
+ if errors.Is(err, os.ErrNotExist) {
112
117
  scope := recheck
113
118
  if slice != "" {
114
119
  scope = " --slice " + slice + recheck
115
120
  }
116
121
  return fail(fmt.Errorf("%s/%s is missing; run devrites-engine context %s --phase %s --role %s%s first", rel, bundle, slug, phase, role, scope))
117
122
  }
123
+ if err != nil {
124
+ return fail(err)
125
+ }
126
+ if len(context) == 0 {
127
+ return fail(fmt.Errorf("%s/%s is empty; run context first", rel, bundle))
128
+ }
129
+ contextName := wave + "." + role + ".context.bundle.md"
130
+ contextPath := rel + "/packets/" + contextName
118
131
  candidate, partial, err := frozenCandidate(workspace, wave)
119
132
  if err != nil {
120
133
  return fail(err)
@@ -135,10 +148,11 @@ func renderPacketLocked(root, slug, workspace, wave, phase, role, slice, checks,
135
148
  } else {
136
149
  b.WriteString("Authority: read-only reviewer. Edit nothing and ask the user nothing; judge the candidate against the bundle sources.\n")
137
150
  }
138
- fmt.Fprintf(&b, "\nRead first:\n- %s/%s\n", rel, bundle)
151
+ fmt.Fprintf(&b, "\nRead first:\n- %s\n", contextPath)
139
152
  if candidate != "" {
140
153
  fmt.Fprintf(&b, "- %s/packets/%s.diff\n", rel, wave)
141
154
  }
155
+ fmt.Fprintf(&b, "\nContext binding:\ncontext %x %s\n", sha256.Sum256(context), contextPath)
142
156
  if wright {
143
157
  raw, err := readWorkspaceArtifact(root, slug, "tasks.md")
144
158
  if err != nil {
@@ -199,7 +213,14 @@ func renderPacketLocked(root, slug, workspace, wave, phase, role, slice, checks,
199
213
  name = wave + ".contract.md"
200
214
  }
201
215
  out := filepath.Join(workspace, "packets", name)
202
- if err := os.MkdirAll(filepath.Dir(out), 0o755); err != nil {
216
+ if prior, err := readBoundedRegularFile(out, maxContractBytes); err == nil {
217
+ if err := lintPacketContexts(root, slug, prior); err != nil {
218
+ return fail(fmt.Errorf("existing packet context: %w", err))
219
+ }
220
+ } else if !errors.Is(err, os.ErrNotExist) {
221
+ return fail(err)
222
+ }
223
+ if err := retainPacketContext(workspace, contextName, context); err != nil {
203
224
  return fail(err)
204
225
  }
205
226
  // Lint a sibling draft so a refused re-render keeps the packet already on disk.
@@ -231,6 +252,9 @@ func renderPacketLocked(root, slug, workspace, wave, phase, role, slice, checks,
231
252
  // contract also gets the gates.md `RUN: writer` precheck lint.
232
253
  func lintRenderedPacket(root, slug, path, role string, wright, finalPhase bool) error {
233
254
  if wright {
255
+ if err := lintPacketDiffs(root, slug, path, false); err != nil {
256
+ return err
257
+ }
234
258
  return lintWrightContractFor(root, slug, path)
235
259
  }
236
260
  if err := lintReviewerWavePacket(path, []string{role}); err != nil {
@@ -8,7 +8,10 @@ import (
8
8
  "io"
9
9
  "os"
10
10
  "path/filepath"
11
+ "regexp"
11
12
  "strings"
13
+
14
+ "github.com/devrites/devrites/internal/devritespaths"
12
15
  )
13
16
 
14
17
  // Binary patches can exceed the candidate's raw-byte bound; stream verification.
@@ -85,13 +88,21 @@ func verifyFrozenDiff(name, expected string) error {
85
88
 
86
89
  func resolvePacketDiff(root, slug, name string) (string, error) {
87
90
  clean := filepath.Clean(filepath.FromSlash(name))
91
+ workspace, err := devritespaths.ExistingFeatureDirChecked(root, slug)
92
+ if err != nil {
93
+ return "", err
94
+ }
95
+ if rel, ok := strings.CutPrefix(name, workspaceRel(root, workspace)+"/"); ok && clean == filepath.FromSlash(name) {
96
+ name = rel
97
+ clean = filepath.Clean(filepath.FromSlash(name))
98
+ }
88
99
  if clean == ".." || strings.HasPrefix(clean, ".."+string(filepath.Separator)) || clean != filepath.FromSlash(name) {
89
100
  return "", fmt.Errorf("unsafe frozen candidate diff path %q", name)
90
101
  }
91
- dirs := []string{projectDir(root), featureDir(root, slug)}
102
+ dirs := []string{projectDir(root), workspace}
92
103
  // Freeze's checksum rows use this workspace-relative namespace, never a product packets/ directory.
93
104
  if strings.HasPrefix(name, "packets/") {
94
- dirs = []string{featureDir(root, slug)}
105
+ dirs = []string{workspace}
95
106
  }
96
107
  if filepath.IsAbs(name) {
97
108
  parent, err := filepath.EvalSymlinks(filepath.Dir(name))
@@ -120,12 +131,193 @@ func resolvePacketDiff(root, slug, name string) (string, error) {
120
131
  return "", fmt.Errorf("frozen candidate diff %q does not exist", name)
121
132
  }
122
133
 
134
+ var packetContextPathRE = regexp.MustCompile(`(?:[A-Za-z]:/)?[A-Za-z0-9_./@+-]+\.bundle\.md\b`)
135
+
136
+ type packetContextRef struct {
137
+ dir string
138
+ name string
139
+ }
140
+
141
+ // Preserve the leaf spelling below the workspace when an ancestor has an OS alias.
142
+ func packetContextWorkspaceRelative(workspace, name string) (string, error) {
143
+ if rel, err := filepath.Rel(workspace, name); err == nil && filepath.IsLocal(rel) {
144
+ return rel, nil
145
+ }
146
+ selected, err := os.Stat(workspace)
147
+ if err != nil {
148
+ return "", err
149
+ }
150
+ for parent := filepath.Dir(name); ; parent = filepath.Dir(parent) {
151
+ info, err := os.Stat(parent)
152
+ if err != nil {
153
+ return "", err
154
+ }
155
+ if os.SameFile(selected, info) {
156
+ return filepath.Rel(parent, name)
157
+ }
158
+ if parent == filepath.Dir(parent) {
159
+ return "", nil
160
+ }
161
+ }
162
+ }
163
+
164
+ func resolvePacketContext(root, slug, name string) (packetContextRef, error) {
165
+ workspace, err := devritespaths.ExistingFeatureDirChecked(root, slug)
166
+ if err != nil {
167
+ return packetContextRef{}, err
168
+ }
169
+ workspace, err = filepath.Abs(workspace)
170
+ if err != nil {
171
+ return packetContextRef{}, err
172
+ }
173
+ clean := filepath.Clean(filepath.FromSlash(name))
174
+ if clean != filepath.FromSlash(name) {
175
+ return packetContextRef{}, fmt.Errorf("unsafe packet context path %q", name)
176
+ }
177
+ dirs := []string{projectDir(root), workspace}
178
+ // Only the selected workspace's exact namespace may contain leading "..".
179
+ if rel, ok := strings.CutPrefix(name, workspaceRel(root, workspace)+"/"); ok {
180
+ dirs, clean = []string{workspace}, filepath.FromSlash(rel)
181
+ } else if filepath.IsAbs(clean) {
182
+ rel, err := packetContextWorkspaceRelative(workspace, clean)
183
+ if err != nil {
184
+ return packetContextRef{}, err
185
+ }
186
+ if rel != "" {
187
+ dirs, clean = []string{workspace}, rel
188
+ } else {
189
+ // Absolute manual bundles remain supported, but not foreign workspaces.
190
+ physicalParent, err := filepath.EvalSymlinks(filepath.Dir(clean))
191
+ if err != nil {
192
+ return packetContextRef{}, err
193
+ }
194
+ physical := filepath.Join(physicalParent, filepath.Base(clean))
195
+ for _, work := range []string{filepath.Dir(workspace), filepath.Join(root, "work")} {
196
+ absoluteWork, err := filepath.Abs(work)
197
+ if err != nil {
198
+ return packetContextRef{}, err
199
+ }
200
+ absoluteWork, err = filepath.EvalSymlinks(absoluteWork)
201
+ if err != nil {
202
+ return packetContextRef{}, err
203
+ }
204
+ if rel, err := filepath.Rel(absoluteWork, physical); err == nil && filepath.IsLocal(rel) {
205
+ return packetContextRef{}, fmt.Errorf("foreign packet context path %q", name)
206
+ }
207
+ }
208
+ dirs, clean = []string{filepath.Dir(clean)}, filepath.Base(clean)
209
+ }
210
+ } else if strings.HasPrefix(name, "ctx/") || strings.HasPrefix(name, "packets/") {
211
+ dirs = []string{workspace}
212
+ } else {
213
+ work := filepath.ToSlash(workspaceRel(root, filepath.Dir(workspace))) + "/"
214
+ if strings.HasPrefix(name, work) {
215
+ return packetContextRef{}, fmt.Errorf("foreign packet context path %q", name)
216
+ }
217
+ }
218
+ if !filepath.IsLocal(clean) {
219
+ return packetContextRef{}, fmt.Errorf("unsafe packet context path %q", name)
220
+ }
221
+ for _, dir := range dirs {
222
+ if err := contextParents(dir, clean); err != nil {
223
+ return packetContextRef{}, err
224
+ }
225
+ ws, err := os.OpenRoot(dir)
226
+ if err != nil {
227
+ return packetContextRef{}, err
228
+ }
229
+ info, err := ws.Lstat(clean)
230
+ closeErr := ws.Close()
231
+ if err != nil && !errors.Is(err, os.ErrNotExist) {
232
+ return packetContextRef{}, err
233
+ }
234
+ if closeErr != nil {
235
+ return packetContextRef{}, closeErr
236
+ }
237
+ if errors.Is(err, os.ErrNotExist) {
238
+ continue
239
+ }
240
+ if !info.Mode().IsRegular() {
241
+ return packetContextRef{}, errors.New("packet context is not a regular file")
242
+ }
243
+ absoluteDir, err := filepath.Abs(dir)
244
+ if err != nil {
245
+ return packetContextRef{}, err
246
+ }
247
+ return packetContextRef{dir: absoluteDir, name: clean}, nil
248
+ }
249
+ return packetContextRef{}, fmt.Errorf("packet context %q does not exist", name)
250
+ }
251
+
252
+ func lintPacketContexts(root, slug string, raw []byte) error {
253
+ bound := make(map[string]string)
254
+ var names []string
255
+ for _, line := range strings.Split(string(raw), "\n") {
256
+ fields := strings.Fields(line)
257
+ if len(fields) > 0 && fields[0] == "context" {
258
+ if len(fields) < 3 || len(fields[1]) != sha256.Size*2 {
259
+ return errors.New("malformed context checksum; use context <SHA256> <path.bundle.md>")
260
+ }
261
+ if _, err := hex.DecodeString(fields[1]); err != nil {
262
+ return errors.New("malformed context SHA256 checksum")
263
+ }
264
+ entry := strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(line), "context"))
265
+ name := strings.TrimSpace(entry[len(fields[1]):])
266
+ ref, err := resolvePacketContext(root, slug, name)
267
+ if err != nil {
268
+ return err
269
+ }
270
+ path := filepath.Join(ref.dir, ref.name)
271
+ digest := strings.ToLower(fields[1])
272
+ if prior, ok := bound[path]; ok && prior != digest {
273
+ return fmt.Errorf("conflicting context checksums for %s", name)
274
+ }
275
+ body, err := readContextArtifact(ref.dir, ref.name)
276
+ if err != nil {
277
+ return fmt.Errorf("packet context %s: %w", name, err)
278
+ }
279
+ if len(body) == 0 || fmt.Sprintf("%x", sha256.Sum256(body)) != digest {
280
+ return fmt.Errorf("packet context %s SHA256 mismatch; render the intended read-set under a new wave", name)
281
+ }
282
+ bound[path] = digest
283
+ continue
284
+ }
285
+ if len(fields) >= 3 && fields[0] == "file" {
286
+ if fields[1] == "absent" {
287
+ continue
288
+ }
289
+ if digest, err := hex.DecodeString(fields[1]); err == nil && len(digest) == sha256.Size {
290
+ continue
291
+ }
292
+ }
293
+ if name, ok := strings.CutPrefix(strings.TrimSpace(line), "- "); ok && strings.HasSuffix(name, ".bundle.md") {
294
+ names = append(names, name)
295
+ continue
296
+ }
297
+ names = append(names, packetContextPathRE.FindAllString(line, -1)...)
298
+ }
299
+ for _, name := range names {
300
+ ref, err := resolvePacketContext(root, slug, name)
301
+ if err != nil {
302
+ return err
303
+ }
304
+ path := filepath.Join(ref.dir, ref.name)
305
+ if _, ok := bound[path]; !ok {
306
+ return fmt.Errorf("unbound packet context %s; add context <SHA256> %s to the packet", name, name)
307
+ }
308
+ }
309
+ return nil
310
+ }
311
+
123
312
  // Caller may already hold the feature lock (dispatch); never acquire it here.
124
313
  func lintPacketDiffs(root, slug, packet string, required bool) error {
125
314
  raw, err := readBoundedRegularFile(packet, maxContractBytes)
126
315
  if err != nil {
127
316
  return fmt.Errorf("reviewer packet %s: %w", packet, err)
128
317
  }
318
+ if err := lintPacketContexts(root, slug, raw); err != nil {
319
+ return err
320
+ }
129
321
  bindings, err := frozenDiffBindings(raw)
130
322
  if err != nil {
131
323
  return err