devrites 4.5.0 → 4.7.0

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 (191) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/NOTICE.md +0 -13
  3. package/README.md +4 -3
  4. package/docs/adr/0029-v5-workspace-schema-and-native-migration.md +62 -0
  5. package/docs/adr/README.md +3 -2
  6. package/docs/architecture.md +1 -1
  7. package/docs/cli.md +2 -2
  8. package/docs/command-map.md +2 -2
  9. package/docs/engine/commands.md +9 -0
  10. package/docs/engine/state-schema.md +2 -2
  11. package/docs/engine/workspace-schema.md +9 -3
  12. package/docs/usage.md +1 -1
  13. package/engine/commands.go +23 -0
  14. package/engine/internal/fsutil/copy_test.go +193 -0
  15. package/engine/internal/gate/gate_test.go +7 -7
  16. package/engine/internal/gate/readiness_binding_test.go +8 -2
  17. package/engine/internal/install/apply.go +314 -0
  18. package/engine/internal/install/binary.go +255 -0
  19. package/engine/internal/install/install.go +0 -1596
  20. package/engine/internal/install/manifest.go +109 -0
  21. package/engine/internal/install/merge.go +468 -0
  22. package/engine/internal/install/preflight.go +234 -0
  23. package/engine/internal/install/uninstall.go +119 -0
  24. package/engine/internal/install/update.go +184 -0
  25. package/engine/internal/lib/candidate_test.go +3 -0
  26. package/engine/internal/lib/closeout.go +4 -0
  27. package/engine/internal/lib/closeout_test.go +3 -3
  28. package/engine/internal/lib/migrate.go +219 -0
  29. package/engine/internal/lib/migrate_test.go +154 -0
  30. package/engine/internal/lib/resolve.go +3 -0
  31. package/engine/internal/lib/resolve_remediation_test.go +1 -1
  32. package/engine/internal/lib/secretscan_test.go +1 -0
  33. package/engine/internal/lib/taskgraph_test.go +2 -2
  34. package/engine/internal/parallel/ops.go +67 -16
  35. package/engine/internal/parallel/ops_test.go +211 -0
  36. package/engine/internal/state/cmd/workflowmanifest/main.go +24 -13
  37. package/engine/internal/state/cmd/workflowmanifest/main_test.go +96 -0
  38. package/engine/internal/state/cursor.go +74 -0
  39. package/engine/internal/state/schema.go +3 -2
  40. package/engine/internal/state/state_test.go +2 -2
  41. package/engine/internal/state/workflow_manifest.json +1 -1
  42. package/engine/internal/state/workspaceschema.go +58 -0
  43. package/engine/internal/state/workspaceschema_test.go +67 -0
  44. package/engine/main.go +7 -31
  45. package/engine/root_routing_test.go +10 -2
  46. package/engine/testdata/fixtures/basic/devrites-root/work/auth-tokens/state.md +1 -0
  47. package/engine/testdata/fixtures/basic/devrites-root/work/search-ranking/state.md +1 -0
  48. package/engine/testdata/golden/TestParityResolve/answer/state.md.golden +1 -0
  49. package/engine/testdata/golden/TestParityResolve/drop/state.md.golden +1 -0
  50. package/engine/tests/parity_closeout_test.go +3 -0
  51. package/engine/tests/parity_resolve_test.go +1 -0
  52. package/pack/.claude/agents/devrites-code-reviewer.md +8 -0
  53. package/pack/.claude/agents/devrites-devex-reviewer.md +7 -0
  54. package/pack/.claude/agents/devrites-doubt-reviewer.md +7 -0
  55. package/pack/.claude/agents/devrites-evidence-scout.md +4 -0
  56. package/pack/.claude/agents/devrites-frontend-reviewer.md +8 -0
  57. package/pack/.claude/agents/devrites-performance-reviewer.md +7 -0
  58. package/pack/.claude/agents/devrites-plan-reviewer.md +8 -0
  59. package/pack/.claude/agents/devrites-retrospector.md +2 -0
  60. package/pack/.claude/agents/devrites-security-auditor.md +7 -0
  61. package/pack/.claude/agents/devrites-simplifier-reviewer.md +7 -0
  62. package/pack/.claude/agents/devrites-spec-reviewer.md +7 -0
  63. package/pack/.claude/agents/devrites-strategy-reviewer.md +7 -0
  64. package/pack/.claude/agents/devrites-test-analyst.md +7 -0
  65. package/pack/.claude/skills/devrites-frontend-craft/SKILL.md +4 -7
  66. package/pack/.claude/skills/devrites-frontend-craft/reference/quality-standards.md +9 -4
  67. package/pack/.claude/skills/devrites-lib/reference/intent-map.md +7 -0
  68. package/pack/.claude/skills/devrites-lib/reference/parallel-dispatch.md +1 -1
  69. package/pack/.claude/skills/devrites-lib/reference/reply-contract.md +9 -7
  70. package/pack/.claude/skills/devrites-lib/reference/standards/agents.md +1 -1
  71. package/pack/.claude/skills/devrites-lib/reference/standards/code-review.md +6 -1
  72. package/pack/.claude/skills/devrites-lib/reference/standards/coding-style.md +4 -3
  73. package/pack/.claude/skills/devrites-lib/reference/standards/context-hygiene.md +4 -0
  74. package/pack/.claude/skills/devrites-lib/reference/standards/core.md +3 -6
  75. package/pack/.claude/skills/devrites-lib/reference/standards/data-integrity.md +4 -0
  76. package/pack/.claude/skills/devrites-lib/reference/standards/development-workflow.md +8 -0
  77. package/pack/.claude/skills/devrites-lib/reference/standards/documentation.md +6 -0
  78. package/pack/.claude/skills/devrites-lib/reference/standards/edge-case-trace.md +3 -1
  79. package/pack/.claude/skills/devrites-lib/reference/standards/elicitation.md +5 -0
  80. package/pack/.claude/skills/devrites-lib/reference/standards/error-handling.md +14 -0
  81. package/pack/.claude/skills/devrites-lib/reference/standards/integration-reliability.md +18 -6
  82. package/pack/.claude/skills/devrites-lib/reference/standards/patterns.md +16 -0
  83. package/pack/.claude/skills/devrites-lib/reference/standards/performance.md +4 -0
  84. package/pack/.claude/skills/devrites-lib/reference/standards/repository-topology.md +6 -0
  85. package/pack/.claude/skills/devrites-lib/reference/standards/security-checklist.md +3 -0
  86. package/pack/.claude/skills/devrites-lib/reference/standards/security.md +10 -2
  87. package/pack/.claude/skills/devrites-lib/reference/standards/skill-authoring.md +8 -0
  88. package/pack/.claude/skills/devrites-lib/reference/standards/spec-grammar.md +5 -6
  89. package/pack/.claude/skills/devrites-lib/reference/standards/testing.md +2 -2
  90. package/pack/.claude/skills/devrites-lib/reference/standards/tooling.md +1 -1
  91. package/pack/.claude/skills/rite-define/reference/plan-template.md +5 -2
  92. package/pack/.claude/skills/rite-learn/SKILL.md +6 -0
  93. package/pack/.claude/skills/rite-polish/SKILL.md +4 -0
  94. package/pack/.claude/skills/rite-polish/reference/anti-ai-slop.md +12 -53
  95. package/pack/.claude/skills/rite-pr-feedback/SKILL.md +7 -2
  96. package/pack/.claude/skills/rite-spec/reference/spec-template.md +5 -4
  97. package/pack/.claude/skills/rite-status/SKILL.md +2 -0
  98. package/pack/generated/claude/agents/devrites-code-reviewer.md +8 -0
  99. package/pack/generated/claude/agents/devrites-devex-reviewer.md +7 -0
  100. package/pack/generated/claude/agents/devrites-doubt-reviewer.md +7 -0
  101. package/pack/generated/claude/agents/devrites-evidence-scout.md +4 -0
  102. package/pack/generated/claude/agents/devrites-frontend-reviewer.md +8 -0
  103. package/pack/generated/claude/agents/devrites-performance-reviewer.md +7 -0
  104. package/pack/generated/claude/agents/devrites-plan-reviewer.md +8 -0
  105. package/pack/generated/claude/agents/devrites-retrospector.md +2 -0
  106. package/pack/generated/claude/agents/devrites-security-auditor.md +7 -0
  107. package/pack/generated/claude/agents/devrites-simplifier-reviewer.md +7 -0
  108. package/pack/generated/claude/agents/devrites-spec-reviewer.md +7 -0
  109. package/pack/generated/claude/agents/devrites-strategy-reviewer.md +7 -0
  110. package/pack/generated/claude/agents/devrites-test-analyst.md +7 -0
  111. package/pack/generated/claude/skills/devrites-frontend-craft/SKILL.md +4 -7
  112. package/pack/generated/claude/skills/devrites-frontend-craft/reference/quality-standards.md +9 -4
  113. package/pack/generated/claude/skills/devrites-lib/reference/intent-map.md +7 -0
  114. package/pack/generated/claude/skills/devrites-lib/reference/parallel-dispatch.md +1 -1
  115. package/pack/generated/claude/skills/devrites-lib/reference/reply-contract.md +9 -7
  116. package/pack/generated/claude/skills/devrites-lib/reference/standards/agents.md +1 -1
  117. package/pack/generated/claude/skills/devrites-lib/reference/standards/code-review.md +6 -1
  118. package/pack/generated/claude/skills/devrites-lib/reference/standards/coding-style.md +4 -3
  119. package/pack/generated/claude/skills/devrites-lib/reference/standards/context-hygiene.md +4 -0
  120. package/pack/generated/claude/skills/devrites-lib/reference/standards/core.md +3 -6
  121. package/pack/generated/claude/skills/devrites-lib/reference/standards/data-integrity.md +4 -0
  122. package/pack/generated/claude/skills/devrites-lib/reference/standards/development-workflow.md +8 -0
  123. package/pack/generated/claude/skills/devrites-lib/reference/standards/documentation.md +6 -0
  124. package/pack/generated/claude/skills/devrites-lib/reference/standards/edge-case-trace.md +3 -1
  125. package/pack/generated/claude/skills/devrites-lib/reference/standards/elicitation.md +5 -0
  126. package/pack/generated/claude/skills/devrites-lib/reference/standards/error-handling.md +14 -0
  127. package/pack/generated/claude/skills/devrites-lib/reference/standards/integration-reliability.md +18 -6
  128. package/pack/generated/claude/skills/devrites-lib/reference/standards/patterns.md +16 -0
  129. package/pack/generated/claude/skills/devrites-lib/reference/standards/performance.md +4 -0
  130. package/pack/generated/claude/skills/devrites-lib/reference/standards/repository-topology.md +6 -0
  131. package/pack/generated/claude/skills/devrites-lib/reference/standards/security-checklist.md +3 -0
  132. package/pack/generated/claude/skills/devrites-lib/reference/standards/security.md +10 -2
  133. package/pack/generated/claude/skills/devrites-lib/reference/standards/skill-authoring.md +8 -0
  134. package/pack/generated/claude/skills/devrites-lib/reference/standards/spec-grammar.md +5 -6
  135. package/pack/generated/claude/skills/devrites-lib/reference/standards/testing.md +2 -2
  136. package/pack/generated/claude/skills/devrites-lib/reference/standards/tooling.md +1 -1
  137. package/pack/generated/claude/skills/rite-define/reference/plan-template.md +5 -2
  138. package/pack/generated/claude/skills/rite-learn/SKILL.md +6 -0
  139. package/pack/generated/claude/skills/rite-polish/SKILL.md +4 -0
  140. package/pack/generated/claude/skills/rite-polish/reference/anti-ai-slop.md +12 -53
  141. package/pack/generated/claude/skills/rite-pr-feedback/SKILL.md +7 -2
  142. package/pack/generated/claude/skills/rite-spec/reference/spec-template.md +5 -4
  143. package/pack/generated/claude/skills/rite-status/SKILL.md +2 -0
  144. package/pack/generated/codex/agents/devrites-code-reviewer.toml +8 -0
  145. package/pack/generated/codex/agents/devrites-devex-reviewer.toml +7 -0
  146. package/pack/generated/codex/agents/devrites-doubt-reviewer.toml +7 -0
  147. package/pack/generated/codex/agents/devrites-evidence-scout.toml +4 -0
  148. package/pack/generated/codex/agents/devrites-frontend-reviewer.toml +8 -0
  149. package/pack/generated/codex/agents/devrites-performance-reviewer.toml +7 -0
  150. package/pack/generated/codex/agents/devrites-plan-reviewer.toml +8 -0
  151. package/pack/generated/codex/agents/devrites-retrospector.toml +2 -0
  152. package/pack/generated/codex/agents/devrites-security-auditor.toml +7 -0
  153. package/pack/generated/codex/agents/devrites-simplifier-reviewer.toml +7 -0
  154. package/pack/generated/codex/agents/devrites-spec-reviewer.toml +7 -0
  155. package/pack/generated/codex/agents/devrites-strategy-reviewer.toml +7 -0
  156. package/pack/generated/codex/agents/devrites-test-analyst.toml +7 -0
  157. package/pack/generated/codex/skills/devrites-frontend-craft/SKILL.md +4 -7
  158. package/pack/generated/codex/skills/devrites-frontend-craft/reference/quality-standards.md +9 -4
  159. package/pack/generated/codex/skills/devrites-lib/reference/intent-map.md +7 -0
  160. package/pack/generated/codex/skills/devrites-lib/reference/parallel-dispatch.md +1 -1
  161. package/pack/generated/codex/skills/devrites-lib/reference/reply-contract.md +9 -7
  162. package/pack/generated/codex/skills/devrites-lib/reference/standards/agents.md +1 -1
  163. package/pack/generated/codex/skills/devrites-lib/reference/standards/code-review.md +6 -1
  164. package/pack/generated/codex/skills/devrites-lib/reference/standards/coding-style.md +4 -3
  165. package/pack/generated/codex/skills/devrites-lib/reference/standards/context-hygiene.md +4 -0
  166. package/pack/generated/codex/skills/devrites-lib/reference/standards/core.md +3 -6
  167. package/pack/generated/codex/skills/devrites-lib/reference/standards/data-integrity.md +4 -0
  168. package/pack/generated/codex/skills/devrites-lib/reference/standards/development-workflow.md +8 -0
  169. package/pack/generated/codex/skills/devrites-lib/reference/standards/documentation.md +6 -0
  170. package/pack/generated/codex/skills/devrites-lib/reference/standards/edge-case-trace.md +3 -1
  171. package/pack/generated/codex/skills/devrites-lib/reference/standards/elicitation.md +5 -0
  172. package/pack/generated/codex/skills/devrites-lib/reference/standards/error-handling.md +14 -0
  173. package/pack/generated/codex/skills/devrites-lib/reference/standards/integration-reliability.md +18 -6
  174. package/pack/generated/codex/skills/devrites-lib/reference/standards/patterns.md +16 -0
  175. package/pack/generated/codex/skills/devrites-lib/reference/standards/performance.md +4 -0
  176. package/pack/generated/codex/skills/devrites-lib/reference/standards/repository-topology.md +6 -0
  177. package/pack/generated/codex/skills/devrites-lib/reference/standards/security-checklist.md +3 -0
  178. package/pack/generated/codex/skills/devrites-lib/reference/standards/security.md +10 -2
  179. package/pack/generated/codex/skills/devrites-lib/reference/standards/skill-authoring.md +8 -0
  180. package/pack/generated/codex/skills/devrites-lib/reference/standards/spec-grammar.md +5 -6
  181. package/pack/generated/codex/skills/devrites-lib/reference/standards/testing.md +2 -2
  182. package/pack/generated/codex/skills/devrites-lib/reference/standards/tooling.md +1 -1
  183. package/pack/generated/codex/skills/rite-define/reference/plan-template.md +5 -2
  184. package/pack/generated/codex/skills/rite-learn/SKILL.md +6 -0
  185. package/pack/generated/codex/skills/rite-polish/SKILL.md +4 -0
  186. package/pack/generated/codex/skills/rite-polish/reference/anti-ai-slop.md +12 -53
  187. package/pack/generated/codex/skills/rite-pr-feedback/SKILL.md +7 -2
  188. package/pack/generated/codex/skills/rite-spec/reference/spec-template.md +5 -4
  189. package/pack/generated/codex/skills/rite-status/SKILL.md +2 -0
  190. package/package.json +1 -1
  191. package/docs/markdown-instruction-upgrade-2026-08-27.md +0 -127
@@ -0,0 +1,234 @@
1
+ package install
2
+
3
+ import (
4
+ "crypto/sha256"
5
+ "encoding/hex"
6
+ "fmt"
7
+ "io"
8
+ "io/fs"
9
+ "os"
10
+ "path/filepath"
11
+ "sort"
12
+ "strings"
13
+
14
+ "github.com/devrites/devrites/internal/hostpack"
15
+ "github.com/devrites/devrites/internal/safepath"
16
+ )
17
+
18
+ func (r *runner) preflightInstall() error {
19
+ desired, err := r.desiredInstallPaths()
20
+ if err != nil {
21
+ return fmt.Errorf("preflight install paths: %w", err)
22
+ }
23
+ var conflicts []string
24
+ for rel := range desired {
25
+ snapshot, err := r.rememberPath(rel)
26
+ if err != nil {
27
+ return err
28
+ }
29
+ record, managed := r.prev[rel]
30
+ if managed && !snapshot.missing {
31
+ if kind := managedConflict(record, snapshot); kind != "" && !r.opts.Force {
32
+ conflicts = append(conflicts, fmt.Sprintf("%s: %s", kind, rel))
33
+ }
34
+ }
35
+ }
36
+ for rel, record := range r.prev {
37
+ if desired[rel] || hostpack.PreserveOnPrune(rel) {
38
+ continue
39
+ }
40
+ snapshot, err := r.rememberPath(rel)
41
+ if err != nil {
42
+ return err
43
+ }
44
+ if !snapshot.missing {
45
+ if kind := managedConflict(record, snapshot); kind != "" && !r.opts.Force {
46
+ conflicts = append(conflicts, fmt.Sprintf("%s: %s", kind, rel))
47
+ }
48
+ }
49
+ if merge, ok := hostpack.ManagedMergeForMarker(rel); ok {
50
+ if _, err := r.rememberPath(merge.TargetRel); err != nil {
51
+ return err
52
+ }
53
+ }
54
+ }
55
+ for _, rel := range r.installMergeTargets() {
56
+ if _, err := r.rememberPath(rel); err != nil {
57
+ return err
58
+ }
59
+ }
60
+ if _, err := r.rememberPath(ManifestName); err != nil {
61
+ return err
62
+ }
63
+ return managedConflictError(conflicts)
64
+ }
65
+
66
+ func (r *runner) preflightUninstall(entries []string) error {
67
+ var conflicts []string
68
+ for _, rel := range entries {
69
+ if merge, ok := hostpack.ManagedMergeForMarker(rel); ok {
70
+ if _, err := r.rememberPath(merge.TargetRel); err != nil {
71
+ return err
72
+ }
73
+ }
74
+ if !hostpack.ShouldRemoveOnUninstall(rel, entries) {
75
+ continue
76
+ }
77
+ snapshot, err := r.rememberPath(rel)
78
+ if err != nil {
79
+ return err
80
+ }
81
+ if snapshot.missing {
82
+ continue
83
+ }
84
+ if kind := managedConflict(r.prev[rel], snapshot); kind != "" && !r.opts.Force {
85
+ conflicts = append(conflicts, fmt.Sprintf("%s: %s", kind, rel))
86
+ }
87
+ }
88
+ if _, err := r.rememberPath(ManifestName); err != nil {
89
+ return err
90
+ }
91
+ return managedConflictError(conflicts)
92
+ }
93
+
94
+ func (r *runner) desiredInstallPaths() (map[string]bool, error) {
95
+ out := map[string]bool{".devrites/README.md": true}
96
+ for _, tree := range hostpack.InstallTrees(r.opts.WithSkills, r.opts.WithAgents, r.opts.WithCodex) {
97
+ err := fs.WalkDir(r.payloadFS, tree.PayloadPrefix, func(path string, d fs.DirEntry, err error) error {
98
+ if err != nil {
99
+ return err
100
+ }
101
+ if d.IsDir() {
102
+ return nil
103
+ }
104
+ rel, err := filepath.Rel(tree.PayloadPrefix, path)
105
+ if err != nil {
106
+ return err
107
+ }
108
+ out[filepath.ToSlash(filepath.Join(tree.TargetPrefix, rel))] = true
109
+ return nil
110
+ })
111
+ if err != nil {
112
+ return nil, err
113
+ }
114
+ }
115
+ if r.opts.WithSkills && r.opts.AliasMode == "all" {
116
+ for _, alias := range hostpack.Aliases {
117
+ for _, rel := range hostpack.AliasTargets(alias, r.opts.WithCodex) {
118
+ out[rel] = true
119
+ }
120
+ }
121
+ }
122
+ if r.opts.WithSkills && r.opts.WithCodex {
123
+ out[hostpack.CodexAgentsMerge.MarkerRel] = true
124
+ out[hostpack.CodexConfigMerge.MarkerRel] = true
125
+ }
126
+ if r.opts.WithSkills {
127
+ out[hostpack.ClaudeSettingsMerge.MarkerRel] = true
128
+ }
129
+ return out, nil
130
+ }
131
+
132
+ func (r *runner) installMergeTargets() []string {
133
+ var out []string
134
+ if r.opts.WithSkills {
135
+ out = append(out, hostpack.ClaudeSettingsMerge.TargetRel)
136
+ if r.opts.WithCodex {
137
+ out = append(out, hostpack.CodexAgentsMerge.TargetRel, hostpack.CodexConfigMerge.TargetRel)
138
+ }
139
+ }
140
+ return out
141
+ }
142
+
143
+ func (r *runner) rememberPath(rel string) (pathSnapshot, error) {
144
+ rel = filepath.ToSlash(rel)
145
+ if snapshot, ok := r.preflight[rel]; ok {
146
+ return snapshot, nil
147
+ }
148
+ snapshot, err := inspectManagedPath(r.target, rel)
149
+ if err != nil {
150
+ return pathSnapshot{}, err
151
+ }
152
+ r.preflight[rel] = snapshot
153
+ return snapshot, nil
154
+ }
155
+
156
+ func (r *runner) recheckPath(rel string) error {
157
+ rel = filepath.ToSlash(rel)
158
+ before, ok := r.preflight[rel]
159
+ if !ok {
160
+ return fmt.Errorf("path %s was not preflighted", rel)
161
+ }
162
+ now, err := inspectManagedPath(r.target, rel)
163
+ if err != nil {
164
+ return err
165
+ }
166
+ if now != before {
167
+ return fmt.Errorf("refusing to change %s: file changed after preflight; retry the operation", rel)
168
+ }
169
+ return nil
170
+ }
171
+
172
+ func inspectManagedPath(target, rel string) (pathSnapshot, error) {
173
+ native := filepath.FromSlash(rel)
174
+ if !filepath.IsLocal(native) {
175
+ return pathSnapshot{}, fmt.Errorf("refusing unsafe managed path %q", rel)
176
+ }
177
+ dest := filepath.Join(target, native)
178
+ if !safepath.WithinResolved(dest, target) {
179
+ return pathSnapshot{}, fmt.Errorf("refusing managed path outside target: %s", rel)
180
+ }
181
+ walk := target
182
+ parts := strings.Split(native, string(filepath.Separator))
183
+ for _, part := range parts {
184
+ walk = filepath.Join(walk, part)
185
+ info, err := os.Lstat(walk)
186
+ if os.IsNotExist(err) {
187
+ return pathSnapshot{missing: true}, nil
188
+ }
189
+ if err != nil {
190
+ return pathSnapshot{}, fmt.Errorf("inspect managed path %s: %w", rel, err)
191
+ }
192
+ if info.Mode()&os.ModeSymlink != 0 {
193
+ return pathSnapshot{}, fmt.Errorf("refusing symlink or junction in managed path: %s", rel)
194
+ }
195
+ }
196
+ info, err := os.Lstat(dest)
197
+ if os.IsNotExist(err) {
198
+ return pathSnapshot{missing: true}, nil
199
+ }
200
+ if err != nil {
201
+ return pathSnapshot{}, fmt.Errorf("inspect managed path %s: %w", rel, err)
202
+ }
203
+ if !info.Mode().IsRegular() {
204
+ return pathSnapshot{}, fmt.Errorf("refusing non-regular managed path: %s", rel)
205
+ }
206
+ f, err := os.Open(dest)
207
+ if err != nil {
208
+ return pathSnapshot{}, fmt.Errorf("read managed path %s: %w", rel, err)
209
+ }
210
+ defer f.Close()
211
+ sum := sha256.New()
212
+ if _, err := io.Copy(sum, f); err != nil {
213
+ return pathSnapshot{}, fmt.Errorf("hash managed path %s: %w", rel, err)
214
+ }
215
+ return pathSnapshot{hash: "sha256:" + hex.EncodeToString(sum.Sum(nil))}, nil
216
+ }
217
+
218
+ func managedConflict(record managedRecord, snapshot pathSnapshot) string {
219
+ if record.Hash == "" {
220
+ return "legacy manifest entry (no hash)"
221
+ }
222
+ if record.Hash != snapshot.hash {
223
+ return "customized managed file"
224
+ }
225
+ return ""
226
+ }
227
+
228
+ func managedConflictError(conflicts []string) error {
229
+ if len(conflicts) == 0 {
230
+ return nil
231
+ }
232
+ sort.Strings(conflicts)
233
+ return fmt.Errorf("managed files differ from the install manifest:\n %s\nrerun with --force to replace or remove these files", strings.Join(conflicts, "\n "))
234
+ }
@@ -0,0 +1,119 @@
1
+ package install
2
+
3
+ import (
4
+ "fmt"
5
+ "os"
6
+ "path/filepath"
7
+
8
+ "github.com/devrites/devrites/internal/hostpack"
9
+ )
10
+
11
+ func (r *runner) uninstall() error {
12
+ mf := filepath.Join(r.target, ManifestName)
13
+ entries, err := readManifestList(mf)
14
+ if err != nil {
15
+ return err
16
+ }
17
+ if len(entries) == 0 {
18
+ return fmt.Errorf("no DevRites manifest at %s - nothing to uninstall", mf)
19
+ }
20
+ fmt.Fprintln(r.opts.Stdout, "DevRites uninstaller")
21
+ fmt.Fprintf(r.opts.Stdout, " target : %s\n", r.target)
22
+ fmt.Fprintf(r.opts.Stdout, " manifest: %s\n", mf)
23
+ if r.opts.DryRun {
24
+ fmt.Fprintln(r.opts.Stdout, " (dry run - no changes)")
25
+ }
26
+ fmt.Fprintln(r.opts.Stdout)
27
+ if err := r.preflightUninstall(entries); err != nil {
28
+ return err
29
+ }
30
+
31
+ for _, rel := range entries {
32
+ merge, ok := hostpack.ManagedMergeForMarker(rel)
33
+ if !ok {
34
+ continue
35
+ }
36
+ if r.opts.DryRun {
37
+ fmt.Fprintf(r.opts.Stdout, " [merge-remove] %s\n", merge.DryRun)
38
+ continue
39
+ }
40
+ if err := r.recheckPath(rel); err != nil {
41
+ return err
42
+ }
43
+ if err := r.recheckPath(merge.TargetRel); err != nil {
44
+ return err
45
+ }
46
+ if merge.MarkerRel == hostpack.LegacyCodexHooksMerge.MarkerRel {
47
+ if err := stripHooksPath(filepath.Join(r.target, filepath.FromSlash(merge.TargetRel))); err != nil {
48
+ return fmt.Errorf("strip hooks from %s: %w", merge.TargetRel, err)
49
+ }
50
+ continue
51
+ }
52
+ if merge.TargetRel == hostpack.ClaudeSettingsMerge.TargetRel {
53
+ if err := r.stripClaudeSettings(filepath.Join(r.target, filepath.FromSlash(merge.TargetRel)), true); err != nil {
54
+ return fmt.Errorf("strip DevRites settings from %s: %w", merge.TargetRel, err)
55
+ }
56
+ continue
57
+ }
58
+ if err := stripMarkerPath(filepath.Join(r.target, filepath.FromSlash(merge.TargetRel)), merge.Begin, merge.End); err != nil {
59
+ return fmt.Errorf("strip marker block from %s: %w", merge.TargetRel, err)
60
+ }
61
+ }
62
+
63
+ var dirs []string
64
+ for _, rel := range entries {
65
+ if !hostpack.ShouldRemoveOnUninstall(rel, entries) {
66
+ continue
67
+ }
68
+ dest := filepath.Join(r.target, filepath.FromSlash(rel))
69
+ if exists(dest) {
70
+ action := "remove"
71
+ if managedConflict(r.prev[rel], r.preflight[rel]) != "" && r.opts.Force {
72
+ action = "remove(force-customized)"
73
+ }
74
+ if r.opts.DryRun {
75
+ fmt.Fprintf(r.opts.Stdout, " [%s] %s\n", action, rel)
76
+ } else {
77
+ if err := r.recheckPath(rel); err != nil {
78
+ return err
79
+ }
80
+ if err := os.Remove(dest); err != nil && !os.IsNotExist(err) {
81
+ return fmt.Errorf("remove %s: %w", rel, err)
82
+ }
83
+ }
84
+ dirs = append(dirs, filepath.Dir(dest))
85
+ r.stats.removed++
86
+ } else {
87
+ r.stats.missing++
88
+ }
89
+ }
90
+ if r.opts.DryRun {
91
+ fmt.Fprintf(r.opts.Stdout, " [remove] %s\n", ManifestName)
92
+ } else {
93
+ if err := r.recheckPath(ManifestName); err != nil {
94
+ return err
95
+ }
96
+ _ = os.Remove(mf)
97
+ dirs = append(dirs, filepath.Dir(mf))
98
+ for _, d := range dirs {
99
+ pruneEmptyDirs(d, r.target)
100
+ }
101
+ }
102
+ if err := r.removeBinary(); err != nil {
103
+ return fmt.Errorf("remove engine binary: %w", err)
104
+ }
105
+ fmt.Fprintln(r.opts.Stdout)
106
+ if r.opts.DryRun {
107
+ fmt.Fprintln(r.opts.Stdout, "DevRites uninstall plan complete (dry run)")
108
+ } else {
109
+ fmt.Fprintln(r.opts.Stdout, "DevRites uninstalled")
110
+ }
111
+ fmt.Fprintf(r.opts.Stdout, " removed: %d already-absent: %d\n", r.stats.removed, r.stats.missing)
112
+ if exists(filepath.Join(r.target, ".devrites", "work")) {
113
+ fmt.Fprintln(r.opts.Stdout, " kept .devrites/work/ (your feature data)")
114
+ }
115
+ if exists(filepath.Join(r.target, ".devrites", "ACTIVE")) {
116
+ fmt.Fprintln(r.opts.Stdout, " kept .devrites/ACTIVE (active-feature cursor)")
117
+ }
118
+ return nil
119
+ }
@@ -0,0 +1,184 @@
1
+ package install
2
+
3
+ import (
4
+ "context"
5
+ "fmt"
6
+ "os"
7
+ "os/exec"
8
+ "path/filepath"
9
+ "strings"
10
+ "time"
11
+
12
+ "github.com/devrites/devrites/internal/hostpack"
13
+ "github.com/devrites/devrites/internal/release"
14
+ )
15
+
16
+ func runUpdate(opts Options) error {
17
+ target, err := filepath.Abs(opts.Target)
18
+ if err != nil {
19
+ return fmt.Errorf("resolve target %s: %w", opts.Target, err)
20
+ }
21
+ mf := filepath.Join(target, ManifestName)
22
+ if !exists(mf) {
23
+ return fmt.Errorf("no DevRites install found at %s (missing %s)", target, ManifestName)
24
+ }
25
+ installed := manifestHeader(mf, "devrites-version")
26
+ if installed == "" {
27
+ installed = "unknown"
28
+ }
29
+ if opts.SourceDir == "" {
30
+ if opts.PayloadDir != "" {
31
+ return fmt.Errorf("local update with --payload-dir also requires --source-dir")
32
+ }
33
+ return runRemoteUpdate(opts, target, installed)
34
+ }
35
+ source, err := filepath.Abs(opts.SourceDir)
36
+ if err != nil {
37
+ return fmt.Errorf("resolve source %s: %w", opts.SourceDir, err)
38
+ }
39
+ candidate := installedVersion(source)
40
+ if candidate == "" || candidate == "unknown" {
41
+ return fmt.Errorf("derive update version from local source %s: package.json has no version", source)
42
+ }
43
+ candidate = strings.TrimPrefix(candidate, "v")
44
+ if os.Getenv("DEVRITES_UPDATE_HANDOFF") != "1" {
45
+ fmt.Fprintln(opts.Stdout, "DevRites update")
46
+ fmt.Fprintf(opts.Stdout, " project: %s\n", target)
47
+ fmt.Fprintf(opts.Stdout, " installed: %s\n", installed)
48
+ fmt.Fprintf(opts.Stdout, " candidate: %s\n", candidate)
49
+ }
50
+ if opts.UpdateCheck {
51
+ if installed == candidate || strings.TrimPrefix(installed, "v") == candidate {
52
+ fmt.Fprintln(opts.Stdout, "up to date.")
53
+ return nil
54
+ }
55
+ return fmt.Errorf("update available: %s -> %s", installed, candidate)
56
+ }
57
+ if !opts.Force && (installed == candidate || strings.TrimPrefix(installed, "v") == candidate) {
58
+ fmt.Fprintln(opts.Stdout, "already up to date (use --force to reinstall).")
59
+ return nil
60
+ }
61
+ if opts.PayloadDir == "" {
62
+ return fmt.Errorf("update requires --payload-dir with the locally prepared host payload")
63
+ }
64
+ payload, err := filepath.Abs(opts.PayloadDir)
65
+ if err != nil {
66
+ return fmt.Errorf("resolve payload %s: %w", opts.PayloadDir, err)
67
+ }
68
+ if err := hostpack.ValidatePayload(os.DirFS(payload), true); err != nil {
69
+ return fmt.Errorf("local update payload under %s is invalid: %w", payload, err)
70
+ }
71
+ installFlags := manifestHeader(mf, "devrites-flags")
72
+ next := DefaultOptions(ModeInstall)
73
+ next.Stdout = opts.Stdout
74
+ next.Stderr = opts.Stderr
75
+ if installFlags != "" {
76
+ if err := parseArgs(strings.Fields(installFlags), &next); err != nil {
77
+ return fmt.Errorf("parse manifest install flags: %w", err)
78
+ }
79
+ }
80
+ next.Target = target
81
+ next.PayloadDir = payload
82
+ next.SourceDir = source
83
+ next.Force = opts.Force
84
+ if opts.DryRun {
85
+ next.DryRun = true
86
+ }
87
+ r, err := newRunner(next)
88
+ if err != nil {
89
+ return fmt.Errorf("prepare installer: %w", err)
90
+ }
91
+ r.requiredBinaryTag = "v" + candidate
92
+ if next.WithBinary && os.Getenv("DEVRITES_NO_BINARY") != "1" && !next.DryRun {
93
+ staged, cleanup, err := r.acquireBinary(r.requiredBinaryTag)
94
+ if err != nil {
95
+ return fmt.Errorf("prepare engine binary: %w", err)
96
+ }
97
+ defer cleanup()
98
+ r.preparedBinary = staged
99
+ }
100
+ return r.install()
101
+ }
102
+
103
+ var (
104
+ resolveLatestRelease = release.Latest
105
+ acquireRelease = release.Acquire
106
+ )
107
+
108
+ func runRemoteUpdate(opts Options, target, installed string) error {
109
+ repository := os.Getenv("DEVRITES_REPO")
110
+ if repository == "" {
111
+ repository = release.DefaultRepository
112
+ }
113
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
114
+ defer cancel()
115
+ tag, err := resolveLatestRelease(ctx, repository)
116
+ if err != nil {
117
+ return err
118
+ }
119
+ latest := strings.TrimPrefix(tag, "v")
120
+ fmt.Fprintln(opts.Stdout, "DevRites update")
121
+ fmt.Fprintf(opts.Stdout, " project: %s\n", target)
122
+ fmt.Fprintf(opts.Stdout, " installed: %s\n", installed)
123
+ fmt.Fprintf(opts.Stdout, " latest: %s\n", latest)
124
+ if opts.UpdateCheck {
125
+ if installed == latest || strings.TrimPrefix(installed, "v") == latest {
126
+ fmt.Fprintln(opts.Stdout, "up to date.")
127
+ return nil
128
+ }
129
+ return fmt.Errorf("update available: %s -> %s", installed, latest)
130
+ }
131
+ if !opts.Force && (installed == latest || strings.TrimPrefix(installed, "v") == latest) {
132
+ fmt.Fprintln(opts.Stdout, "already up to date (use --force to reinstall).")
133
+ return nil
134
+ }
135
+ candidate, cleanup, err := acquireRelease(ctx, repository, tag)
136
+ if err != nil {
137
+ return fmt.Errorf("acquire update %s: %w", tag, err)
138
+ }
139
+ defer cleanup()
140
+ fmt.Fprintf(opts.Stdout, " bundle: %s\n", candidate.BundleURL)
141
+ if _, err := verifyEngineBinary(candidate.EnginePath, tag, 30*time.Second); err != nil {
142
+ return fmt.Errorf("verify update engine: %w", err)
143
+ }
144
+ args := []string{
145
+ "update",
146
+ "--target", target,
147
+ "--source-dir", candidate.SourceDir,
148
+ "--payload-dir", candidate.PayloadDir,
149
+ }
150
+ if opts.DryRun {
151
+ args = append(args, "--dry-run")
152
+ }
153
+ if opts.Force {
154
+ args = append(args, "--force")
155
+ }
156
+ cmd := exec.CommandContext(ctx, candidate.EnginePath, args...)
157
+ cmd.Stdout = opts.Stdout
158
+ cmd.Stderr = opts.Stderr
159
+ cmd.Env = replaceEnv(os.Environ(), map[string]string{
160
+ "DEVRITES_ENGINE_CLI": candidate.EnginePath,
161
+ "DEVRITES_UPDATE_HANDOFF": "1",
162
+ })
163
+ if err := cmd.Run(); err != nil {
164
+ return fmt.Errorf("apply update %s: %w", tag, err)
165
+ }
166
+ return nil
167
+ }
168
+
169
+ func replaceEnv(env []string, replacements map[string]string) []string {
170
+ out := make([]string, 0, len(env)+len(replacements))
171
+ for _, entry := range env {
172
+ name, _, found := strings.Cut(entry, "=")
173
+ if found {
174
+ if _, replace := replacements[name]; replace {
175
+ continue
176
+ }
177
+ }
178
+ out = append(out, entry)
179
+ }
180
+ for name, value := range replacements {
181
+ out = append(out, name+"="+value)
182
+ }
183
+ return out
184
+ }
@@ -45,6 +45,9 @@ func writeCandidateTestWorkspace(t *testing.T, manifest string) (project, root s
45
45
  t.Fatal(err)
46
46
  }
47
47
  }
48
+ if err := os.WriteFile(filepath.Join(workspace, "state.md"), []byte("| schema | 3 |\n"), 0o644); err != nil {
49
+ t.Fatal(err)
50
+ }
48
51
  return project, root
49
52
  }
50
53
 
@@ -37,6 +37,10 @@ func CloseOut(root string, args []string, stdout, stderr io.Writer) int {
37
37
  fmt.Fprintf(stderr, "state close: invalid workspace for %s: %v\n", slug, err)
38
38
  return 1
39
39
  }
40
+ if err := state.RequireWorkspaceSchema(dv, slug); err != nil {
41
+ fmt.Fprintf(stderr, "state close: %v\n", err)
42
+ return 3
43
+ }
40
44
  archiveDir, err := devritespaths.ArchiveDirChecked(dv)
41
45
  if err != nil {
42
46
  fmt.Fprintf(stderr, "state close: invalid archive directory: %v\n", err)
@@ -28,7 +28,7 @@ func runCloseOut(t *testing.T, root string, args ...string) (int, string) {
28
28
 
29
29
  func TestCloseOutRejectsRootOverrideArgument(t *testing.T) {
30
30
  root := t.TempDir()
31
- writeFile(t, filepath.Join(root, "work", "feat", "state.md"), "inside\n")
31
+ writeFile(t, filepath.Join(root, "work", "feat", "state.md"), "inside\n- Schema: 3\n")
32
32
 
33
33
  code, out := runCloseOut(t, root, "feat", t.TempDir())
34
34
  if code != 4 || !strings.Contains(out, "usage: devrites-engine state close <slug>") {
@@ -65,7 +65,7 @@ func TestCloseOutRejectsWorkspaceSymlinkEscape(t *testing.T) {
65
65
  func TestCloseOutRejectsArchiveSymlinkEscape(t *testing.T) {
66
66
  root := t.TempDir()
67
67
  outside := t.TempDir()
68
- writeFile(t, filepath.Join(root, "work", "feat", "state.md"), "inside\n")
68
+ writeFile(t, filepath.Join(root, "work", "feat", "state.md"), "inside\n- Schema: 3\n")
69
69
  if err := os.Symlink(outside, filepath.Join(root, "archive")); err != nil {
70
70
  t.Skipf("symlinks unavailable: %v", err)
71
71
  }
@@ -92,7 +92,7 @@ func TestCloseOutRollsBackArchiveWhenActiveCannotBeCleared(t *testing.T) {
92
92
  t.Skip("permission model differs on Windows")
93
93
  }
94
94
  root := t.TempDir()
95
- writeFile(t, filepath.Join(root, "work", "feat", "state.md"), "inside\n")
95
+ writeFile(t, filepath.Join(root, "work", "feat", "state.md"), "inside\n- Schema: 3\n")
96
96
  writeFile(t, filepath.Join(root, "ACTIVE"), "feat\n")
97
97
  if err := os.MkdirAll(filepath.Join(root, "archive"), 0o755); err != nil {
98
98
  t.Fatal(err)