devrites 7.3.1 → 7.3.3
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 +38 -0
- package/README.md +19 -3
- package/bin/devrites.mjs +31 -5
- package/docs/cli.md +15 -7
- package/docs/engine/commands.md +37 -5
- package/docs/engine/workspace-schema.md +4 -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/hostpack/hostpack.go +1 -1
- 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/apply_bundle.go +25 -8
- 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/regression.go +0 -5
- package/engine/internal/lib/resolve.go +148 -145
- 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/rootfacts/facts.go +11 -0
- package/engine/internal/state/acceptance.go +35 -5
- package/engine/internal/state/feature.go +3 -2
- package/engine/internal/state/lock_other.go +4 -0
- package/engine/internal/state/observation.go +1 -1
- package/engine/internal/state/write.go +56 -39
- package/engine/internal/workflow/commands.go +25 -3
- package/engine/main.go +21 -2
- 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/.claude/skills/rite-doctor/SKILL.md +70 -68
- package/pack/generated/README.md +1 -1
- 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/claude/skills/rite-doctor/SKILL.md +70 -68
- 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/codex/skills/rite-doctor/SKILL.md +70 -68
- 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/devin/skills/rite-doctor/SKILL.md +70 -68
- 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/dsh/skills/rite-doctor/SKILL.md +70 -68
- package/pack/generated/omp/skills/devrites-lib/reference/parallel-dispatch.md +41 -39
- package/pack/generated/omp/skills/devrites-lib/reference/standards/agents.md +1 -1
- 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 +18 -18
- package/pack/generated/omp/skills/rite-clarify/SKILL.md +25 -25
- package/pack/generated/omp/skills/rite-doctor/SKILL.md +70 -68
- package/pack/generated/pi/AGENTS.md +1 -1
- 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/pack/generated/pi/skills/rite-doctor/SKILL.md +70 -68
- package/package.json +1 -1
- package/scripts/build-host-artifacts.sh +1 -1
- package/scripts/dsh-generate.py +6 -2
- package/scripts/install-lib.sh +29 -0
- package/scripts/omp-generate.sh +4 -4
- package/scripts/pi-generate.sh +3 -3
- package/uninstall.sh +1 -0
|
@@ -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 {
|
|
@@ -14,7 +14,6 @@ import (
|
|
|
14
14
|
"strings"
|
|
15
15
|
|
|
16
16
|
"github.com/devrites/devrites/internal/hostpack"
|
|
17
|
-
"github.com/devrites/devrites/internal/safepath"
|
|
18
17
|
)
|
|
19
18
|
|
|
20
19
|
func (r *runner) preflightInstall() error {
|
|
@@ -46,10 +45,15 @@ func (r *runner) preflightInstall() error {
|
|
|
46
45
|
return err
|
|
47
46
|
}
|
|
48
47
|
if !snapshot.missing && r.isPinnedAlias(rel) {
|
|
49
|
-
//
|
|
50
|
-
|
|
48
|
+
// Carry pins without adopting edits; only legacy pins need a baseline.
|
|
49
|
+
if record.Hash == "" {
|
|
50
|
+
record.Hash = snapshot.hash
|
|
51
|
+
}
|
|
52
|
+
if kind := managedConflict(record, snapshot); kind != "" && !r.opts.Force {
|
|
53
|
+
conflicts = append(conflicts, fmt.Sprintf("%s: %s", kind, rel))
|
|
54
|
+
}
|
|
51
55
|
r.addManifest(rel)
|
|
52
|
-
r.addInstallRecord(rel,
|
|
56
|
+
r.addInstallRecord(rel, record.Hash)
|
|
53
57
|
continue
|
|
54
58
|
}
|
|
55
59
|
if !snapshot.missing {
|
|
@@ -101,23 +105,33 @@ func (r *runner) preflightMerges() error {
|
|
|
101
105
|
if _, _, err := r.markerMergeNext(m.merge); err != nil {
|
|
102
106
|
return fmt.Errorf("merge %s: %w", m.merge.TargetRel, err)
|
|
103
107
|
}
|
|
108
|
+
if err := r.preflightMarkerOwnership(m.merge.TargetRel, m.merge.MarkerRel, m.merge.MarkerText); err != nil {
|
|
109
|
+
return err
|
|
110
|
+
}
|
|
104
111
|
}
|
|
105
112
|
if r.opts.WithCodex {
|
|
106
113
|
if _, err := r.codexConfigNext(); err != nil {
|
|
107
114
|
return fmt.Errorf("merge %s: %w", hostpack.CodexConfigMerge.TargetRel, err)
|
|
108
115
|
}
|
|
116
|
+
merge := hostpack.CodexConfigMerge
|
|
117
|
+
if err := r.preflightMarkerOwnership(merge.TargetRel, merge.MarkerRel, merge.MarkerText); err != nil {
|
|
118
|
+
return err
|
|
119
|
+
}
|
|
109
120
|
}
|
|
110
121
|
_, settings, err := r.claudeSettingsNext()
|
|
111
122
|
if err != nil {
|
|
112
123
|
return fmt.Errorf("seed claude settings: %w", err)
|
|
113
124
|
}
|
|
114
|
-
|
|
125
|
+
merge := hostpack.ClaudeSettingsMerge
|
|
126
|
+
return r.preflightMarkerOwnership(merge.TargetRel, merge.MarkerRel, claudeSettingsMarkerText(r.pendingClaudeSettingsOwnership(settings)))
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
func (r *runner) preflightMarkerOwnership(target, marker, text string) error {
|
|
115
130
|
if snapshot := r.preflight[marker]; !snapshot.missing && !r.opts.Force {
|
|
116
131
|
if _, managed := r.prev[marker]; !managed {
|
|
117
|
-
|
|
118
|
-
sum := sha256.Sum256([]byte(text))
|
|
132
|
+
sum := sha256.Sum256([]byte(text + "\n"))
|
|
119
133
|
if snapshot.hash != fmt.Sprintf("sha256:%x", sum[:]) {
|
|
120
|
-
return fmt.Errorf("cannot merge %s without owning %s; use --force to replace the marker",
|
|
134
|
+
return fmt.Errorf("cannot merge %s without owning %s; use --force to replace the marker", target, marker)
|
|
121
135
|
}
|
|
122
136
|
}
|
|
123
137
|
}
|
|
@@ -249,10 +263,17 @@ func (r *runner) installMergeTargets() []string {
|
|
|
249
263
|
|
|
250
264
|
func (r *runner) rememberPath(rel string) (pathSnapshot, error) {
|
|
251
265
|
rel = filepath.ToSlash(rel)
|
|
266
|
+
if r.targetInfo == nil {
|
|
267
|
+
info, err := os.Stat(r.target)
|
|
268
|
+
if err != nil {
|
|
269
|
+
return pathSnapshot{}, err
|
|
270
|
+
}
|
|
271
|
+
r.targetInfo = info
|
|
272
|
+
}
|
|
252
273
|
if snapshot, ok := r.preflight[rel]; ok {
|
|
253
274
|
return snapshot, nil
|
|
254
275
|
}
|
|
255
|
-
snapshot, err := inspectManagedPath(
|
|
276
|
+
snapshot, err := r.inspectManagedPath(rel)
|
|
256
277
|
if err != nil {
|
|
257
278
|
return pathSnapshot{}, err
|
|
258
279
|
}
|
|
@@ -264,7 +285,7 @@ func (r *runner) rememberPath(rel string) (pathSnapshot, error) {
|
|
|
264
285
|
// verified write (marker merge, strip). Needed when several managed merges
|
|
265
286
|
// share one target such as AGENTS.md.
|
|
266
287
|
func (r *runner) refreshPreflight(rel string) error {
|
|
267
|
-
snap, err := inspectManagedPath(
|
|
288
|
+
snap, err := r.inspectManagedPath(rel)
|
|
268
289
|
if err != nil {
|
|
269
290
|
return err
|
|
270
291
|
}
|
|
@@ -273,12 +294,15 @@ func (r *runner) refreshPreflight(rel string) error {
|
|
|
273
294
|
}
|
|
274
295
|
|
|
275
296
|
func (r *runner) recheckPath(rel string) error {
|
|
297
|
+
if err := r.checkTargetIdentity(); err != nil {
|
|
298
|
+
return err
|
|
299
|
+
}
|
|
276
300
|
rel = filepath.ToSlash(rel)
|
|
277
301
|
before, ok := r.preflight[rel]
|
|
278
302
|
if !ok {
|
|
279
303
|
return fmt.Errorf("path %s was not preflighted", rel)
|
|
280
304
|
}
|
|
281
|
-
now, err := inspectManagedPath(
|
|
305
|
+
now, err := r.inspectManagedPath(rel)
|
|
282
306
|
if err != nil {
|
|
283
307
|
return err
|
|
284
308
|
}
|
|
@@ -287,6 +311,9 @@ func (r *runner) recheckPath(rel string) error {
|
|
|
287
311
|
}
|
|
288
312
|
if afterRecheckPath != nil {
|
|
289
313
|
afterRecheckPath(rel)
|
|
314
|
+
if err := r.checkTargetIdentity(); err != nil {
|
|
315
|
+
return err
|
|
316
|
+
}
|
|
290
317
|
}
|
|
291
318
|
return nil
|
|
292
319
|
}
|
|
@@ -295,20 +322,35 @@ func (r *runner) recheckPath(rel string) error {
|
|
|
295
322
|
// recheck and the write that follows it.
|
|
296
323
|
var afterRecheckPath func(rel string)
|
|
297
324
|
|
|
325
|
+
func (r *runner) inspectManagedPath(rel string) (pathSnapshot, error) {
|
|
326
|
+
if r.targetRoot != nil {
|
|
327
|
+
return inspectManagedPathIn(r.targetRoot, rel)
|
|
328
|
+
}
|
|
329
|
+
return inspectManagedPath(r.target, rel)
|
|
330
|
+
}
|
|
331
|
+
|
|
298
332
|
func inspectManagedPath(target, rel string) (pathSnapshot, error) {
|
|
333
|
+
root, err := os.OpenRoot(target)
|
|
334
|
+
if err != nil {
|
|
335
|
+
return pathSnapshot{}, err
|
|
336
|
+
}
|
|
337
|
+
defer func() { _ = root.Close() }()
|
|
338
|
+
return inspectManagedPathIn(root, rel)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
func inspectManagedPathIn(root *os.Root, rel string) (pathSnapshot, error) {
|
|
299
342
|
native := filepath.FromSlash(rel)
|
|
300
343
|
if !filepath.IsLocal(native) {
|
|
301
344
|
return pathSnapshot{}, fmt.Errorf("refusing unsafe managed path %q", rel)
|
|
302
345
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
return pathSnapshot{}, fmt.Errorf("refusing managed path outside target: %s", rel)
|
|
346
|
+
if root == nil {
|
|
347
|
+
return pathSnapshot{}, fmt.Errorf("install target is not open")
|
|
306
348
|
}
|
|
307
|
-
walk :=
|
|
349
|
+
walk := ""
|
|
308
350
|
parts := strings.Split(native, string(filepath.Separator))
|
|
309
351
|
for _, part := range parts {
|
|
310
352
|
walk = filepath.Join(walk, part)
|
|
311
|
-
info, err :=
|
|
353
|
+
info, err := root.Lstat(walk)
|
|
312
354
|
if os.IsNotExist(err) {
|
|
313
355
|
return pathSnapshot{missing: true}, nil
|
|
314
356
|
}
|
|
@@ -319,7 +361,7 @@ func inspectManagedPath(target, rel string) (pathSnapshot, error) {
|
|
|
319
361
|
return pathSnapshot{}, fmt.Errorf("refusing symlink or junction in managed path: %s", rel)
|
|
320
362
|
}
|
|
321
363
|
}
|
|
322
|
-
info, err :=
|
|
364
|
+
info, err := root.Lstat(native)
|
|
323
365
|
if os.IsNotExist(err) {
|
|
324
366
|
return pathSnapshot{missing: true}, nil
|
|
325
367
|
}
|
|
@@ -329,8 +371,7 @@ func inspectManagedPath(target, rel string) (pathSnapshot, error) {
|
|
|
329
371
|
if !info.Mode().IsRegular() {
|
|
330
372
|
return pathSnapshot{}, fmt.Errorf("refusing non-regular managed path: %s", rel)
|
|
331
373
|
}
|
|
332
|
-
|
|
333
|
-
f, err := os.Open(dest)
|
|
374
|
+
f, err := root.Open(native)
|
|
334
375
|
if err != nil {
|
|
335
376
|
return pathSnapshot{}, fmt.Errorf("read managed path %s: %w", rel, err)
|
|
336
377
|
}
|
|
@@ -12,13 +12,20 @@ import (
|
|
|
12
12
|
|
|
13
13
|
func (r *runner) uninstall() error {
|
|
14
14
|
mf := filepath.Join(r.target, ManifestName)
|
|
15
|
-
|
|
15
|
+
data, err := r.readConfined(mf)
|
|
16
|
+
if err != nil && !os.IsNotExist(err) {
|
|
17
|
+
return fmt.Errorf("read manifest %s: %w", mf, err)
|
|
18
|
+
}
|
|
19
|
+
_, entries, err := parseManifestData(data)
|
|
16
20
|
if err != nil {
|
|
17
21
|
return err
|
|
18
22
|
}
|
|
19
23
|
if len(entries) == 0 {
|
|
20
24
|
return fmt.Errorf("no DevRites manifest at %s - nothing to uninstall", mf)
|
|
21
25
|
}
|
|
26
|
+
if err := r.preflightBinaryRemoval(); err != nil {
|
|
27
|
+
return err
|
|
28
|
+
}
|
|
22
29
|
fmt.Fprintln(r.opts.Stdout, "DevRites uninstaller")
|
|
23
30
|
fmt.Fprintf(r.opts.Stdout, " target : %s\n", r.target)
|
|
24
31
|
fmt.Fprintf(r.opts.Stdout, " manifest: %s\n", mf)
|
|
@@ -99,6 +106,15 @@ func (r *runner) uninstall() error {
|
|
|
99
106
|
r.stats.missing++
|
|
100
107
|
}
|
|
101
108
|
}
|
|
109
|
+
if !r.opts.DryRun {
|
|
110
|
+
for _, d := range dirs {
|
|
111
|
+
r.pruneEmptyDirs(d)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// Keep ownership available for a retry if the external binary cannot be removed.
|
|
115
|
+
if err := r.removeBinary(); err != nil {
|
|
116
|
+
return fmt.Errorf("remove engine binary: %w", err)
|
|
117
|
+
}
|
|
102
118
|
if r.opts.DryRun {
|
|
103
119
|
fmt.Fprintf(r.opts.Stdout, " [remove] %s\n", ManifestName)
|
|
104
120
|
} else {
|
|
@@ -106,13 +122,7 @@ func (r *runner) uninstall() error {
|
|
|
106
122
|
return err
|
|
107
123
|
}
|
|
108
124
|
_ = r.removeConfined(mf)
|
|
109
|
-
|
|
110
|
-
for _, d := range dirs {
|
|
111
|
-
pruneEmptyDirs(d, r.target)
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
if err := r.removeBinary(); err != nil {
|
|
115
|
-
return fmt.Errorf("remove engine binary: %w", err)
|
|
125
|
+
r.pruneEmptyDirs(filepath.Dir(mf))
|
|
116
126
|
}
|
|
117
127
|
fmt.Fprintln(r.opts.Stdout)
|
|
118
128
|
if r.opts.DryRun {
|
|
@@ -141,7 +151,7 @@ func (r *runner) dryRunStripMarker(tree map[string][]byte, merge hostpack.Manage
|
|
|
141
151
|
write := func(data []byte) error { tree[path] = data; return nil }
|
|
142
152
|
remove := func() error { tree[path] = nil; return nil }
|
|
143
153
|
if merge.MarkerRel == hostpack.LegacyCodexHooksMerge.MarkerRel {
|
|
144
|
-
return stripHooksWith(path, write, remove)
|
|
154
|
+
return r.stripHooksWith(path, write, remove)
|
|
145
155
|
}
|
|
146
156
|
if merge.TargetRel == hostpack.ClaudeSettingsMerge.TargetRel {
|
|
147
157
|
return r.stripClaudeSettingsWith(path, write, remove)
|
|
@@ -9,7 +9,6 @@ import (
|
|
|
9
9
|
"strings"
|
|
10
10
|
"time"
|
|
11
11
|
|
|
12
|
-
"github.com/devrites/devrites/internal/hostpack"
|
|
13
12
|
"github.com/devrites/devrites/internal/release"
|
|
14
13
|
)
|
|
15
14
|
|
|
@@ -65,9 +64,6 @@ func runUpdate(opts Options) error {
|
|
|
65
64
|
if err != nil {
|
|
66
65
|
return fmt.Errorf("resolve payload %s: %w", opts.PayloadDir, err)
|
|
67
66
|
}
|
|
68
|
-
if err := hostpack.ValidatePayload(os.DirFS(payload), true, true, true, true, true); err != nil {
|
|
69
|
-
return fmt.Errorf("local update payload under %s is invalid: %w", payload, err)
|
|
70
|
-
}
|
|
71
67
|
installFlags := manifestHeader(mf, "devrites-flags")
|
|
72
68
|
next := DefaultOptions(ModeInstall)
|
|
73
69
|
next.Stdout = opts.Stdout
|
|
@@ -437,20 +437,37 @@ func checkBundleResult(targets []*bundleTarget, retired map[string]bool) []strin
|
|
|
437
437
|
return problems
|
|
438
438
|
}
|
|
439
439
|
|
|
440
|
-
// writeBundleTargets writes every target atomically
|
|
441
|
-
//
|
|
442
|
-
//
|
|
440
|
+
// writeBundleTargets writes every target atomically. A failed write may already
|
|
441
|
+
// have published its bytes, so rollback covers every attempted target and reports
|
|
442
|
+
// any restoration failures.
|
|
443
443
|
func writeBundleTargets(ws *os.Root, targets []*bundleTarget) error {
|
|
444
|
+
return writeBundleTargetsWith(ws, targets, fsutil.WriteFileAtomicIn)
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
func writeBundleTargetsWith(ws *os.Root, targets []*bundleTarget, write func(*os.Root, string, []byte, fs.FileMode) error) error {
|
|
444
448
|
for i, target := range targets {
|
|
445
|
-
if err :=
|
|
446
|
-
|
|
449
|
+
if err := write(ws, filepath.FromSlash(target.rel), target.after, 0o644); err != nil {
|
|
450
|
+
writeErr := fmt.Errorf("write %s: %w", target.rel, err)
|
|
451
|
+
var rollbackErrors []error
|
|
452
|
+
for j := i; j >= 0; j-- {
|
|
453
|
+
done := targets[j]
|
|
454
|
+
var rollbackErr error
|
|
447
455
|
if done.existed {
|
|
448
|
-
|
|
456
|
+
rollbackErr = write(ws, filepath.FromSlash(done.rel), done.before, 0o644)
|
|
449
457
|
} else {
|
|
450
|
-
|
|
458
|
+
rollbackErr = ws.Remove(filepath.FromSlash(done.rel))
|
|
459
|
+
if errors.Is(rollbackErr, fs.ErrNotExist) {
|
|
460
|
+
rollbackErr = nil
|
|
461
|
+
}
|
|
451
462
|
}
|
|
463
|
+
if rollbackErr != nil {
|
|
464
|
+
rollbackErrors = append(rollbackErrors, fmt.Errorf("rollback %s: %w", done.rel, rollbackErr))
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
if len(rollbackErrors) > 0 {
|
|
468
|
+
return errors.Join(writeErr, fmt.Errorf("rollback incomplete: %w", errors.Join(rollbackErrors...)))
|
|
452
469
|
}
|
|
453
|
-
return fmt.Errorf("
|
|
470
|
+
return fmt.Errorf("%w (attempted files restored)", writeErr)
|
|
454
471
|
}
|
|
455
472
|
}
|
|
456
473
|
return nil
|