devrites 7.3.1 → 7.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/README.md +19 -3
- package/bin/devrites.mjs +31 -5
- package/docs/engine/commands.md +35 -3
- package/docs/release.md +5 -2
- package/engine/Makefile +2 -2
- package/engine/internal/acceptance/newfinal_origins.go +39 -0
- package/engine/internal/acceptance/preconditions.go +17 -11
- package/engine/internal/acceptance/transcript.go +23 -4
- package/engine/internal/gate/gate.go +8 -6
- package/engine/internal/gate/readiness_binding.go +50 -21
- package/engine/internal/install/apply.go +36 -21
- package/engine/internal/install/binary.go +67 -9
- package/engine/internal/install/install.go +63 -21
- package/engine/internal/install/manifest.go +4 -5
- package/engine/internal/install/merge.go +89 -33
- package/engine/internal/install/preflight.go +60 -19
- package/engine/internal/install/uninstall.go +19 -9
- package/engine/internal/install/update.go +0 -4
- package/engine/internal/lib/claims.go +170 -4
- package/engine/internal/lib/claims_holder.go +13 -16
- package/engine/internal/lib/closeout.go +52 -28
- package/engine/internal/lib/context_io.go +88 -4
- package/engine/internal/lib/continuation.go +111 -24
- package/engine/internal/lib/dispatch.go +157 -24
- package/engine/internal/lib/final_prerequisites.go +137 -0
- package/engine/internal/lib/manifest_union.go +62 -25
- package/engine/internal/lib/next.go +10 -10
- package/engine/internal/lib/packet.go +32 -8
- package/engine/internal/lib/packet_integrity.go +194 -2
- package/engine/internal/lib/resolve.go +128 -142
- package/engine/internal/lib/treebaseline.go +0 -1
- package/engine/internal/markdowntext/markdowntext.go +110 -31
- package/engine/internal/overhaul/records/check.go +45 -5
- package/engine/internal/overhaul/records/consistency.go +34 -8
- package/engine/internal/overhaul/records/run.go +55 -11
- package/engine/internal/parallel/landing.go +17 -0
- package/engine/internal/state/acceptance.go +35 -5
- package/engine/internal/state/feature.go +3 -2
- package/engine/internal/state/observation.go +1 -1
- package/engine/internal/state/write.go +20 -44
- package/engine/internal/workflow/commands.go +25 -3
- package/engine/main.go +19 -0
- package/pack/.claude/skills/devrites-lib/reference/parallel-dispatch.md +41 -39
- package/pack/.claude/skills/overhaul/SKILL.md +8 -4
- package/pack/.claude/skills/overhaul/references/lifecycle.md +13 -6
- package/pack/.claude/skills/overhaul/references/orchestration.md +32 -21
- package/pack/.claude/skills/overhaul/references/records.md +23 -8
- package/pack/.claude/skills/overhaul/references/scope-and-safety.md +7 -2
- package/pack/.claude/skills/overhaul/references/scoring.md +7 -0
- package/pack/.claude/skills/rite-build/reference/wright-dispatch.md +17 -17
- package/pack/.claude/skills/rite-clarify/SKILL.md +25 -25
- package/pack/generated/claude/skills/devrites-lib/reference/parallel-dispatch.md +41 -39
- package/pack/generated/claude/skills/overhaul/SKILL.md +8 -4
- package/pack/generated/claude/skills/overhaul/references/lifecycle.md +13 -6
- package/pack/generated/claude/skills/overhaul/references/orchestration.md +32 -21
- package/pack/generated/claude/skills/overhaul/references/records.md +23 -8
- package/pack/generated/claude/skills/overhaul/references/scope-and-safety.md +7 -2
- package/pack/generated/claude/skills/overhaul/references/scoring.md +7 -0
- package/pack/generated/claude/skills/rite-build/reference/wright-dispatch.md +17 -17
- package/pack/generated/claude/skills/rite-clarify/SKILL.md +25 -25
- package/pack/generated/codex/skills/devrites-lib/reference/parallel-dispatch.md +41 -39
- package/pack/generated/codex/skills/overhaul/SKILL.md +8 -4
- package/pack/generated/codex/skills/overhaul/references/lifecycle.md +13 -6
- package/pack/generated/codex/skills/overhaul/references/orchestration.md +32 -21
- package/pack/generated/codex/skills/overhaul/references/records.md +23 -8
- package/pack/generated/codex/skills/overhaul/references/scope-and-safety.md +7 -2
- package/pack/generated/codex/skills/overhaul/references/scoring.md +7 -0
- package/pack/generated/codex/skills/rite-build/reference/wright-dispatch.md +17 -17
- package/pack/generated/codex/skills/rite-clarify/SKILL.md +25 -25
- package/pack/generated/devin/skills/devrites-lib/reference/parallel-dispatch.md +41 -39
- package/pack/generated/devin/skills/overhaul/SKILL.md +8 -4
- package/pack/generated/devin/skills/overhaul/references/lifecycle.md +13 -6
- package/pack/generated/devin/skills/overhaul/references/orchestration.md +32 -21
- package/pack/generated/devin/skills/overhaul/references/records.md +23 -8
- package/pack/generated/devin/skills/overhaul/references/scope-and-safety.md +7 -2
- package/pack/generated/devin/skills/overhaul/references/scoring.md +7 -0
- package/pack/generated/devin/skills/rite-build/reference/wright-dispatch.md +17 -17
- package/pack/generated/devin/skills/rite-clarify/SKILL.md +25 -25
- package/pack/generated/dsh/runtime/cordis.patch.yml +28 -14
- package/pack/generated/dsh/skills/devrites-lib/reference/parallel-dispatch.md +41 -39
- package/pack/generated/dsh/skills/overhaul/SKILL.md +8 -4
- package/pack/generated/dsh/skills/overhaul/references/lifecycle.md +13 -6
- package/pack/generated/dsh/skills/overhaul/references/orchestration.md +32 -21
- package/pack/generated/dsh/skills/overhaul/references/records.md +23 -8
- package/pack/generated/dsh/skills/overhaul/references/scope-and-safety.md +7 -2
- package/pack/generated/dsh/skills/overhaul/references/scoring.md +6 -0
- package/pack/generated/dsh/skills/rite-build/reference/wright-dispatch.md +17 -17
- package/pack/generated/dsh/skills/rite-clarify/SKILL.md +25 -25
- package/pack/generated/omp/skills/devrites-lib/reference/parallel-dispatch.md +41 -39
- package/pack/generated/omp/skills/overhaul/SKILL.md +8 -4
- package/pack/generated/omp/skills/overhaul/references/lifecycle.md +13 -6
- package/pack/generated/omp/skills/overhaul/references/orchestration.md +32 -21
- package/pack/generated/omp/skills/overhaul/references/records.md +23 -8
- package/pack/generated/omp/skills/overhaul/references/scope-and-safety.md +7 -2
- package/pack/generated/omp/skills/overhaul/references/scoring.md +7 -0
- package/pack/generated/omp/skills/rite-build/reference/wright-dispatch.md +17 -17
- package/pack/generated/omp/skills/rite-clarify/SKILL.md +25 -25
- package/pack/generated/pi/skills/devrites-lib/reference/parallel-dispatch.md +41 -39
- package/pack/generated/pi/skills/overhaul/SKILL.md +8 -4
- package/pack/generated/pi/skills/overhaul/references/lifecycle.md +13 -6
- package/pack/generated/pi/skills/overhaul/references/orchestration.md +32 -21
- package/pack/generated/pi/skills/overhaul/references/records.md +23 -8
- package/pack/generated/pi/skills/overhaul/references/scope-and-safety.md +7 -2
- package/pack/generated/pi/skills/overhaul/references/scoring.md +7 -0
- package/pack/generated/pi/skills/rite-build/reference/wright-dispatch.md +17 -17
- package/pack/generated/pi/skills/rite-clarify/SKILL.md +25 -25
- package/package.json +1 -1
- package/scripts/dsh-generate.py +1 -1
- package/scripts/install-lib.sh +29 -0
- package/uninstall.sh +1 -0
|
@@ -16,6 +16,10 @@ import (
|
|
|
16
16
|
)
|
|
17
17
|
|
|
18
18
|
func (r *runner) install() error {
|
|
19
|
+
if err := r.openTargetRoot(); err != nil {
|
|
20
|
+
return err
|
|
21
|
+
}
|
|
22
|
+
defer r.closeTargetRoot()
|
|
19
23
|
fmt.Fprintln(r.opts.Stdout, "DevRites installer")
|
|
20
24
|
fmt.Fprintf(r.opts.Stdout, " target : %s\n", r.target)
|
|
21
25
|
fmt.Fprintf(r.opts.Stdout, " payload: %s\n", r.payload)
|
|
@@ -43,6 +47,10 @@ func (r *runner) install() error {
|
|
|
43
47
|
if err := r.preflightInstall(); err != nil {
|
|
44
48
|
return err
|
|
45
49
|
}
|
|
50
|
+
// Complete binary installation before changing payload ownership or version.
|
|
51
|
+
if err := r.installBinary(); err != nil {
|
|
52
|
+
return fmt.Errorf("install engine binary: %w", err)
|
|
53
|
+
}
|
|
46
54
|
|
|
47
55
|
for _, tree := range hostpack.InstallTrees(r.opts.WithSkills, r.opts.WithAgents, r.opts.WithCodex, r.opts.WithOmp, r.opts.WithPi, r.opts.WithDevin, r.opts.WithDsh) {
|
|
48
56
|
if err := r.installTree(tree.PayloadPrefix, tree.TargetPrefix); err != nil {
|
|
@@ -106,9 +114,6 @@ func (r *runner) install() error {
|
|
|
106
114
|
return fmt.Errorf("write manifest: %w", err)
|
|
107
115
|
}
|
|
108
116
|
}
|
|
109
|
-
if err := r.installBinary(); err != nil {
|
|
110
|
-
return fmt.Errorf("install engine binary: %w", err)
|
|
111
|
-
}
|
|
112
117
|
|
|
113
118
|
fmt.Fprintln(r.opts.Stdout)
|
|
114
119
|
if r.opts.DryRun {
|
|
@@ -218,19 +223,32 @@ func (r *runner) installData(data []byte, rel string) error {
|
|
|
218
223
|
return nil
|
|
219
224
|
}
|
|
220
225
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
// after the path recheck cannot redirect the write.
|
|
224
|
-
func (r *runner) writeConfined(dest string, data []byte, perm fs.FileMode) error {
|
|
225
|
-
rel, err := filepath.Rel(r.target, dest)
|
|
226
|
+
func (r *runner) readConfined(path string) ([]byte, error) {
|
|
227
|
+
rel, err := filepath.Rel(r.target, path)
|
|
226
228
|
if err != nil {
|
|
227
|
-
return err
|
|
229
|
+
return nil, err
|
|
230
|
+
}
|
|
231
|
+
if r.targetRoot != nil {
|
|
232
|
+
return r.targetRoot.ReadFile(rel)
|
|
228
233
|
}
|
|
229
234
|
root, err := os.OpenRoot(r.target)
|
|
230
235
|
if err != nil {
|
|
231
|
-
return err
|
|
236
|
+
return nil, err
|
|
232
237
|
}
|
|
233
238
|
defer func() { _ = root.Close() }()
|
|
239
|
+
return root.ReadFile(rel)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// writeConfined uses the operation's validated root, not its mutable pathname.
|
|
243
|
+
func (r *runner) writeConfined(dest string, data []byte, perm fs.FileMode) error {
|
|
244
|
+
rel, err := filepath.Rel(r.target, dest)
|
|
245
|
+
if err != nil {
|
|
246
|
+
return err
|
|
247
|
+
}
|
|
248
|
+
root := r.targetRoot
|
|
249
|
+
if root == nil {
|
|
250
|
+
return fmt.Errorf("install target is not open")
|
|
251
|
+
}
|
|
234
252
|
if info, err := root.Stat(rel); err == nil {
|
|
235
253
|
perm = info.Mode().Perm()
|
|
236
254
|
} else if !os.IsNotExist(err) {
|
|
@@ -239,18 +257,16 @@ func (r *runner) writeConfined(dest string, data []byte, perm fs.FileMode) error
|
|
|
239
257
|
return fsutil.WriteFileAtomicIn(root, rel, data, perm)
|
|
240
258
|
}
|
|
241
259
|
|
|
242
|
-
// removeConfined
|
|
243
|
-
// through a root opened on the target.
|
|
260
|
+
// removeConfined uses the same retained root as writes and managed reads.
|
|
244
261
|
func (r *runner) removeConfined(dest string) error {
|
|
245
262
|
rel, err := filepath.Rel(r.target, dest)
|
|
246
263
|
if err != nil {
|
|
247
264
|
return err
|
|
248
265
|
}
|
|
249
|
-
root
|
|
250
|
-
if
|
|
251
|
-
return
|
|
266
|
+
root := r.targetRoot
|
|
267
|
+
if root == nil {
|
|
268
|
+
return fmt.Errorf("install target is not open")
|
|
252
269
|
}
|
|
253
|
-
defer func() { _ = root.Close() }()
|
|
254
270
|
return root.Remove(rel)
|
|
255
271
|
}
|
|
256
272
|
|
|
@@ -285,11 +301,10 @@ func (r *runner) seedDevrites() error {
|
|
|
285
301
|
fmt.Fprintln(r.opts.Stdout, " [seed] .devrites/ACTIVE (runtime state - preserved on uninstall)")
|
|
286
302
|
return nil
|
|
287
303
|
}
|
|
288
|
-
if err :=
|
|
304
|
+
if err := r.targetRoot.MkdirAll(".devrites", 0o755); err != nil {
|
|
289
305
|
return fmt.Errorf("create %s: %w", filepath.Dir(active), err)
|
|
290
306
|
}
|
|
291
|
-
|
|
292
|
-
f, err := os.OpenFile(active, os.O_CREATE|os.O_EXCL, 0o644)
|
|
307
|
+
f, err := r.targetRoot.OpenFile(filepath.Join(".devrites", "ACTIVE"), os.O_CREATE|os.O_EXCL, 0o644)
|
|
293
308
|
if err != nil {
|
|
294
309
|
if os.IsExist(err) {
|
|
295
310
|
return nil
|
|
@@ -342,7 +357,7 @@ func (r *runner) relinquishEditedHostConfigs() (map[string]bool, error) {
|
|
|
342
357
|
func (r *runner) seedHostConfig(config hostpack.HostConfig) error {
|
|
343
358
|
if _, managed := r.prev[config.Rel]; !managed {
|
|
344
359
|
// #nosec G304 -- fixed host config path under the operator target
|
|
345
|
-
data, err :=
|
|
360
|
+
data, err := r.readConfined(filepath.Join(r.target, filepath.FromSlash(config.Rel)))
|
|
346
361
|
if err == nil && string(data) != config.Content {
|
|
347
362
|
if !strings.Contains(string(data), config.Applied) {
|
|
348
363
|
fmt.Fprintf(r.opts.Stderr, "warning: %s exists and was not changed; to hide duplicate DevRites skills in %s, add: %s\n", config.Rel, config.Host, config.Advice)
|
|
@@ -408,7 +423,7 @@ func (r *runner) pruneDropped() error {
|
|
|
408
423
|
if err := r.removeConfined(dead); err != nil && !os.IsNotExist(err) {
|
|
409
424
|
return fmt.Errorf("remove %s: %w", rel, err)
|
|
410
425
|
}
|
|
411
|
-
pruneEmptyDirs(filepath.Dir(dead)
|
|
426
|
+
r.pruneEmptyDirs(filepath.Dir(dead))
|
|
412
427
|
fmt.Fprintf(r.opts.Stdout, " [%s] %s\n", action, rel)
|
|
413
428
|
}
|
|
414
429
|
r.stats.pruned++
|
|
@@ -63,11 +63,20 @@ func (r *runner) installBinary() error {
|
|
|
63
63
|
if err != nil {
|
|
64
64
|
return err
|
|
65
65
|
}
|
|
66
|
+
removeBackup := true
|
|
66
67
|
if backup != "" {
|
|
67
|
-
defer func() {
|
|
68
|
+
defer func() {
|
|
69
|
+
if removeBackup {
|
|
70
|
+
_ = os.Remove(backup)
|
|
71
|
+
}
|
|
72
|
+
}()
|
|
68
73
|
}
|
|
69
74
|
if err := fsutil.WriteFileAtomic(dest, data, 0o755); err != nil {
|
|
70
75
|
if restoreErr := restoreBinary(dest, backup, oldMode, hadOld); restoreErr != nil {
|
|
76
|
+
removeBackup = false
|
|
77
|
+
if backup != "" {
|
|
78
|
+
restoreErr = fmt.Errorf("%w (previous binary retained at %s)", restoreErr, backup)
|
|
79
|
+
}
|
|
71
80
|
return fmt.Errorf("install binary: %v; restore previous binary: %w", err, restoreErr)
|
|
72
81
|
}
|
|
73
82
|
return err
|
|
@@ -75,6 +84,10 @@ func (r *runner) installBinary() error {
|
|
|
75
84
|
_ = os.Chmod(dest, 0o755)
|
|
76
85
|
if _, err := verifyEngineBinary(dest, tag, 30*time.Second); err != nil {
|
|
77
86
|
if restoreErr := restoreBinary(dest, backup, oldMode, hadOld); restoreErr != nil {
|
|
87
|
+
removeBackup = false
|
|
88
|
+
if backup != "" {
|
|
89
|
+
restoreErr = fmt.Errorf("%w (previous binary retained at %s)", restoreErr, backup)
|
|
90
|
+
}
|
|
78
91
|
return fmt.Errorf("verify installed engine binary: %v; restore previous binary: %w", err, restoreErr)
|
|
79
92
|
}
|
|
80
93
|
if hadOld {
|
|
@@ -173,6 +186,44 @@ func (r *runner) acquireBinary(tag string) (string, func(), error) {
|
|
|
173
186
|
return path, func() {}, nil
|
|
174
187
|
}
|
|
175
188
|
|
|
189
|
+
func (r *runner) preflightBinaryRemoval() error {
|
|
190
|
+
if runtime.GOOS != "windows" || r.opts.DryRun || r.opts.KeepBinary || !r.opts.WithBinary || os.Getenv("DEVRITES_NO_BINARY") == "1" {
|
|
191
|
+
return nil
|
|
192
|
+
}
|
|
193
|
+
for _, path := range binaryCandidates() {
|
|
194
|
+
info, err := os.Lstat(path)
|
|
195
|
+
if os.IsNotExist(err) {
|
|
196
|
+
continue
|
|
197
|
+
}
|
|
198
|
+
if err != nil {
|
|
199
|
+
return fmt.Errorf("inspect engine binary %s: %w", path, err)
|
|
200
|
+
}
|
|
201
|
+
if info.Mode()&os.ModeSymlink != 0 {
|
|
202
|
+
info, err = os.Stat(path)
|
|
203
|
+
if os.IsNotExist(err) {
|
|
204
|
+
return nil
|
|
205
|
+
}
|
|
206
|
+
if err != nil {
|
|
207
|
+
return fmt.Errorf("inspect engine binary %s: %w", path, err)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
executable, err := os.Executable()
|
|
211
|
+
if err != nil {
|
|
212
|
+
return fmt.Errorf("locate running engine binary: %w", err)
|
|
213
|
+
}
|
|
214
|
+
running, err := os.Stat(executable)
|
|
215
|
+
if err != nil {
|
|
216
|
+
return fmt.Errorf("inspect running engine binary %s: %w", executable, err)
|
|
217
|
+
}
|
|
218
|
+
if os.SameFile(info, running) {
|
|
219
|
+
return fmt.Errorf("cannot remove running engine binary %s on Windows; use npx devrites@latest uninstall --target %q or --keep-binary", path, r.target)
|
|
220
|
+
}
|
|
221
|
+
// Removal stops at the first existing candidate, even if another is running.
|
|
222
|
+
return nil
|
|
223
|
+
}
|
|
224
|
+
return nil
|
|
225
|
+
}
|
|
226
|
+
|
|
176
227
|
func (r *runner) removeBinary() error {
|
|
177
228
|
if r.opts.KeepBinary {
|
|
178
229
|
fmt.Fprintln(r.opts.Stdout, " kept the global devrites-engine binary (--keep-binary).")
|
|
@@ -187,17 +238,24 @@ func (r *runner) removeBinary() error {
|
|
|
187
238
|
return nil
|
|
188
239
|
}
|
|
189
240
|
for _, p := range binaryCandidates() {
|
|
190
|
-
if p == "" || !exists(p) {
|
|
191
|
-
continue
|
|
192
|
-
}
|
|
193
241
|
if r.opts.DryRun {
|
|
242
|
+
if _, err := os.Lstat(p); err != nil {
|
|
243
|
+
if os.IsNotExist(err) {
|
|
244
|
+
continue
|
|
245
|
+
}
|
|
246
|
+
return fmt.Errorf("inspect engine binary %s: %w", p, err)
|
|
247
|
+
}
|
|
194
248
|
fmt.Fprintf(r.opts.Stdout, " [remove] %s (global engine binary)\n", p)
|
|
195
249
|
continue
|
|
196
250
|
}
|
|
197
|
-
if err := os.Remove(p); err
|
|
198
|
-
|
|
199
|
-
|
|
251
|
+
if err := os.Remove(p); err != nil {
|
|
252
|
+
if os.IsNotExist(err) {
|
|
253
|
+
continue
|
|
254
|
+
}
|
|
255
|
+
return fmt.Errorf("remove %s: %w", p, err)
|
|
200
256
|
}
|
|
257
|
+
fmt.Fprintf(r.opts.Stdout, " [remove] %s\n", p)
|
|
258
|
+
return nil
|
|
201
259
|
}
|
|
202
260
|
return nil
|
|
203
261
|
}
|
|
@@ -220,10 +278,10 @@ func engineBinaryName() string {
|
|
|
220
278
|
}
|
|
221
279
|
|
|
222
280
|
func binaryCandidates() []string {
|
|
223
|
-
candidates := []string{}
|
|
224
281
|
if dir := os.Getenv("DEVRITES_BIN_DIR"); dir != "" {
|
|
225
|
-
|
|
282
|
+
return []string{filepath.Join(dir, engineBinaryName())}
|
|
226
283
|
}
|
|
284
|
+
candidates := []string{}
|
|
227
285
|
home, _ := os.UserHomeDir()
|
|
228
286
|
if home != "" {
|
|
229
287
|
candidates = append(candidates, filepath.Join(home, ".local", "bin", engineBinaryName()))
|
|
@@ -61,6 +61,8 @@ type stats struct {
|
|
|
61
61
|
type runner struct {
|
|
62
62
|
opts Options
|
|
63
63
|
target string
|
|
64
|
+
targetInfo fs.FileInfo
|
|
65
|
+
targetRoot *os.Root
|
|
64
66
|
payload string
|
|
65
67
|
payloadFS fs.FS
|
|
66
68
|
source string
|
|
@@ -246,6 +248,10 @@ func Apply(opts Options) error {
|
|
|
246
248
|
return fmt.Errorf("prepare installer: %w", err)
|
|
247
249
|
}
|
|
248
250
|
if opts.Mode == ModeUninstall {
|
|
251
|
+
if err := r.openTargetRoot(); err != nil {
|
|
252
|
+
return err
|
|
253
|
+
}
|
|
254
|
+
defer r.closeTargetRoot()
|
|
249
255
|
return r.uninstall()
|
|
250
256
|
}
|
|
251
257
|
if err := r.install(); err != nil {
|
|
@@ -297,27 +303,75 @@ func newRunner(opts Options) (*runner, error) {
|
|
|
297
303
|
}
|
|
298
304
|
}
|
|
299
305
|
r := &runner{
|
|
300
|
-
opts:
|
|
301
|
-
target:
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
306
|
+
opts: opts,
|
|
307
|
+
target: target,
|
|
308
|
+
targetInfo: info,
|
|
309
|
+
payload: payload,
|
|
310
|
+
payloadFS: os.DirFS(payload),
|
|
311
|
+
source: source,
|
|
312
|
+
prev: map[string]managedRecord{},
|
|
313
|
+
records: map[string]string{},
|
|
314
|
+
preflight: map[string]pathSnapshot{},
|
|
308
315
|
}
|
|
309
316
|
if opts.Mode != ModeUninstall {
|
|
310
317
|
if err := r.validatePayload(); err != nil {
|
|
311
318
|
return nil, fmt.Errorf("validate payload: %w", err)
|
|
312
319
|
}
|
|
313
320
|
}
|
|
314
|
-
|
|
321
|
+
if err := r.openTargetRoot(); err != nil {
|
|
322
|
+
return nil, err
|
|
323
|
+
}
|
|
324
|
+
defer r.closeTargetRoot()
|
|
325
|
+
data, err := r.readConfined(filepath.Join(target, ManifestName))
|
|
326
|
+
if err != nil && !os.IsNotExist(err) {
|
|
327
|
+
return nil, fmt.Errorf("read manifest %s: %w", ManifestName, err)
|
|
328
|
+
}
|
|
329
|
+
r.prev, _, err = parseManifestData(data)
|
|
315
330
|
if err != nil {
|
|
316
331
|
return nil, err
|
|
317
332
|
}
|
|
318
333
|
return r, nil
|
|
319
334
|
}
|
|
320
335
|
|
|
336
|
+
func (r *runner) openTargetRoot() error {
|
|
337
|
+
root, err := os.OpenRoot(r.target)
|
|
338
|
+
if err != nil {
|
|
339
|
+
return err
|
|
340
|
+
}
|
|
341
|
+
info, err := root.Stat(".")
|
|
342
|
+
if err != nil || r.targetInfo == nil || !os.SameFile(r.targetInfo, info) {
|
|
343
|
+
_ = root.Close()
|
|
344
|
+
return fmt.Errorf("install target changed before operation: %s", r.target)
|
|
345
|
+
}
|
|
346
|
+
r.targetRoot = root
|
|
347
|
+
return nil
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
func (r *runner) closeTargetRoot() {
|
|
351
|
+
if r.targetRoot != nil {
|
|
352
|
+
_ = r.targetRoot.Close()
|
|
353
|
+
r.targetRoot = nil
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
func (r *runner) checkTargetIdentity() error {
|
|
358
|
+
info, err := os.Stat(r.target)
|
|
359
|
+
if err != nil || r.targetInfo == nil || !os.SameFile(r.targetInfo, info) {
|
|
360
|
+
return fmt.Errorf("install target changed during operation: %s", r.target)
|
|
361
|
+
}
|
|
362
|
+
return nil
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
func (r *runner) pruneEmptyDirs(start string) {
|
|
366
|
+
for start != r.target && start != filepath.Dir(start) {
|
|
367
|
+
rel, err := filepath.Rel(r.target, start)
|
|
368
|
+
if err != nil || r.targetRoot == nil || r.targetRoot.Remove(rel) != nil {
|
|
369
|
+
return
|
|
370
|
+
}
|
|
371
|
+
start = filepath.Dir(start)
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
321
375
|
func inferSourceDir() string {
|
|
322
376
|
if v := os.Getenv("DEVRITES_SOURCE_DIR"); v != "" {
|
|
323
377
|
return v
|
|
@@ -390,18 +444,6 @@ func isGlobalTarget(target string) bool {
|
|
|
390
444
|
return false
|
|
391
445
|
}
|
|
392
446
|
|
|
393
|
-
func pruneEmptyDirs(start, stop string) {
|
|
394
|
-
for {
|
|
395
|
-
if start == stop || start == filepath.Dir(start) || start == string(os.PathSeparator) {
|
|
396
|
-
return
|
|
397
|
-
}
|
|
398
|
-
if err := os.Remove(start); err != nil {
|
|
399
|
-
return
|
|
400
|
-
}
|
|
401
|
-
start = filepath.Dir(start)
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
|
|
405
447
|
func exists(path string) bool {
|
|
406
448
|
// #nosec G703 -- existence probe on managed install paths
|
|
407
449
|
_, err := os.Lstat(path)
|
|
@@ -19,11 +19,6 @@ func readManifest(path string) (map[string]managedRecord, error) {
|
|
|
19
19
|
return records, err
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
func readManifestList(path string) ([]string, error) {
|
|
23
|
-
_, entries, err := parseManifest(path)
|
|
24
|
-
return entries, err
|
|
25
|
-
}
|
|
26
|
-
|
|
27
22
|
func parseManifest(path string) (map[string]managedRecord, []string, error) {
|
|
28
23
|
// #nosec G304 -- install manifest under the operator-selected target
|
|
29
24
|
data, err := os.ReadFile(path)
|
|
@@ -33,6 +28,10 @@ func parseManifest(path string) (map[string]managedRecord, []string, error) {
|
|
|
33
28
|
}
|
|
34
29
|
return nil, nil, fmt.Errorf("read manifest %s: %w", path, err)
|
|
35
30
|
}
|
|
31
|
+
return parseManifestData(data)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
func parseManifestData(data []byte) (map[string]managedRecord, []string, error) {
|
|
36
35
|
hashes := map[string]string{}
|
|
37
36
|
var out []string
|
|
38
37
|
for _, line := range strings.Split(string(data), "\n") {
|
|
@@ -50,7 +50,7 @@ const (
|
|
|
50
50
|
)
|
|
51
51
|
|
|
52
52
|
func (r *runner) claudeSettingsMarkerLines() []string {
|
|
53
|
-
data, err :=
|
|
53
|
+
data, err := r.readConfined(filepath.Join(r.target, filepath.FromSlash(hostpack.ClaudeSettingsMerge.MarkerRel)))
|
|
54
54
|
if err != nil {
|
|
55
55
|
return nil
|
|
56
56
|
}
|
|
@@ -134,18 +134,24 @@ func (r *runner) mergeMarkerFile(merge hostpack.MarkerMerge) error {
|
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
136
|
fmt.Fprintf(r.opts.Stdout, " [merge] %s (%s)\n", merge.TargetRel, verb)
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
137
|
+
return r.installMarker(merge.MarkerRel, merge.MarkerText)
|
|
138
|
+
}
|
|
139
|
+
if err := r.installMarker(merge.MarkerRel, merge.MarkerText); err != nil {
|
|
140
|
+
return err
|
|
141
|
+
}
|
|
142
|
+
if r.records[merge.MarkerRel] == "" {
|
|
143
|
+
return fmt.Errorf("cannot merge %s without owning %s; use --force to replace the marker", merge.TargetRel, merge.MarkerRel)
|
|
144
|
+
}
|
|
145
|
+
if err := r.recheckPath(merge.TargetRel); err != nil {
|
|
146
|
+
return err
|
|
147
|
+
}
|
|
148
|
+
dest := filepath.Join(r.target, filepath.FromSlash(merge.TargetRel))
|
|
149
|
+
if err := r.writeConfined(dest, next, 0o644); err != nil {
|
|
150
|
+
return fmt.Errorf("cannot write %s: %w", merge.TargetRel, err)
|
|
147
151
|
}
|
|
148
|
-
|
|
152
|
+
// Multiple marker blocks can share one target (AGENTS.md carries both
|
|
153
|
+
// the Codex and pi blocks).
|
|
154
|
+
return r.refreshPreflight(merge.TargetRel)
|
|
149
155
|
}
|
|
150
156
|
|
|
151
157
|
// markerMergeNext returns the merged target bytes and the current bytes
|
|
@@ -156,8 +162,7 @@ func (r *runner) markerMergeNext(merge hostpack.MarkerMerge) ([]byte, []byte, er
|
|
|
156
162
|
return nil, nil, fmt.Errorf("read payload %s: %w", merge.PayloadRel, err)
|
|
157
163
|
}
|
|
158
164
|
dest := filepath.Join(r.target, filepath.FromSlash(merge.TargetRel))
|
|
159
|
-
|
|
160
|
-
current, readErr := os.ReadFile(dest)
|
|
165
|
+
current, readErr := r.readConfined(dest)
|
|
161
166
|
if errors.Is(readErr, fs.ErrNotExist) {
|
|
162
167
|
return block, nil, nil
|
|
163
168
|
}
|
|
@@ -208,11 +213,20 @@ func (r *runner) mergeCodexConfig() error {
|
|
|
208
213
|
if err != nil {
|
|
209
214
|
return err
|
|
210
215
|
}
|
|
216
|
+
if err := r.installMarker(merge.MarkerRel, merge.MarkerText); err != nil {
|
|
217
|
+
return err
|
|
218
|
+
}
|
|
219
|
+
if r.records[merge.MarkerRel] == "" {
|
|
220
|
+
return fmt.Errorf("cannot merge %s without owning %s; use --force to replace the marker", merge.TargetRel, merge.MarkerRel)
|
|
221
|
+
}
|
|
222
|
+
if err := r.recheckPath(merge.TargetRel); err != nil {
|
|
223
|
+
return err
|
|
224
|
+
}
|
|
211
225
|
dest := filepath.Join(r.target, filepath.FromSlash(merge.TargetRel))
|
|
212
226
|
if err := r.writeConfined(dest, next, 0o644); err != nil {
|
|
213
227
|
return fmt.Errorf("cannot write %s: %w", merge.TargetRel, err)
|
|
214
228
|
}
|
|
215
|
-
return
|
|
229
|
+
return nil
|
|
216
230
|
}
|
|
217
231
|
|
|
218
232
|
// codexConfigNext returns the merged .codex/config.toml without writing it.
|
|
@@ -227,8 +241,7 @@ func (r *runner) codexConfigNext() ([]byte, error) {
|
|
|
227
241
|
}
|
|
228
242
|
dest := filepath.Join(r.target, filepath.FromSlash(merge.TargetRel))
|
|
229
243
|
var current []byte
|
|
230
|
-
|
|
231
|
-
if data, readErr := os.ReadFile(dest); readErr == nil {
|
|
244
|
+
if data, readErr := r.readConfined(dest); readErr == nil {
|
|
232
245
|
// Drop a UTF-8 BOM: the block may go at offset 0, and a BOM after
|
|
233
246
|
// it is invalid TOML.
|
|
234
247
|
data = bytes.TrimPrefix(data, utf8BOM)
|
|
@@ -423,7 +436,7 @@ func (r *runner) claudeSettingsNext() (map[string]any, claudeSettingsMerge, erro
|
|
|
423
436
|
}
|
|
424
437
|
current := map[string]any{}
|
|
425
438
|
state := claudeSettingsMerge{created: r.claudeSettingsCreated()}
|
|
426
|
-
if data, readErr := readJSON(dest); readErr == nil {
|
|
439
|
+
if data, readErr := r.readJSON(dest); readErr == nil {
|
|
427
440
|
current = data
|
|
428
441
|
} else if errors.Is(readErr, fs.ErrNotExist) {
|
|
429
442
|
state.created = true
|
|
@@ -443,13 +456,13 @@ func (r *runner) seedClaudeSettings() error {
|
|
|
443
456
|
}
|
|
444
457
|
|
|
445
458
|
func (r *runner) stripHooksConfined(path string) error {
|
|
446
|
-
return stripHooksWith(path, func(data []byte) error {
|
|
459
|
+
return r.stripHooksWith(path, func(data []byte) error {
|
|
447
460
|
return r.writeConfined(path, data, 0o644)
|
|
448
461
|
}, func() error { return r.removeConfined(path) })
|
|
449
462
|
}
|
|
450
463
|
|
|
451
|
-
func stripHooksWith(path string, write func([]byte) error, remove func() error) error {
|
|
452
|
-
current, err := readJSON(path)
|
|
464
|
+
func (r *runner) stripHooksWith(path string, write func([]byte) error, remove func() error) error {
|
|
465
|
+
current, err := r.readJSON(path)
|
|
453
466
|
if err != nil {
|
|
454
467
|
if errors.Is(err, fs.ErrNotExist) {
|
|
455
468
|
return nil
|
|
@@ -475,7 +488,7 @@ func (r *runner) stripClaudeSettings(path string) error {
|
|
|
475
488
|
}
|
|
476
489
|
|
|
477
490
|
func (r *runner) stripClaudeSettingsWith(path string, write func([]byte) error, remove func() error) error {
|
|
478
|
-
current, err := readJSON(path)
|
|
491
|
+
current, err := r.readJSON(path)
|
|
479
492
|
if err != nil {
|
|
480
493
|
if errors.Is(err, fs.ErrNotExist) {
|
|
481
494
|
return nil
|
|
@@ -495,14 +508,13 @@ func (r *runner) stripClaudeSettingsWith(path string, write func([]byte) error,
|
|
|
495
508
|
}
|
|
496
509
|
|
|
497
510
|
func (r *runner) stripMarkerConfined(path, begin, end string) error {
|
|
498
|
-
return stripMarkerWith(path, begin, end, func(data []byte) error {
|
|
511
|
+
return r.stripMarkerWith(path, begin, end, func(data []byte) error {
|
|
499
512
|
return r.writeConfined(path, data, 0o644)
|
|
500
513
|
}, func() error { return r.removeConfined(path) })
|
|
501
514
|
}
|
|
502
515
|
|
|
503
|
-
func stripMarkerWith(path, begin, end string, write func([]byte) error, remove func() error) error {
|
|
504
|
-
|
|
505
|
-
data, err := os.ReadFile(path)
|
|
516
|
+
func (r *runner) stripMarkerWith(path, begin, end string, write func([]byte) error, remove func() error) error {
|
|
517
|
+
data, err := r.readConfined(path)
|
|
506
518
|
if err != nil {
|
|
507
519
|
if os.IsNotExist(err) {
|
|
508
520
|
return nil
|
|
@@ -536,9 +548,8 @@ func hasTopLevelTOMLKey(data []byte, key string) bool {
|
|
|
536
548
|
return found
|
|
537
549
|
}
|
|
538
550
|
|
|
539
|
-
func readJSON(path string) (map[string]any, error) {
|
|
540
|
-
|
|
541
|
-
data, err := os.ReadFile(path)
|
|
551
|
+
func (r *runner) readJSON(path string) (map[string]any, error) {
|
|
552
|
+
data, err := r.readConfined(path)
|
|
542
553
|
if err != nil {
|
|
543
554
|
return nil, fmt.Errorf("read %s: %w", path, err)
|
|
544
555
|
}
|
|
@@ -567,10 +578,55 @@ func isDevritesHookCommand(v any) bool {
|
|
|
567
578
|
return false
|
|
568
579
|
}
|
|
569
580
|
command, _ := hook["command"].(string)
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
581
|
+
// Match shipped command templates, not quoted or logged mentions in user commands.
|
|
582
|
+
switch command {
|
|
583
|
+
case `printf '%s\n' 'DevRites: this subagent is the single write-capable slice-wright. Stay within the current slice contract and touched-files.md; write no .devrites bookkeeping.'`,
|
|
584
|
+
`printf '%s\n' 'DevRites: this subagent is read-only. Inspect with read/search/test commands and return findings only; do not mutate the tree.'`,
|
|
585
|
+
`printf '%s\n' 'DevRites: reconcile this subagent result against the active DevRites skill contract before claiming completion.'`,
|
|
586
|
+
`root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"; cd "$root" && DEVRITES_REVIEWER_AGENT_REQUIRED=1 bash "$root/.codex/hooks/devrites-reviewer-readonly.sh"`,
|
|
587
|
+
`root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"; cd "$root" && DEVRITES_WRIGHT_AGENT_REQUIRED=1 bash "$root/.codex/hooks/devrites-wright-scope.sh"`:
|
|
588
|
+
return true
|
|
589
|
+
}
|
|
590
|
+
for _, prefix := range [...]string{
|
|
591
|
+
`bash "$CLAUDE_PROJECT_DIR/.claude/hooks/devrites-`,
|
|
592
|
+
`bash "${CLAUDE_PLUGIN_ROOT}/pack/.claude/hooks/devrites-`,
|
|
593
|
+
`root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"; cd "$root" && bash "$root/.codex/hooks/devrites-`,
|
|
594
|
+
} {
|
|
595
|
+
if script, ok := strings.CutPrefix(command, prefix); ok {
|
|
596
|
+
switch script {
|
|
597
|
+
case `a1-guard.sh"`, `allow.sh"`, `cursor.sh"`, `orient.sh"`,
|
|
598
|
+
`redwatch.sh"`, `refresh-indexes.sh"`, `stop-gate.sh"`:
|
|
599
|
+
return true
|
|
600
|
+
}
|
|
601
|
+
return false
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
if rest, ok := strings.CutPrefix(command, "command -v devrites-engine >/dev/null 2>&1 && exec "); ok {
|
|
605
|
+
var complete bool
|
|
606
|
+
command, complete = strings.CutSuffix(rest, " || exit 0")
|
|
607
|
+
if !complete {
|
|
608
|
+
return false
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
command = strings.TrimPrefix(command, `command -v devrites-engine >/dev/null 2>&1 || exit 0; cd "$(git rev-parse --show-toplevel 2>/dev/null || pwd)" 2>/dev/null || exit 0; exec `)
|
|
612
|
+
switch command {
|
|
613
|
+
case "env DEVRITES_REVIEWER_AGENT_REQUIRED=1 devrites-engine hook reviewer-readonly --harness=codex",
|
|
614
|
+
"env DEVRITES_WRIGHT_AGENT_REQUIRED=1 devrites-engine hook wright-scope --harness=codex":
|
|
615
|
+
return true
|
|
616
|
+
}
|
|
617
|
+
args, ok := strings.CutPrefix(command, "devrites-engine hook ")
|
|
618
|
+
if !ok {
|
|
619
|
+
return false
|
|
620
|
+
}
|
|
621
|
+
args = strings.TrimSuffix(strings.TrimSuffix(args, " --harness=claude"), " --harness=codex")
|
|
622
|
+
switch args {
|
|
623
|
+
case "a1-guard", "allow", "auq", "cursor", "event session-end", "event subagent-stop",
|
|
624
|
+
"git-guard", "handoff-snapshot", "old-gate", "orient", "redwatch", "refresh-indexes",
|
|
625
|
+
"reviewer-readonly", "source-cache-post", "source-cache-pre", "statusline",
|
|
626
|
+
"stop-gate", "subagent-orient", "wright-scope":
|
|
627
|
+
return true
|
|
628
|
+
}
|
|
629
|
+
return false
|
|
574
630
|
}
|
|
575
631
|
|
|
576
632
|
func isDevritesHooksComment(comment string) bool {
|