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.
Files changed (75) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +1 -1
  3. package/docs/adr/0039-autocomplete-structural-admission.md +87 -0
  4. package/docs/adr/0040-early-preparation-admission.md +76 -0
  5. package/docs/adr/README.md +2 -0
  6. package/docs/cli.md +103 -5
  7. package/docs/engine/commands.md +88 -1
  8. package/engine/internal/lib/claims.go +65 -8
  9. package/engine/internal/lib/context.go +50 -2
  10. package/engine/internal/lib/continuation.go +8 -1
  11. package/engine/internal/lib/dispatch.go +147 -46
  12. package/engine/internal/lib/frontier.go +139 -59
  13. package/engine/internal/lib/frontier_controller.go +667 -0
  14. package/engine/internal/lib/frontier_preparation.go +210 -0
  15. package/engine/internal/lib/frontier_roadmap.go +168 -0
  16. package/engine/internal/lib/next.go +14 -2
  17. package/engine/internal/lib/triggers.go +9 -0
  18. package/engine/internal/parallel/repository.go +10 -0
  19. package/pack/.claude/skills/devrites-lib/SKILL.md +2 -0
  20. package/pack/.claude/skills/devrites-lib/reference/parallel-workspaces.md +108 -115
  21. package/pack/.claude/skills/devrites-lib/reference/standards/afk-hitl.md +18 -17
  22. package/pack/.claude/skills/devrites-lib/reference/workspace-artifact-schema.md +41 -38
  23. package/pack/.claude/skills/rite-autocomplete/SKILL.md +35 -38
  24. package/pack/.claude/skills/rite-autocomplete/reference/loop.md +66 -59
  25. package/pack/.claude/skills/rite-autocomplete/reference/stop-conditions.md +28 -28
  26. package/pack/.claude/skills/rite-build/reference/parallel-batch.md +17 -15
  27. package/pack/generated/claude/skills/devrites-lib/SKILL.md +2 -0
  28. package/pack/generated/claude/skills/devrites-lib/reference/parallel-workspaces.md +108 -115
  29. package/pack/generated/claude/skills/devrites-lib/reference/standards/afk-hitl.md +18 -17
  30. package/pack/generated/claude/skills/devrites-lib/reference/workspace-artifact-schema.md +41 -38
  31. package/pack/generated/claude/skills/rite-autocomplete/SKILL.md +35 -38
  32. package/pack/generated/claude/skills/rite-autocomplete/reference/loop.md +66 -59
  33. package/pack/generated/claude/skills/rite-autocomplete/reference/stop-conditions.md +28 -28
  34. package/pack/generated/claude/skills/rite-build/reference/parallel-batch.md +17 -15
  35. package/pack/generated/codex/skills/devrites-lib/SKILL.md +2 -0
  36. package/pack/generated/codex/skills/devrites-lib/reference/parallel-workspaces.md +108 -115
  37. package/pack/generated/codex/skills/devrites-lib/reference/standards/afk-hitl.md +18 -17
  38. package/pack/generated/codex/skills/devrites-lib/reference/workspace-artifact-schema.md +41 -38
  39. package/pack/generated/codex/skills/rite-autocomplete/SKILL.md +35 -38
  40. package/pack/generated/codex/skills/rite-autocomplete/reference/loop.md +66 -59
  41. package/pack/generated/codex/skills/rite-autocomplete/reference/stop-conditions.md +28 -28
  42. package/pack/generated/codex/skills/rite-build/reference/parallel-batch.md +17 -15
  43. package/pack/generated/devin/skills/devrites-lib/SKILL.md +2 -0
  44. package/pack/generated/devin/skills/devrites-lib/reference/parallel-workspaces.md +108 -115
  45. package/pack/generated/devin/skills/devrites-lib/reference/standards/afk-hitl.md +18 -17
  46. package/pack/generated/devin/skills/devrites-lib/reference/workspace-artifact-schema.md +41 -38
  47. package/pack/generated/devin/skills/rite-autocomplete/SKILL.md +35 -38
  48. package/pack/generated/devin/skills/rite-autocomplete/reference/loop.md +66 -59
  49. package/pack/generated/devin/skills/rite-autocomplete/reference/stop-conditions.md +28 -28
  50. package/pack/generated/devin/skills/rite-build/reference/parallel-batch.md +17 -15
  51. package/pack/generated/dsh/skills/devrites-lib/SKILL.md +2 -0
  52. package/pack/generated/dsh/skills/devrites-lib/reference/parallel-workspaces.md +108 -115
  53. package/pack/generated/dsh/skills/devrites-lib/reference/standards/afk-hitl.md +18 -17
  54. package/pack/generated/dsh/skills/devrites-lib/reference/workspace-artifact-schema.md +41 -38
  55. package/pack/generated/dsh/skills/rite-autocomplete/SKILL.md +35 -38
  56. package/pack/generated/dsh/skills/rite-autocomplete/reference/loop.md +66 -59
  57. package/pack/generated/dsh/skills/rite-autocomplete/reference/stop-conditions.md +28 -28
  58. package/pack/generated/dsh/skills/rite-build/reference/parallel-batch.md +17 -15
  59. package/pack/generated/omp/skills/devrites-lib/SKILL.md +2 -0
  60. package/pack/generated/omp/skills/devrites-lib/reference/parallel-workspaces.md +108 -115
  61. package/pack/generated/omp/skills/devrites-lib/reference/standards/afk-hitl.md +18 -17
  62. package/pack/generated/omp/skills/devrites-lib/reference/workspace-artifact-schema.md +41 -38
  63. package/pack/generated/omp/skills/rite-autocomplete/SKILL.md +35 -38
  64. package/pack/generated/omp/skills/rite-autocomplete/reference/loop.md +66 -59
  65. package/pack/generated/omp/skills/rite-autocomplete/reference/stop-conditions.md +28 -28
  66. package/pack/generated/omp/skills/rite-build/reference/parallel-batch.md +17 -15
  67. package/pack/generated/pi/skills/devrites-lib/SKILL.md +2 -0
  68. package/pack/generated/pi/skills/devrites-lib/reference/parallel-workspaces.md +108 -115
  69. package/pack/generated/pi/skills/devrites-lib/reference/standards/afk-hitl.md +18 -17
  70. package/pack/generated/pi/skills/devrites-lib/reference/workspace-artifact-schema.md +41 -38
  71. package/pack/generated/pi/skills/rite-autocomplete/SKILL.md +35 -38
  72. package/pack/generated/pi/skills/rite-autocomplete/reference/loop.md +66 -59
  73. package/pack/generated/pi/skills/rite-autocomplete/reference/stop-conditions.md +28 -28
  74. package/pack/generated/pi/skills/rite-build/reference/parallel-batch.md +17 -15
  75. 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
- if policy.Target == state.PhaseClarify && openQuestions == 0 {
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