devrites 7.3.4 → 7.3.6
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 +34 -0
- package/README.md +1 -1
- package/docs/adr/0039-autocomplete-structural-admission.md +87 -0
- package/docs/adr/0040-early-preparation-admission.md +76 -0
- package/docs/adr/README.md +2 -0
- package/docs/cli.md +103 -5
- package/docs/engine/commands.md +88 -1
- package/engine/internal/lib/claims.go +65 -8
- package/engine/internal/lib/context.go +50 -2
- package/engine/internal/lib/continuation.go +8 -1
- package/engine/internal/lib/dispatch.go +147 -46
- package/engine/internal/lib/frontier.go +139 -59
- package/engine/internal/lib/frontier_controller.go +667 -0
- package/engine/internal/lib/frontier_preparation.go +210 -0
- package/engine/internal/lib/frontier_roadmap.go +168 -0
- package/engine/internal/lib/next.go +14 -2
- package/engine/internal/lib/triggers.go +9 -0
- package/engine/internal/parallel/repository.go +10 -0
- package/pack/.claude/skills/devrites-lib/SKILL.md +2 -0
- package/pack/.claude/skills/devrites-lib/reference/parallel-workspaces.md +108 -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 +35 -38
- package/pack/.claude/skills/rite-autocomplete/reference/loop.md +66 -59
- 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/SKILL.md +2 -0
- package/pack/generated/claude/skills/devrites-lib/reference/parallel-workspaces.md +108 -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 +35 -38
- package/pack/generated/claude/skills/rite-autocomplete/reference/loop.md +66 -59
- 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/SKILL.md +2 -0
- package/pack/generated/codex/skills/devrites-lib/reference/parallel-workspaces.md +108 -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 +35 -38
- package/pack/generated/codex/skills/rite-autocomplete/reference/loop.md +66 -59
- 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/SKILL.md +2 -0
- package/pack/generated/devin/skills/devrites-lib/reference/parallel-workspaces.md +108 -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 +35 -38
- package/pack/generated/devin/skills/rite-autocomplete/reference/loop.md +66 -59
- 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/SKILL.md +2 -0
- package/pack/generated/dsh/skills/devrites-lib/reference/parallel-workspaces.md +108 -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 +35 -38
- package/pack/generated/dsh/skills/rite-autocomplete/reference/loop.md +66 -59
- 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/SKILL.md +2 -0
- package/pack/generated/omp/skills/devrites-lib/reference/parallel-workspaces.md +108 -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 +35 -38
- package/pack/generated/omp/skills/rite-autocomplete/reference/loop.md +66 -59
- 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/SKILL.md +2 -0
- package/pack/generated/pi/skills/devrites-lib/reference/parallel-workspaces.md +108 -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 +35 -38
- package/pack/generated/pi/skills/rite-autocomplete/reference/loop.md +66 -59
- 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,210 @@
|
|
|
1
|
+
package lib
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"os"
|
|
6
|
+
"path/filepath"
|
|
7
|
+
"strings"
|
|
8
|
+
|
|
9
|
+
"github.com/devrites/devrites/internal/devritespaths"
|
|
10
|
+
"github.com/devrites/devrites/internal/parallel"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
type preparationPurpose struct {
|
|
14
|
+
parent string
|
|
15
|
+
research bool
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
func preparationPhase(phase string) bool {
|
|
19
|
+
switch phase {
|
|
20
|
+
case "spec", "clarify", "temper", "define", "plan", "vet":
|
|
21
|
+
return true
|
|
22
|
+
}
|
|
23
|
+
return false
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
func preparationRole(phase, role string) bool {
|
|
27
|
+
switch phase {
|
|
28
|
+
case "spec", "clarify", "temper":
|
|
29
|
+
return role == "evidence-scout" || role == "strategy-reviewer"
|
|
30
|
+
case "define", "plan":
|
|
31
|
+
return role == "plan-drafter" || role == "evidence-scout"
|
|
32
|
+
case "vet":
|
|
33
|
+
return role == "plan-reviewer" || role == "devex-reviewer" || role == "strategy-reviewer" || role == "code-reviewer"
|
|
34
|
+
}
|
|
35
|
+
return false
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
func (p preparationPurpose) validate(phase string, roles []string, context bool) error {
|
|
39
|
+
phase = strings.ToLower(phase)
|
|
40
|
+
if p.parent == "" && !p.research {
|
|
41
|
+
return nil
|
|
42
|
+
}
|
|
43
|
+
if p.parent != "" && p.research {
|
|
44
|
+
return fmt.Errorf("--prepare-for and --sequence-research are mutually exclusive")
|
|
45
|
+
}
|
|
46
|
+
if !preparationPhase(phase) {
|
|
47
|
+
return fmt.Errorf("preparation purpose requires a native preparation phase")
|
|
48
|
+
}
|
|
49
|
+
if len(roles) == 0 && (!context || p.research) {
|
|
50
|
+
return fmt.Errorf("preparation dispatch/research requires an allowed named role")
|
|
51
|
+
}
|
|
52
|
+
for _, role := range roles {
|
|
53
|
+
if p.research && role != "evidence-scout" || !p.research && !preparationRole(phase, roleBase(role)) {
|
|
54
|
+
return fmt.Errorf("role %q is not permitted for this preparation purpose", role)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return nil
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
func earlyPreparationPhase(phase string) bool {
|
|
61
|
+
phase = strings.ToLower(phase)
|
|
62
|
+
return phase == "temper" || phase == "define" || phase == "plan" || phase == "vet"
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Context and dispatch share admission; neither a bundle nor a purpose flag proves a native launch.
|
|
66
|
+
func preparationAdmission(root, source, phase string, purpose preparationPurpose, current *dispatchFile) ([]string, error) {
|
|
67
|
+
phase = strings.ToLower(phase)
|
|
68
|
+
if purpose.research {
|
|
69
|
+
if current != nil {
|
|
70
|
+
return nil, nil
|
|
71
|
+
}
|
|
72
|
+
dir, err := devritespaths.ExistingFeatureDirChecked(root, source)
|
|
73
|
+
if err != nil {
|
|
74
|
+
return nil, err
|
|
75
|
+
}
|
|
76
|
+
raw, err := readBoundedRegularFile(filepath.Join(dir, "dispatch.json"), workspaceArtifactLimit)
|
|
77
|
+
if os.IsNotExist(err) {
|
|
78
|
+
return nil, nil
|
|
79
|
+
}
|
|
80
|
+
if err != nil {
|
|
81
|
+
return nil, err
|
|
82
|
+
}
|
|
83
|
+
_, err = decodeDispatch(raw)
|
|
84
|
+
return nil, err
|
|
85
|
+
}
|
|
86
|
+
if purpose.parent == "" && !earlyPreparationPhase(phase) {
|
|
87
|
+
return nil, nil
|
|
88
|
+
}
|
|
89
|
+
mode, err := autocompleteMode(root)
|
|
90
|
+
if err != nil {
|
|
91
|
+
return nil, err
|
|
92
|
+
}
|
|
93
|
+
if purpose.parent == "" && !mode {
|
|
94
|
+
return nil, nil
|
|
95
|
+
}
|
|
96
|
+
allow, capacity, err := frontierPolicy(root)
|
|
97
|
+
if err != nil {
|
|
98
|
+
return nil, err
|
|
99
|
+
}
|
|
100
|
+
if mode {
|
|
101
|
+
_, lease, leaseErr := parallel.ControllerLease(filepath.Dir(root))
|
|
102
|
+
if leaseErr != nil {
|
|
103
|
+
return nil, leaseErr
|
|
104
|
+
}
|
|
105
|
+
if lease != nil && lease.Capacity > 0 && lease.Capacity < capacity {
|
|
106
|
+
capacity = lease.Capacity
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if purpose.parent == "" && capacity <= 1 {
|
|
110
|
+
return nil, nil
|
|
111
|
+
}
|
|
112
|
+
nodes, err := frontierScan(root)
|
|
113
|
+
if err != nil {
|
|
114
|
+
return nil, err
|
|
115
|
+
}
|
|
116
|
+
chain, err := frontierChain(nodes, source)
|
|
117
|
+
if err != nil {
|
|
118
|
+
return nil, err
|
|
119
|
+
}
|
|
120
|
+
if mode && capacity > 1 {
|
|
121
|
+
if _, err := frontierPreparationBindings(nodes[source], current); err != nil {
|
|
122
|
+
return nil, err
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
ordered, _ := frontierRelated(nodes, chain)
|
|
126
|
+
mapping, _, problem, invalid := frontierRoadmap(nodes, ordered)
|
|
127
|
+
if invalid {
|
|
128
|
+
return nil, fmt.Errorf("invalid canonical roadmap: %s", problem)
|
|
129
|
+
}
|
|
130
|
+
if purpose.parent != "" {
|
|
131
|
+
parent, child := nodes[purpose.parent], nodes[source]
|
|
132
|
+
if parent == nil || child == nil || parent == child || child.parent != parent.slug || !parent.hasPosition || !child.hasPosition || child.position != parent.position+1 {
|
|
133
|
+
return nil, fmt.Errorf("--prepare-for requires an existing bound immediate parent with adjacent sequence positions")
|
|
134
|
+
}
|
|
135
|
+
if _, err := frontierChain(nodes, parent.slug); err != nil {
|
|
136
|
+
return nil, err
|
|
137
|
+
}
|
|
138
|
+
target, err := frontierSuccessor(parent, nodes, mapping)
|
|
139
|
+
if problem != "" || err != nil || target != source {
|
|
140
|
+
return nil, fmt.Errorf("--prepare-for parent/successor authority does not agree: %s %v", problem, err)
|
|
141
|
+
}
|
|
142
|
+
return nil, nil
|
|
143
|
+
}
|
|
144
|
+
records, err := frontierEvidence(root, source)
|
|
145
|
+
if err != nil {
|
|
146
|
+
return nil, err
|
|
147
|
+
}
|
|
148
|
+
return frontierPreparationAdmission(root, source, nodes, mapping, problem, allow && capacity > 1, records)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Validate the whole retained ledger before any current-scope classification or credit.
|
|
152
|
+
// A decoded account may be supplied by dispatch admission; recovery uses decode alone.
|
|
153
|
+
func frontierPreparationBindings(n *frontierWorkspace, account *dispatchFile) (*dispatchFile, error) {
|
|
154
|
+
if n == nil {
|
|
155
|
+
return nil, fmt.Errorf("preparation binding source is not an existing sequence workspace")
|
|
156
|
+
}
|
|
157
|
+
if account == nil {
|
|
158
|
+
raw, err := readBoundedRegularFile(filepath.Join(n.dir, "dispatch.json"), workspaceArtifactLimit)
|
|
159
|
+
if os.IsNotExist(err) {
|
|
160
|
+
return nil, nil
|
|
161
|
+
}
|
|
162
|
+
if err != nil {
|
|
163
|
+
return nil, err
|
|
164
|
+
}
|
|
165
|
+
account, err = decodeDispatch(raw)
|
|
166
|
+
if err != nil {
|
|
167
|
+
return nil, err
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
for _, wave := range account.Waves {
|
|
171
|
+
if wave.PreparationFor != "" && wave.PreparationFor != n.parent && wave.State != "complete" && (wave.State != "abandoned" || wave.TerminalAt == "") {
|
|
172
|
+
return nil, fmt.Errorf("workspace %s preparation account does not match its immediate parent", n.slug)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return account, nil
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
func frontierPreparatoryOnly(root string, n *frontierWorkspace, nodes map[string]*frontierWorkspace, mapping map[int]string, problem string) (bool, *dispatchFile, error) {
|
|
179
|
+
account, err := frontierPreparationBindings(n, nil)
|
|
180
|
+
if err != nil {
|
|
181
|
+
return false, nil, err
|
|
182
|
+
}
|
|
183
|
+
parent := nodes[n.parent]
|
|
184
|
+
if parent == nil || frontierTerminal(parent) || frontierTerminal(n) || problem != "" || !parent.hasPosition || !n.hasPosition || n.position != parent.position+1 {
|
|
185
|
+
return false, account, nil
|
|
186
|
+
}
|
|
187
|
+
if _, err := frontierChain(nodes, n.slug); err != nil {
|
|
188
|
+
return false, account, nil
|
|
189
|
+
}
|
|
190
|
+
target, err := frontierSuccessor(parent, nodes, mapping)
|
|
191
|
+
if err != nil || target != n.slug {
|
|
192
|
+
return false, account, nil
|
|
193
|
+
}
|
|
194
|
+
active, err := readBoundedRegularFile(filepath.Join(root, "ACTIVE"), workspaceArtifactLimit)
|
|
195
|
+
if err != nil && !os.IsNotExist(err) {
|
|
196
|
+
return false, account, err
|
|
197
|
+
}
|
|
198
|
+
if strings.TrimSpace(string(active)) == n.slug {
|
|
199
|
+
return false, account, nil
|
|
200
|
+
}
|
|
201
|
+
if account == nil {
|
|
202
|
+
return true, nil, nil
|
|
203
|
+
}
|
|
204
|
+
for _, wave := range account.Waves {
|
|
205
|
+
if wave.PreparationFor == "" && !wave.SequenceResearch && (earlyPreparationPhase(wave.Phase) || isBuildWrightWave(wave)) {
|
|
206
|
+
return false, account, nil
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return true, account, nil
|
|
210
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
package lib
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"os"
|
|
6
|
+
"path/filepath"
|
|
7
|
+
"strconv"
|
|
8
|
+
"strings"
|
|
9
|
+
|
|
10
|
+
"github.com/devrites/devrites/internal/devritespaths"
|
|
11
|
+
"github.com/devrites/devrites/internal/markdowntext"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
// frontierCanonicalRoadmap reads current authority from the full related sequence.
|
|
15
|
+
// A nil map without an error leaves headerless historical maps to legacy parsing.
|
|
16
|
+
func frontierCanonicalRoadmap(nodes map[string]*frontierWorkspace, ordered []string) (mapping map[int]string, artifacts []string, err error) {
|
|
17
|
+
var owner *frontierWorkspace
|
|
18
|
+
var lines []string
|
|
19
|
+
for _, slug := range ordered {
|
|
20
|
+
n := nodes[slug]
|
|
21
|
+
if n == nil || n.dir == "" {
|
|
22
|
+
continue
|
|
23
|
+
}
|
|
24
|
+
path := filepath.Join(n.dir, "roadmap.md")
|
|
25
|
+
raw, readErr := readBoundedRegularFile(path, workspaceArtifactLimit)
|
|
26
|
+
if os.IsNotExist(readErr) {
|
|
27
|
+
continue
|
|
28
|
+
}
|
|
29
|
+
if readErr != nil {
|
|
30
|
+
return nil, append(artifacts, path), fmt.Errorf("roadmap %s unreadable: %w", path, readErr)
|
|
31
|
+
}
|
|
32
|
+
structural, parseErr := markdowntext.Structural(raw)
|
|
33
|
+
if parseErr != nil {
|
|
34
|
+
return nil, append(artifacts, path), fmt.Errorf("roadmap %s malformed: %w", path, parseErr)
|
|
35
|
+
}
|
|
36
|
+
candidate := strings.Split(string(structural), "\n")
|
|
37
|
+
explicit := false
|
|
38
|
+
for _, line := range candidate {
|
|
39
|
+
line = strings.TrimSpace(line)
|
|
40
|
+
if strings.HasPrefix(line, "Sequence root:") || strings.HasPrefix(line, "Decision:") {
|
|
41
|
+
explicit = true
|
|
42
|
+
break
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if !explicit {
|
|
46
|
+
continue
|
|
47
|
+
}
|
|
48
|
+
artifacts = append(artifacts, path)
|
|
49
|
+
if owner != nil {
|
|
50
|
+
return nil, artifacts, fmt.Errorf("multiple canonical roadmap owners: %s and %s", owner.slug, slug)
|
|
51
|
+
}
|
|
52
|
+
owner, lines = n, candidate
|
|
53
|
+
}
|
|
54
|
+
if owner == nil {
|
|
55
|
+
return nil, nil, nil
|
|
56
|
+
}
|
|
57
|
+
root, decision := "", ""
|
|
58
|
+
rootCount, decisionCount := 0, 0
|
|
59
|
+
for _, line := range lines {
|
|
60
|
+
line = strings.TrimSpace(line)
|
|
61
|
+
if value, ok := strings.CutPrefix(line, "Sequence root:"); ok {
|
|
62
|
+
root, rootCount = strings.TrimSpace(value), rootCount+1
|
|
63
|
+
}
|
|
64
|
+
if value, ok := strings.CutPrefix(line, "Decision:"); ok {
|
|
65
|
+
decision, decisionCount = strings.TrimSpace(value), decisionCount+1
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if rootCount != 1 || decisionCount != 1 || !devritespaths.ValidSlug(root) || strings.ContainsAny(root, " \t") || strings.HasPrefix(root, "-") {
|
|
69
|
+
return nil, artifacts, fmt.Errorf("canonical roadmap requires one valid Sequence root and Decision header")
|
|
70
|
+
}
|
|
71
|
+
if len(decision) != 7 || !strings.HasPrefix(decision, "DEC-") || strings.Trim(decision[4:], "0123456789") != "" {
|
|
72
|
+
return nil, artifacts, fmt.Errorf("canonical roadmap Decision must be DEC-NNN")
|
|
73
|
+
}
|
|
74
|
+
decisionPath := filepath.Join(owner.dir, "decisions.md")
|
|
75
|
+
artifacts = append(artifacts, decisionPath)
|
|
76
|
+
raw, readErr := readBoundedRegularFile(decisionPath, workspaceArtifactLimit)
|
|
77
|
+
if readErr != nil {
|
|
78
|
+
return nil, artifacts, fmt.Errorf("canonical roadmap decision unreadable: %w", readErr)
|
|
79
|
+
}
|
|
80
|
+
structural, parseErr := markdowntext.Structural(raw)
|
|
81
|
+
if parseErr != nil {
|
|
82
|
+
return nil, artifacts, fmt.Errorf("canonical roadmap decision malformed: %w", parseErr)
|
|
83
|
+
}
|
|
84
|
+
headings := 0
|
|
85
|
+
for _, line := range strings.Split(string(structural), "\n") {
|
|
86
|
+
fields := strings.Fields(line)
|
|
87
|
+
if len(fields) >= 2 && fields[0] == "##" && fields[1] == decision {
|
|
88
|
+
headings++
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if headings != 1 {
|
|
92
|
+
return nil, artifacts, fmt.Errorf("canonical roadmap Decision %s requires one exact owner-local heading, found %d", decision, headings)
|
|
93
|
+
}
|
|
94
|
+
mapping = map[int]string{}
|
|
95
|
+
positions := map[string]int{}
|
|
96
|
+
tables := 0
|
|
97
|
+
for i := 0; i < len(lines); i++ {
|
|
98
|
+
cells := frontierCells(lines[i])
|
|
99
|
+
if len(cells) != 0 && (len(cells) != 2 || cells[0] != "Position" || cells[1] != "Workspace") {
|
|
100
|
+
return nil, artifacts, fmt.Errorf("canonical roadmap contains an unexpected table")
|
|
101
|
+
}
|
|
102
|
+
if len(cells) != 2 || cells[0] != "Position" || cells[1] != "Workspace" {
|
|
103
|
+
continue
|
|
104
|
+
}
|
|
105
|
+
tables++
|
|
106
|
+
if tables != 1 || i+1 >= len(lines) {
|
|
107
|
+
return nil, artifacts, fmt.Errorf("canonical roadmap requires exactly one Position/Workspace table")
|
|
108
|
+
}
|
|
109
|
+
separator := frontierCells(lines[i+1])
|
|
110
|
+
if len(separator) != 2 {
|
|
111
|
+
return nil, artifacts, fmt.Errorf("canonical roadmap table separator malformed")
|
|
112
|
+
}
|
|
113
|
+
for _, cell := range separator {
|
|
114
|
+
if len(strings.Trim(cell, ":")) < 3 || strings.Trim(cell, "-:") != "" {
|
|
115
|
+
return nil, artifacts, fmt.Errorf("canonical roadmap table separator malformed")
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
i += 2
|
|
119
|
+
for ; i < len(lines) && strings.HasPrefix(strings.TrimSpace(lines[i]), "|"); i++ {
|
|
120
|
+
row := frontierCells(lines[i])
|
|
121
|
+
if len(row) != 2 {
|
|
122
|
+
return nil, artifacts, fmt.Errorf("canonical roadmap table row malformed")
|
|
123
|
+
}
|
|
124
|
+
position, numberErr := strconv.Atoi(row[0])
|
|
125
|
+
slug := row[1]
|
|
126
|
+
if numberErr != nil || position != len(mapping)+1 || strconv.Itoa(position) != row[0] {
|
|
127
|
+
return nil, artifacts, fmt.Errorf("canonical roadmap positions must be unique and contiguous from 1")
|
|
128
|
+
}
|
|
129
|
+
if !devritespaths.ValidSlug(slug) || strings.ContainsAny(slug, " \t") || strings.HasPrefix(slug, "-") || positions[slug] != 0 {
|
|
130
|
+
return nil, artifacts, fmt.Errorf("canonical roadmap requires unique valid workspace slugs")
|
|
131
|
+
}
|
|
132
|
+
mapping[position], positions[slug] = slug, position
|
|
133
|
+
}
|
|
134
|
+
i--
|
|
135
|
+
}
|
|
136
|
+
if tables != 1 || len(mapping) == 0 || mapping[1] != root {
|
|
137
|
+
return nil, artifacts, fmt.Errorf("canonical roadmap requires one nonempty table rooted at Sequence root")
|
|
138
|
+
}
|
|
139
|
+
for _, slug := range ordered {
|
|
140
|
+
n := nodes[slug]
|
|
141
|
+
chain, chainErr := frontierChain(nodes, slug)
|
|
142
|
+
if chainErr != nil {
|
|
143
|
+
return nil, artifacts, fmt.Errorf("canonical roadmap recorded chain invalid: %w", chainErr)
|
|
144
|
+
}
|
|
145
|
+
if chain[len(chain)-1] != root {
|
|
146
|
+
return nil, artifacts, fmt.Errorf("canonical roadmap root %s does not match actual chain anchor", root)
|
|
147
|
+
}
|
|
148
|
+
position := positions[slug]
|
|
149
|
+
if position == 0 || n.hasPosition && n.position != position {
|
|
150
|
+
return nil, artifacts, fmt.Errorf("canonical roadmap conflicts with recorded position for %s", slug)
|
|
151
|
+
}
|
|
152
|
+
if n.parent != mapping[position-1] {
|
|
153
|
+
return nil, artifacts, fmt.Errorf("canonical roadmap conflicts with recorded predecessor for %s", slug)
|
|
154
|
+
}
|
|
155
|
+
if target := frontierSpecTarget(n); target != "" && mapping[position+1] != target {
|
|
156
|
+
return nil, artifacts, fmt.Errorf("canonical roadmap conflicts with next Spec for %s", slug)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
for _, slug := range mapping {
|
|
160
|
+
if n := nodes[slug]; n != nil {
|
|
161
|
+
position := positions[slug]
|
|
162
|
+
if n.problem != nil || n.hasPosition && n.position != position || n.parent != mapping[position-1] {
|
|
163
|
+
return nil, artifacts, fmt.Errorf("canonical roadmap workspace %s is not bound to its recorded sequence position", slug)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return mapping, artifacts, nil
|
|
168
|
+
}
|
|
@@ -82,6 +82,18 @@ func RunNext(root string, args []string, stdout, stderr io.Writer) int {
|
|
|
82
82
|
for _, line := range lines {
|
|
83
83
|
fmt.Fprintln(stdout, line)
|
|
84
84
|
}
|
|
85
|
+
mode, err := autocompleteMode(root)
|
|
86
|
+
if err != nil {
|
|
87
|
+
fmt.Fprintf(stderr, "next: %v\n", err)
|
|
88
|
+
return 2
|
|
89
|
+
}
|
|
90
|
+
if mode && frontierGlobalPriority(slug, lines) {
|
|
91
|
+
fmt.Fprintln(stdout, "next-command: /rite-autocomplete")
|
|
92
|
+
return exit
|
|
93
|
+
}
|
|
94
|
+
if mode && frontierSourcePreparationWait(slug, lines) {
|
|
95
|
+
return exit
|
|
96
|
+
}
|
|
85
97
|
}
|
|
86
98
|
if report.Phase == state.PhaseBuild {
|
|
87
99
|
printPlanBatch(stdout, root, slug)
|
|
@@ -109,11 +121,11 @@ func RunNext(root string, args []string, stdout, stderr io.Writer) int {
|
|
|
109
121
|
}
|
|
110
122
|
missing = report.MissingFiles
|
|
111
123
|
} else {
|
|
112
|
-
|
|
124
|
+
missing = missingArtifacts(featureDir, policy.RequiredArtifacts)
|
|
125
|
+
if policy.Target == state.PhaseClarify && openQuestions == 0 && len(missing) == 0 {
|
|
113
126
|
skips = append(skips, "clarify (0 open questions)")
|
|
114
127
|
continue
|
|
115
128
|
}
|
|
116
|
-
missing = missingArtifacts(featureDir, policy.RequiredArtifacts)
|
|
117
129
|
// A phase that adds no required artifact over the previous one
|
|
118
130
|
// (build after vet, polish after prove) cannot be proven done by
|
|
119
131
|
// artifact presence, so it is never skipped. Converge is the
|
|
@@ -176,5 +176,14 @@ func triggerApplyCommand(opts contextOpts, suggested []string) string {
|
|
|
176
176
|
}
|
|
177
177
|
}
|
|
178
178
|
parts = append(parts, "--trigger", strings.Join(suggested, ","))
|
|
179
|
+
if opts.purpose.parent != "" {
|
|
180
|
+
parts = append(parts, "--prepare-for", frontierQuote(opts.purpose.parent))
|
|
181
|
+
}
|
|
182
|
+
if opts.purpose.research {
|
|
183
|
+
parts = append(parts, "--sequence-research")
|
|
184
|
+
}
|
|
185
|
+
if opts.skillsRoot != "" {
|
|
186
|
+
parts = append(parts, "--skills-root", frontierQuote(opts.skillsRoot))
|
|
187
|
+
}
|
|
179
188
|
return strings.Join(parts, " ")
|
|
180
189
|
}
|
|
@@ -169,6 +169,8 @@ func controllerLease(root, controller string) (*repositoryLease, error) {
|
|
|
169
169
|
|
|
170
170
|
// ControllerLease returns the retained repository controller without changing its lease.
|
|
171
171
|
// A lease in another checkout requires recovery at that explicit control root.
|
|
172
|
+
// Recorded checkout/ref identity must still match; legacy leases lack that evidence.
|
|
173
|
+
// Active reservations must remain path-disjoint; integrated history holds no slot.
|
|
172
174
|
func ControllerLease(repoRoot string) (string, *Lease, error) {
|
|
173
175
|
found, err := controllerLease(repoRoot, "")
|
|
174
176
|
if err != nil || found == nil {
|
|
@@ -177,6 +179,14 @@ func ControllerLease(repoRoot string) (string, *Lease, error) {
|
|
|
177
179
|
if found.root != resolvedPath(repoRoot) {
|
|
178
180
|
return "", nil, fmt.Errorf("repository controller lease retained by %s at %s; recover at its explicit root", found.controller, found.root)
|
|
179
181
|
}
|
|
182
|
+
if err := ensureControllerIdentity(repoRoot, found.lease); err != nil {
|
|
183
|
+
return "", nil, err
|
|
184
|
+
}
|
|
185
|
+
if active := activeSlices(found.lease); len(active) >= 2 {
|
|
186
|
+
if _, err := CheckPathDisjoint(active, repoRoot); err != nil {
|
|
187
|
+
return "", nil, err
|
|
188
|
+
}
|
|
189
|
+
}
|
|
180
190
|
return found.controller, found.lease, nil
|
|
181
191
|
}
|
|
182
192
|
|
|
@@ -39,6 +39,8 @@ lifecycle state from chat or optional `README.md`.
|
|
|
39
39
|
completion states. The host renders the response normally.
|
|
40
40
|
- [`reference/parallel-workspaces.md`](reference/parallel-workspaces.md): explicit
|
|
41
41
|
phase targets, one isolated global controller, and source-owned proof.
|
|
42
|
+
- [Autocomplete loop](../rite-autocomplete/reference/loop.md#early-preparation-admission):
|
|
43
|
+
controller-only early preparation, bounded successor routing and normalization research.
|
|
42
44
|
- [`reference/visual-playbooks/index.md`](reference/visual-playbooks/index.md): progressive
|
|
43
45
|
visual HTML playbook router (load matching ids only; dual-read outline).
|
|
44
46
|
|