devrites 7.3.4 → 7.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/README.md +1 -1
- package/docs/adr/0039-autocomplete-structural-admission.md +87 -0
- package/docs/adr/README.md +1 -0
- package/docs/cli.md +103 -5
- package/docs/engine/commands.md +46 -1
- package/engine/internal/lib/claims.go +65 -8
- package/engine/internal/lib/continuation.go +6 -1
- package/engine/internal/lib/dispatch.go +78 -32
- package/engine/internal/lib/frontier.go +103 -47
- package/engine/internal/lib/frontier_controller.go +652 -0
- package/engine/internal/lib/frontier_roadmap.go +168 -0
- package/engine/internal/lib/next.go +9 -0
- package/engine/internal/parallel/repository.go +10 -0
- package/pack/.claude/skills/devrites-lib/reference/parallel-workspaces.md +106 -115
- package/pack/.claude/skills/devrites-lib/reference/standards/afk-hitl.md +18 -17
- package/pack/.claude/skills/devrites-lib/reference/workspace-artifact-schema.md +41 -38
- package/pack/.claude/skills/rite-autocomplete/SKILL.md +33 -38
- package/pack/.claude/skills/rite-autocomplete/reference/loop.md +46 -52
- package/pack/.claude/skills/rite-autocomplete/reference/stop-conditions.md +28 -28
- package/pack/.claude/skills/rite-build/reference/parallel-batch.md +17 -15
- package/pack/generated/claude/skills/devrites-lib/reference/parallel-workspaces.md +106 -115
- package/pack/generated/claude/skills/devrites-lib/reference/standards/afk-hitl.md +18 -17
- package/pack/generated/claude/skills/devrites-lib/reference/workspace-artifact-schema.md +41 -38
- package/pack/generated/claude/skills/rite-autocomplete/SKILL.md +33 -38
- package/pack/generated/claude/skills/rite-autocomplete/reference/loop.md +46 -52
- package/pack/generated/claude/skills/rite-autocomplete/reference/stop-conditions.md +28 -28
- package/pack/generated/claude/skills/rite-build/reference/parallel-batch.md +17 -15
- package/pack/generated/codex/skills/devrites-lib/reference/parallel-workspaces.md +106 -115
- package/pack/generated/codex/skills/devrites-lib/reference/standards/afk-hitl.md +18 -17
- package/pack/generated/codex/skills/devrites-lib/reference/workspace-artifact-schema.md +41 -38
- package/pack/generated/codex/skills/rite-autocomplete/SKILL.md +33 -38
- package/pack/generated/codex/skills/rite-autocomplete/reference/loop.md +46 -52
- package/pack/generated/codex/skills/rite-autocomplete/reference/stop-conditions.md +28 -28
- package/pack/generated/codex/skills/rite-build/reference/parallel-batch.md +17 -15
- package/pack/generated/devin/skills/devrites-lib/reference/parallel-workspaces.md +106 -115
- package/pack/generated/devin/skills/devrites-lib/reference/standards/afk-hitl.md +18 -17
- package/pack/generated/devin/skills/devrites-lib/reference/workspace-artifact-schema.md +41 -38
- package/pack/generated/devin/skills/rite-autocomplete/SKILL.md +33 -38
- package/pack/generated/devin/skills/rite-autocomplete/reference/loop.md +46 -52
- package/pack/generated/devin/skills/rite-autocomplete/reference/stop-conditions.md +28 -28
- package/pack/generated/devin/skills/rite-build/reference/parallel-batch.md +17 -15
- package/pack/generated/dsh/skills/devrites-lib/reference/parallel-workspaces.md +106 -115
- package/pack/generated/dsh/skills/devrites-lib/reference/standards/afk-hitl.md +18 -17
- package/pack/generated/dsh/skills/devrites-lib/reference/workspace-artifact-schema.md +41 -38
- package/pack/generated/dsh/skills/rite-autocomplete/SKILL.md +33 -38
- package/pack/generated/dsh/skills/rite-autocomplete/reference/loop.md +46 -52
- package/pack/generated/dsh/skills/rite-autocomplete/reference/stop-conditions.md +28 -28
- package/pack/generated/dsh/skills/rite-build/reference/parallel-batch.md +17 -15
- package/pack/generated/omp/skills/devrites-lib/reference/parallel-workspaces.md +106 -115
- package/pack/generated/omp/skills/devrites-lib/reference/standards/afk-hitl.md +18 -17
- package/pack/generated/omp/skills/devrites-lib/reference/workspace-artifact-schema.md +41 -38
- package/pack/generated/omp/skills/rite-autocomplete/SKILL.md +33 -38
- package/pack/generated/omp/skills/rite-autocomplete/reference/loop.md +46 -52
- package/pack/generated/omp/skills/rite-autocomplete/reference/stop-conditions.md +28 -28
- package/pack/generated/omp/skills/rite-build/reference/parallel-batch.md +17 -15
- package/pack/generated/pi/skills/devrites-lib/reference/parallel-workspaces.md +106 -115
- package/pack/generated/pi/skills/devrites-lib/reference/standards/afk-hitl.md +18 -17
- package/pack/generated/pi/skills/devrites-lib/reference/workspace-artifact-schema.md +41 -38
- package/pack/generated/pi/skills/rite-autocomplete/SKILL.md +33 -38
- package/pack/generated/pi/skills/rite-autocomplete/reference/loop.md +46 -52
- package/pack/generated/pi/skills/rite-autocomplete/reference/stop-conditions.md +28 -28
- package/pack/generated/pi/skills/rite-build/reference/parallel-batch.md +17 -15
- package/package.json +1 -1
|
@@ -0,0 +1,652 @@
|
|
|
1
|
+
package lib
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"os"
|
|
6
|
+
"path/filepath"
|
|
7
|
+
"regexp"
|
|
8
|
+
"sort"
|
|
9
|
+
"strings"
|
|
10
|
+
|
|
11
|
+
"github.com/devrites/devrites/internal/devritespaths"
|
|
12
|
+
"github.com/devrites/devrites/internal/markdowntext"
|
|
13
|
+
"github.com/devrites/devrites/internal/parallel"
|
|
14
|
+
"github.com/devrites/devrites/internal/state"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
func autocompleteMode(root string) (bool, error) {
|
|
18
|
+
raw, err := readBoundedRegularFile(filepath.Join(root, "AFK"), workspaceArtifactLimit)
|
|
19
|
+
if os.IsNotExist(err) {
|
|
20
|
+
return false, nil
|
|
21
|
+
}
|
|
22
|
+
if err != nil {
|
|
23
|
+
return false, err
|
|
24
|
+
}
|
|
25
|
+
seen := false
|
|
26
|
+
for _, line := range strings.Split(string(raw), "\n") {
|
|
27
|
+
key, value, ok := strings.Cut(strings.TrimSpace(line), ":")
|
|
28
|
+
if !ok || strings.TrimSpace(key) != "mode" {
|
|
29
|
+
continue
|
|
30
|
+
}
|
|
31
|
+
if seen {
|
|
32
|
+
return false, fmt.Errorf("duplicate AFK mode; normalize controller identity")
|
|
33
|
+
}
|
|
34
|
+
seen = true
|
|
35
|
+
value = strings.TrimSpace(value)
|
|
36
|
+
if i := strings.Index(value, " #"); i >= 0 {
|
|
37
|
+
value = strings.TrimSpace(value[:i])
|
|
38
|
+
}
|
|
39
|
+
if value != "autocomplete" {
|
|
40
|
+
return false, fmt.Errorf("invalid AFK mode; expected autocomplete")
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return seen, nil
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
func frontierRelated(nodes map[string]*frontierWorkspace, chain []string) ([]string, map[string]bool) {
|
|
47
|
+
related := map[string]bool{chain[len(chain)-1]: true}
|
|
48
|
+
for range maxSequenceChain {
|
|
49
|
+
changed := false
|
|
50
|
+
for name, n := range nodes {
|
|
51
|
+
if related[n.parent] && !related[name] {
|
|
52
|
+
related[name], changed = true, true
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if !changed {
|
|
56
|
+
break
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
ordered := make([]string, 0, len(related))
|
|
60
|
+
depth := make(map[string]int, len(related))
|
|
61
|
+
for name := range related {
|
|
62
|
+
ordered = append(ordered, name)
|
|
63
|
+
ancestors, _ := frontierChain(nodes, name)
|
|
64
|
+
depth[name] = len(ancestors)
|
|
65
|
+
}
|
|
66
|
+
sort.Slice(ordered, func(i, j int) bool {
|
|
67
|
+
a, b := nodes[ordered[i]], nodes[ordered[j]]
|
|
68
|
+
if depth[a.slug] != depth[b.slug] {
|
|
69
|
+
return depth[a.slug] < depth[b.slug]
|
|
70
|
+
}
|
|
71
|
+
if a.position != b.position {
|
|
72
|
+
return a.position < b.position
|
|
73
|
+
}
|
|
74
|
+
return a.slug < b.slug
|
|
75
|
+
})
|
|
76
|
+
return ordered, related
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
type frontierRecords struct {
|
|
80
|
+
blocked map[string]string
|
|
81
|
+
serial map[string]string
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
var frontierBlockedRe = regexp.MustCompile("(?m)^[ \\t]*(?:[-*][ \\t]+)?frontier-blocked:[ \\t]+([a-zA-Z0-9][a-zA-Z0-9._-]*)[ \\t]+—[ \\t]+([^\\s`][^\\n]*|`[^\\s`][^\\n]*)")
|
|
85
|
+
|
|
86
|
+
func frontierEvidence(root, slug string) (frontierRecords, error) {
|
|
87
|
+
records := frontierRecords{}
|
|
88
|
+
raw, err := readSequenceArtifact(root, slug, "evidence.md")
|
|
89
|
+
if os.IsNotExist(err) {
|
|
90
|
+
return records, nil
|
|
91
|
+
}
|
|
92
|
+
if err != nil {
|
|
93
|
+
return records, err
|
|
94
|
+
}
|
|
95
|
+
raw, err = markdowntext.Structural(raw)
|
|
96
|
+
if err != nil {
|
|
97
|
+
return records, err
|
|
98
|
+
}
|
|
99
|
+
text := string(raw)
|
|
100
|
+
for _, match := range frontierBlockedRe.FindAllStringSubmatch(text, -1) {
|
|
101
|
+
if devritespaths.ValidSlug(match[1]) {
|
|
102
|
+
if records.blocked == nil {
|
|
103
|
+
records.blocked = map[string]string{}
|
|
104
|
+
}
|
|
105
|
+
records.blocked[match[1]] = strings.TrimSpace(match[2])
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
for _, match := range serialRecordRe.FindAllStringSubmatch(text, -1) {
|
|
109
|
+
if !serialReasonWaives(match[2]) {
|
|
110
|
+
continue
|
|
111
|
+
}
|
|
112
|
+
for _, token := range strings.FieldsFunc(match[1], func(r rune) bool { return r == ',' || r == ' ' || r == '\t' || r == '`' }) {
|
|
113
|
+
workspace, id, qualified := strings.Cut(token, "/")
|
|
114
|
+
if !qualified {
|
|
115
|
+
id, workspace = workspace, ""
|
|
116
|
+
}
|
|
117
|
+
id = strings.ToUpper(id)
|
|
118
|
+
if !state.ValidSliceID(id) || qualified && !devritespaths.ValidSlug(workspace) {
|
|
119
|
+
continue
|
|
120
|
+
}
|
|
121
|
+
if records.serial == nil {
|
|
122
|
+
records.serial = map[string]string{}
|
|
123
|
+
}
|
|
124
|
+
records.serial[parallel.SliceKey(workspace, "SLICE-"+sliceNumber(id))] = strings.TrimSpace(match[2])
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return records, nil
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
func frontierSerialReason(records frontierRecords, source, workspace, id string) string {
|
|
131
|
+
if source == workspace {
|
|
132
|
+
workspace = ""
|
|
133
|
+
}
|
|
134
|
+
return records.serial[parallel.SliceKey(workspace, "SLICE-"+sliceNumber(id))]
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
func frontierPendingPreparation(n *frontierWorkspace) (bool, error) {
|
|
138
|
+
raw, err := readBoundedRegularFile(filepath.Join(n.dir, "dispatch.json"), workspaceArtifactLimit)
|
|
139
|
+
if os.IsNotExist(err) {
|
|
140
|
+
return false, nil
|
|
141
|
+
}
|
|
142
|
+
if err != nil {
|
|
143
|
+
return false, err
|
|
144
|
+
}
|
|
145
|
+
dispatch, err := decodeDispatch(raw)
|
|
146
|
+
if err != nil {
|
|
147
|
+
return false, err
|
|
148
|
+
}
|
|
149
|
+
for _, wave := range dispatch.Waves {
|
|
150
|
+
if wave.State != "open" && wave.State != "sealed" {
|
|
151
|
+
continue
|
|
152
|
+
}
|
|
153
|
+
for role, actor := range wave.Roles {
|
|
154
|
+
if actor.Handle == "" || actor.StartedAt == "" || actor.Returned {
|
|
155
|
+
continue
|
|
156
|
+
}
|
|
157
|
+
base := roleBase(role)
|
|
158
|
+
switch strings.ToLower(wave.Phase) {
|
|
159
|
+
case "spec", "clarify", "temper":
|
|
160
|
+
if base == "evidence-scout" || base == "strategy-reviewer" {
|
|
161
|
+
return true, nil
|
|
162
|
+
}
|
|
163
|
+
case "define", "plan":
|
|
164
|
+
if base == "plan-drafter" || base == "evidence-scout" {
|
|
165
|
+
return true, nil
|
|
166
|
+
}
|
|
167
|
+
case "vet":
|
|
168
|
+
if base == "plan-reviewer" || base == "devex-reviewer" || base == "strategy-reviewer" || base == "code-reviewer" {
|
|
169
|
+
return true, nil
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return false, nil
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
func frontierSuccessor(n *frontierWorkspace, nodes map[string]*frontierWorkspace, mapping map[int]string) (string, error) {
|
|
178
|
+
target := frontierSpecTarget(n)
|
|
179
|
+
if mapped := mapping[n.position+1]; n.hasPosition && mapped != "" {
|
|
180
|
+
if target != "" && mapped != target {
|
|
181
|
+
return "", fmt.Errorf("next Spec conflicts with recorded successor; native authority-normalization required")
|
|
182
|
+
}
|
|
183
|
+
target = mapped
|
|
184
|
+
}
|
|
185
|
+
for _, child := range nodes {
|
|
186
|
+
if child.parent != n.slug || !n.hasPosition || !child.hasPosition || child.position != n.position+1 {
|
|
187
|
+
continue
|
|
188
|
+
}
|
|
189
|
+
if target != "" && target != child.slug {
|
|
190
|
+
return "", fmt.Errorf("conflicting successor identities; native authority-normalization required")
|
|
191
|
+
}
|
|
192
|
+
target = child.slug
|
|
193
|
+
}
|
|
194
|
+
if child := nodes[target]; child != nil && child.parent != n.slug {
|
|
195
|
+
return "", fmt.Errorf("successor %s is not bound to source %s; native authority-normalization required", target, n.slug)
|
|
196
|
+
}
|
|
197
|
+
return target, nil
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
func frontierPreparationAdmission(root, source string, nodes map[string]*frontierWorkspace, mapping map[int]string, problem string, allow bool, records frontierRecords) ([]string, error) {
|
|
201
|
+
n := nodes[source]
|
|
202
|
+
if !allow || n.hasRemaining && n.remaining == 0 {
|
|
203
|
+
return nil, nil
|
|
204
|
+
}
|
|
205
|
+
if hold, err := frontierQuestionHold(n); err != nil || hold != "" {
|
|
206
|
+
return nil, err
|
|
207
|
+
}
|
|
208
|
+
target, err := frontierSuccessor(n, nodes, mapping)
|
|
209
|
+
for _, identity := range [...]string{frontierSpecTarget(n), mapping[n.position+1], target} {
|
|
210
|
+
if child := nodes[identity]; child != nil && child.problem != nil {
|
|
211
|
+
return nil, child.problem
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if problem != "" || err != nil || target == "" && n.hasRemaining && n.remaining > 0 {
|
|
215
|
+
if reason := records.blocked["sequence"]; reason != "" {
|
|
216
|
+
return []string{"frontier-blocked: sequence — " + reason + "; scheduling deferred, not normalized"}, nil
|
|
217
|
+
}
|
|
218
|
+
reason := problem
|
|
219
|
+
if err != nil {
|
|
220
|
+
reason = err.Error()
|
|
221
|
+
}
|
|
222
|
+
if reason == "" {
|
|
223
|
+
reason = "positive remaining budget has no exact recorded successor"
|
|
224
|
+
}
|
|
225
|
+
return nil, fmt.Errorf("source %s native authority-normalization required before writer admission: %s; read sequence artifacts, record current roadmap through its owning decision, or retain frontier-blocked: sequence — <specific constraint>", source, reason)
|
|
226
|
+
}
|
|
227
|
+
if target == "" {
|
|
228
|
+
return nil, nil
|
|
229
|
+
}
|
|
230
|
+
if child := nodes[target]; child != nil {
|
|
231
|
+
if _, err := frontierChain(nodes, target); err != nil {
|
|
232
|
+
return nil, err
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if reason := records.blocked[target]; reason != "" {
|
|
236
|
+
return []string{"frontier-blocked: " + target + " — " + reason + "; preparation deferred, not approved"}, nil
|
|
237
|
+
}
|
|
238
|
+
if child := nodes[target]; child != nil {
|
|
239
|
+
if hold, err := frontierQuestionHold(child); err != nil || hold != "" {
|
|
240
|
+
return nil, err
|
|
241
|
+
}
|
|
242
|
+
if frontierTerminal(child) {
|
|
243
|
+
return nil, nil
|
|
244
|
+
}
|
|
245
|
+
if pending, err := frontierPendingPreparation(child); err != nil || pending {
|
|
246
|
+
return nil, err
|
|
247
|
+
}
|
|
248
|
+
ready, notices, err := WorkspaceReady(filepath.Dir(root), source, target)
|
|
249
|
+
if err != nil {
|
|
250
|
+
return nil, err
|
|
251
|
+
}
|
|
252
|
+
if len(ready) > 0 || len(notices) == 0 {
|
|
253
|
+
var diagnostics []string
|
|
254
|
+
for _, notice := range notices {
|
|
255
|
+
diagnostics = append(diagnostics, "frontier-readiness: "+notice)
|
|
256
|
+
}
|
|
257
|
+
return diagnostics, nil
|
|
258
|
+
}
|
|
259
|
+
dependenciesOnly := true
|
|
260
|
+
for _, notice := range notices {
|
|
261
|
+
dependenciesOnly = dependenciesOnly && strings.Contains(notice, ": waiting for durable BUILT/integrated producer ")
|
|
262
|
+
}
|
|
263
|
+
if dependenciesOnly {
|
|
264
|
+
return nil, nil
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return nil, fmt.Errorf("successor %s preparation has not actually started; invoke /rite-spec %s or its native Plan/Vet cursor and retain launched planning/review handles before /rite-build %s; alternatively retain frontier-blocked: %s — <specific constraint> in evidence.md", target, frontierQuote(target), source, target)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
func frontierNormalizePaths(paths []string) error {
|
|
271
|
+
for i, path := range paths {
|
|
272
|
+
normalized, err := parallel.NormalizePath(path)
|
|
273
|
+
if err != nil {
|
|
274
|
+
return err
|
|
275
|
+
}
|
|
276
|
+
paths[i] = normalized
|
|
277
|
+
}
|
|
278
|
+
return nil
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
func frontierSamePaths(admitted, reserved []string) bool {
|
|
282
|
+
if len(admitted) == 0 || len(admitted) != len(reserved) {
|
|
283
|
+
return false
|
|
284
|
+
}
|
|
285
|
+
for _, reservedPath := range reserved {
|
|
286
|
+
found := false
|
|
287
|
+
for _, path := range admitted {
|
|
288
|
+
found = found || strings.EqualFold(path, reservedPath)
|
|
289
|
+
}
|
|
290
|
+
if !found {
|
|
291
|
+
return false
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return true
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
func frontierLeaseReserved(lease *parallel.Lease, controller, source string, ids []string, ready []parallel.SlicePaths) bool {
|
|
298
|
+
if lease == nil || lease.Status != parallel.StatusRunning && lease.Status != parallel.StatusIntegrateFailed || lease.PendingLanding != nil {
|
|
299
|
+
return false
|
|
300
|
+
}
|
|
301
|
+
for _, id := range ids {
|
|
302
|
+
found := false
|
|
303
|
+
for _, row := range lease.Slices {
|
|
304
|
+
workspace := row.Workspace
|
|
305
|
+
if workspace == "" {
|
|
306
|
+
workspace = controller
|
|
307
|
+
}
|
|
308
|
+
if workspace != source || sliceNumber(row.ID) != sliceNumber(id) || row.WrightStatus == parallel.WrightIntegrated {
|
|
309
|
+
continue
|
|
310
|
+
}
|
|
311
|
+
for _, candidate := range ready {
|
|
312
|
+
if sliceNumber(candidate.ID) == sliceNumber(id) && frontierSamePaths(candidate.Paths, row.Paths) {
|
|
313
|
+
found = true
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if !found {
|
|
318
|
+
return false
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return len(ids) > 0
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
func frontierWriterRetention(root, source, controller, binding string, lease *parallel.Lease, nodes map[string]*frontierWorkspace, ordered, ids []string, current *dispatchFile) error {
|
|
325
|
+
var claims map[string]*claimRecord
|
|
326
|
+
for _, slug := range ordered {
|
|
327
|
+
if _, err := frontierChain(nodes, slug); err != nil {
|
|
328
|
+
return err
|
|
329
|
+
}
|
|
330
|
+
dispatch := current
|
|
331
|
+
if slug != source {
|
|
332
|
+
raw, err := readBoundedRegularFile(filepath.Join(nodes[slug].dir, "dispatch.json"), workspaceArtifactLimit)
|
|
333
|
+
if os.IsNotExist(err) {
|
|
334
|
+
continue
|
|
335
|
+
}
|
|
336
|
+
if err != nil {
|
|
337
|
+
return err
|
|
338
|
+
}
|
|
339
|
+
dispatch, err = decodeDispatch(raw)
|
|
340
|
+
if err != nil {
|
|
341
|
+
return err
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
for name, wave := range dispatch.Waves {
|
|
345
|
+
if wave.State == "complete" || wave.TerminalAt != "" {
|
|
346
|
+
continue
|
|
347
|
+
}
|
|
348
|
+
writerChecked := false
|
|
349
|
+
for role, actor := range wave.Roles {
|
|
350
|
+
wright := strings.Contains(role, "wright")
|
|
351
|
+
if actor.Returned || actor.Handle == "" && (!wright || wave.State == "abandoned") {
|
|
352
|
+
continue
|
|
353
|
+
}
|
|
354
|
+
if !wright {
|
|
355
|
+
if slug == source && (isReviewerRole(role) || roleBase(role) == "proof-runner") {
|
|
356
|
+
return fmt.Errorf("source %s retains a protected reviewer/proof account; reconcile it before writer admission", source)
|
|
357
|
+
}
|
|
358
|
+
continue
|
|
359
|
+
}
|
|
360
|
+
if !writerChecked {
|
|
361
|
+
if binding == "" || wave.ControllerBatch != binding {
|
|
362
|
+
return fmt.Errorf("retained writer %s/%s has unknown/same-tree isolation; reconcile its native handle before another writer; later lease IDs cannot retrofit actor checkout", slug, name)
|
|
363
|
+
}
|
|
364
|
+
if wave.Claim != "" {
|
|
365
|
+
if claims == nil {
|
|
366
|
+
latest, _, bad, err := readClaims(claimsPath(root))
|
|
367
|
+
if err != nil {
|
|
368
|
+
return err
|
|
369
|
+
}
|
|
370
|
+
if bad > 0 {
|
|
371
|
+
return fmt.Errorf("%d unparsed ledger line(s); retained writer claim identity cannot be validated", bad)
|
|
372
|
+
}
|
|
373
|
+
claims = latest
|
|
374
|
+
}
|
|
375
|
+
claim := claims[wave.Claim]
|
|
376
|
+
if claim == nil || claim.Session != wave.ClaimSession {
|
|
377
|
+
return fmt.Errorf("retained writer %s/%s claim %s has missing or mismatched ledger session", slug, name, wave.Claim)
|
|
378
|
+
}
|
|
379
|
+
// Release/expiry changes allocation, never the pending actor's frozen authority.
|
|
380
|
+
} else if wave.ClaimSession != "" {
|
|
381
|
+
return fmt.Errorf("retained writer %s/%s has a claim session without its exact claim identity", slug, name)
|
|
382
|
+
}
|
|
383
|
+
writerChecked = true
|
|
384
|
+
}
|
|
385
|
+
retainedID := ""
|
|
386
|
+
if lane, ok := wrightLane(role); ok {
|
|
387
|
+
for _, id := range wave.Slices {
|
|
388
|
+
if sliceNumber(id) == sliceNumber(lane) {
|
|
389
|
+
retainedID = id
|
|
390
|
+
break
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
} else if len(wave.Slices) == 1 {
|
|
394
|
+
retainedID = wave.Slices[0]
|
|
395
|
+
}
|
|
396
|
+
admitted := wave.AdmittedPaths[retainedID]
|
|
397
|
+
if retainedID == "" || len(admitted) == 0 {
|
|
398
|
+
return fmt.Errorf("retained writer %s/%s role %s lacks immutable admitted paths; reconcile the native actor, not mutable tasks or lease IDs", slug, name, role)
|
|
399
|
+
}
|
|
400
|
+
for _, path := range admitted {
|
|
401
|
+
normalized, err := parallel.NormalizePath(path)
|
|
402
|
+
if err != nil || normalized != path {
|
|
403
|
+
return fmt.Errorf("retained writer %s/%s has invalid admitted paths for %s", slug, name, retainedID)
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
counted, matchingPaths := false, false
|
|
407
|
+
for _, row := range lease.Slices {
|
|
408
|
+
workspace := row.Workspace
|
|
409
|
+
if workspace == "" {
|
|
410
|
+
workspace = controller
|
|
411
|
+
}
|
|
412
|
+
if workspace == slug && sliceNumber(row.ID) == sliceNumber(retainedID) && row.WrightStatus != parallel.WrightIntegrated {
|
|
413
|
+
counted = true
|
|
414
|
+
matchingPaths = frontierSamePaths(admitted, row.Paths)
|
|
415
|
+
break
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
if !counted {
|
|
419
|
+
return fmt.Errorf("retained writer %s/%s lost its active lease reservation for %s; reconcile the native actor before admitting more work", slug, name, retainedID)
|
|
420
|
+
}
|
|
421
|
+
if !matchingPaths {
|
|
422
|
+
return fmt.Errorf("retained writer %s/%s admitted paths for %s no longer match its active lease reservation; mutable tasks cannot move actor authority", slug, name, retainedID)
|
|
423
|
+
}
|
|
424
|
+
if slug == source && containsSlice(ids, retainedID) {
|
|
425
|
+
return fmt.Errorf("retained writer %s/%s still owns %s", slug, name, retainedID)
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return nil
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// The binding records admission into an existing cohort, never an actor's cwd/profile.
|
|
434
|
+
func autocompleteWriterAdmission(root, source string, wave *dispatchWave, current *dispatchFile) (string, []string, error) {
|
|
435
|
+
mode, err := autocompleteMode(root)
|
|
436
|
+
if err != nil || !mode {
|
|
437
|
+
return "", nil, err
|
|
438
|
+
}
|
|
439
|
+
allow, capN, err := frontierPolicy(root)
|
|
440
|
+
if err != nil {
|
|
441
|
+
return "", nil, err
|
|
442
|
+
}
|
|
443
|
+
controller, lease, err := parallel.ControllerLease(filepath.Dir(root))
|
|
444
|
+
if err != nil {
|
|
445
|
+
return "", nil, err
|
|
446
|
+
}
|
|
447
|
+
if lease != nil {
|
|
448
|
+
effectiveCap, occupied := capN, 0
|
|
449
|
+
if lease.Capacity > 0 && lease.Capacity < effectiveCap {
|
|
450
|
+
effectiveCap = lease.Capacity
|
|
451
|
+
}
|
|
452
|
+
if lease.Status == parallel.StatusRunning || lease.Status == parallel.StatusIntegrateFailed {
|
|
453
|
+
for _, row := range lease.Slices {
|
|
454
|
+
if row.WrightStatus != parallel.WrightIntegrated {
|
|
455
|
+
occupied++
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
if occupied > effectiveCap {
|
|
460
|
+
return "", nil, fmt.Errorf("retained cohort has %d occupied reservations above effective cap %d; reconcile capacity and drain existing work before any new writer launch; do not cancel workers", occupied, effectiveCap)
|
|
461
|
+
}
|
|
462
|
+
for i := range lease.Slices {
|
|
463
|
+
if lease.Slices[i].WrightStatus != parallel.WrightIntegrated {
|
|
464
|
+
if err := frontierNormalizePaths(lease.Slices[i].Paths); err != nil {
|
|
465
|
+
return "", nil, err
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
if capN <= 1 && lease == nil {
|
|
471
|
+
return "", nil, nil
|
|
472
|
+
}
|
|
473
|
+
ids := wave.Slices
|
|
474
|
+
nodes, err := frontierScan(root)
|
|
475
|
+
if err != nil {
|
|
476
|
+
return "", nil, err
|
|
477
|
+
}
|
|
478
|
+
chain, err := frontierChain(nodes, source)
|
|
479
|
+
if err != nil {
|
|
480
|
+
return "", nil, err
|
|
481
|
+
}
|
|
482
|
+
ordered, related := frontierRelated(nodes, chain)
|
|
483
|
+
mapping, _, problem, invalidCanonical := frontierRoadmap(nodes, ordered)
|
|
484
|
+
if invalidCanonical {
|
|
485
|
+
return "", nil, fmt.Errorf("invalid canonical roadmap: %s", problem)
|
|
486
|
+
}
|
|
487
|
+
if controller == "" {
|
|
488
|
+
for _, slug := range ordered {
|
|
489
|
+
if nodes[slug].location == "live" {
|
|
490
|
+
controller = slug
|
|
491
|
+
break
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
} else if !related[controller] {
|
|
495
|
+
return "", nil, fmt.Errorf("retained repository controller %s is outside source sequence", controller)
|
|
496
|
+
}
|
|
497
|
+
records, err := frontierEvidence(root, source)
|
|
498
|
+
if err != nil {
|
|
499
|
+
return "", nil, err
|
|
500
|
+
}
|
|
501
|
+
notices, err := frontierPreparationAdmission(root, source, nodes, mapping, problem, allow && capN > 1, records)
|
|
502
|
+
if err != nil {
|
|
503
|
+
return "", nil, err
|
|
504
|
+
}
|
|
505
|
+
local, withheld, err := WorkspaceReady(filepath.Dir(root), controller, source)
|
|
506
|
+
if err != nil {
|
|
507
|
+
return "", nil, err
|
|
508
|
+
}
|
|
509
|
+
if len(local) == 0 && len(withheld) > 0 {
|
|
510
|
+
return "", nil, fmt.Errorf("source readiness: %s", strings.Join(withheld, "; "))
|
|
511
|
+
}
|
|
512
|
+
for _, notice := range withheld {
|
|
513
|
+
notices = append(notices, "frontier-readiness: "+notice)
|
|
514
|
+
}
|
|
515
|
+
for _, id := range ids {
|
|
516
|
+
eligible := false
|
|
517
|
+
for _, candidate := range local {
|
|
518
|
+
eligible = eligible || sliceNumber(candidate.ID) == sliceNumber(id)
|
|
519
|
+
}
|
|
520
|
+
if !eligible {
|
|
521
|
+
return "", nil, fmt.Errorf("source readiness: %s/%s is not eligible; %s", source, id, strings.Join(withheld, "; "))
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
for i := range local {
|
|
525
|
+
if err := frontierNormalizePaths(local[i].Paths); err != nil {
|
|
526
|
+
return "", nil, err
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
binding := ""
|
|
530
|
+
reserved := frontierLeaseReserved(lease, controller, source, ids, local)
|
|
531
|
+
if reserved {
|
|
532
|
+
binding = controller + "/" + lease.BatchID + "@" + lease.CreatedAt
|
|
533
|
+
}
|
|
534
|
+
if err := frontierWriterRetention(root, source, controller, binding, lease, nodes, ordered, ids, current); err != nil {
|
|
535
|
+
return "", nil, err
|
|
536
|
+
}
|
|
537
|
+
var candidates []parallel.SlicePaths
|
|
538
|
+
for _, slug := range ordered {
|
|
539
|
+
if slug == source {
|
|
540
|
+
candidates = append(candidates, local...)
|
|
541
|
+
continue
|
|
542
|
+
}
|
|
543
|
+
if nodes[slug].location != "live" {
|
|
544
|
+
continue
|
|
545
|
+
}
|
|
546
|
+
n := nodes[slug]
|
|
547
|
+
if hold, err := frontierQuestionHold(n); err != nil {
|
|
548
|
+
return "", nil, err
|
|
549
|
+
} else if hold != "" {
|
|
550
|
+
continue
|
|
551
|
+
}
|
|
552
|
+
if _, _, err := frontierLocal(root, controller, n, &candidates); err != nil {
|
|
553
|
+
return "", nil, err
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
filtered := candidates[:0]
|
|
557
|
+
for _, candidate := range candidates {
|
|
558
|
+
workspace := candidate.Workspace
|
|
559
|
+
if workspace == "" {
|
|
560
|
+
workspace = controller
|
|
561
|
+
}
|
|
562
|
+
if workspace != source {
|
|
563
|
+
if reason := frontierSerialReason(records, source, workspace, candidate.ID); reason != "" {
|
|
564
|
+
notices = append(notices, "frontier-serial: "+workspace+"/"+candidate.ID+" — "+reason+"; ordered handoff retained")
|
|
565
|
+
continue
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
filtered = append(filtered, candidate)
|
|
569
|
+
}
|
|
570
|
+
_, picks, _, err := parallel.SelectForSlug(filepath.Dir(root), controller, capN, filtered)
|
|
571
|
+
if err != nil {
|
|
572
|
+
return "", nil, err
|
|
573
|
+
}
|
|
574
|
+
for _, id := range ids {
|
|
575
|
+
selected := reserved
|
|
576
|
+
for _, pick := range picks {
|
|
577
|
+
workspace := pick.Workspace
|
|
578
|
+
if workspace == "" {
|
|
579
|
+
workspace = controller
|
|
580
|
+
}
|
|
581
|
+
selected = selected || workspace == source && sliceNumber(pick.ID) == sliceNumber(id)
|
|
582
|
+
}
|
|
583
|
+
if !selected || source != controller && !reserved {
|
|
584
|
+
return "", nil, fmt.Errorf("%s/%s is not admitted by global lease capacity/path/readiness selection; run frontier and reserve the mixed cohort with parallel create/add before foreign dispatch", source, id)
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
var missing []string
|
|
588
|
+
for _, pick := range picks {
|
|
589
|
+
workspace := pick.Workspace
|
|
590
|
+
if workspace == "" {
|
|
591
|
+
workspace = controller
|
|
592
|
+
}
|
|
593
|
+
if workspace == source && containsSlice(ids, pick.ID) {
|
|
594
|
+
continue
|
|
595
|
+
}
|
|
596
|
+
if frontierSerialReason(records, source, workspace, pick.ID) != "" {
|
|
597
|
+
continue
|
|
598
|
+
}
|
|
599
|
+
missing = append(missing, parallel.SliceKey(pick.Workspace, pick.ID))
|
|
600
|
+
}
|
|
601
|
+
if len(missing) != 0 {
|
|
602
|
+
return "", nil, fmt.Errorf("global picks %s omitted; run devrites-engine parallel select --root %s --slug %s --from-plan --workspaces %s --cap %d and reserve picks with parallel create/add, or record exact serial: <source/SLICE-ID> — <constraint>", strings.Join(missing, ","), frontierQuote(filepath.Dir(root)), frontierQuote(controller), frontierQuote(strings.Join(ordered, ",")), capN)
|
|
603
|
+
}
|
|
604
|
+
wave.AdmittedPaths = make(map[string][]string, len(ids))
|
|
605
|
+
for _, id := range ids {
|
|
606
|
+
for _, candidate := range local {
|
|
607
|
+
if sliceNumber(candidate.ID) == sliceNumber(id) {
|
|
608
|
+
wave.AdmittedPaths[id] = candidate.Paths
|
|
609
|
+
break
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
return binding, notices, nil
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
func frontierGlobalPriority(source string, lines []string) bool {
|
|
617
|
+
controller := source
|
|
618
|
+
for _, line := range lines {
|
|
619
|
+
if strings.HasPrefix(line, "frontier-select-command:") {
|
|
620
|
+
fields := strings.Fields(line)
|
|
621
|
+
for i, field := range fields {
|
|
622
|
+
if field == "--slug" && i+1 < len(fields) {
|
|
623
|
+
controller = strings.Trim(fields[i+1], "'")
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
if strings.HasPrefix(line, "frontier-prepare:") || strings.HasPrefix(line, "frontier-review-sequence:") {
|
|
628
|
+
return true
|
|
629
|
+
}
|
|
630
|
+
if strings.HasPrefix(line, "frontier-workspace:") {
|
|
631
|
+
fields := strings.Fields(line)
|
|
632
|
+
if len(fields) > 3 && fields[1] != source && (fields[3] == "action=repair" || fields[3] == "action=inspect" || fields[3] == "action=prepare") {
|
|
633
|
+
return true
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
for _, line := range lines {
|
|
638
|
+
if !strings.HasPrefix(line, "frontier-picks: ") {
|
|
639
|
+
continue
|
|
640
|
+
}
|
|
641
|
+
for _, key := range strings.Split(strings.TrimPrefix(line, "frontier-picks: "), ",") {
|
|
642
|
+
workspace, _, qualified := strings.Cut(key, "/")
|
|
643
|
+
if !qualified {
|
|
644
|
+
workspace = controller
|
|
645
|
+
}
|
|
646
|
+
if workspace != source {
|
|
647
|
+
return true
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
return false
|
|
652
|
+
}
|