devrites 4.0.0 → 4.0.1
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
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to DevRites are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and DevRites adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Releases are generated automatically by [semantic-release](https://semantic-release.gitbook.io/) from Conventional Commits on `main`.
|
|
4
4
|
|
|
5
|
+
## [4.0.1](https://github.com/ViktorsBaikers/DevRites/compare/v4.0.0...v4.0.1) (2026-08-03)
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
* **installer:** clean legacy Codex hooks during v4 updates ([4908945](https://github.com/ViktorsBaikers/DevRites/commit/49089453ade730a9fede6c9fade180457d8ce2f9))
|
|
10
|
+
|
|
5
11
|
## [4.0.0](https://github.com/ViktorsBaikers/DevRites/compare/v3.2.28...v4.0.0) (2026-08-03)
|
|
6
12
|
|
|
7
13
|
### ⚠ BREAKING CHANGES
|
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ project-conventional push, tag, or PR action, and archive the workspace.
|
|
|
28
28
|
Unattended runs may create local WIP checkpoint commits along the way, but they
|
|
29
29
|
remain local unless Ship's disclosed plan includes an approved remote action.
|
|
30
30
|
|
|
31
|
-
**Status:** [`v4.0.
|
|
31
|
+
**Status:** [`v4.0.1`](https://github.com/ViktorsBaikers/DevRites/releases/tag/v4.0.1): see [`CHANGELOG.md`](CHANGELOG.md) for release notes.
|
|
32
32
|
|
|
33
33
|
This is the latest published release; `main` may contain unreleased work.
|
|
34
34
|
|
|
@@ -107,6 +107,11 @@ Some work needs a different route:
|
|
|
107
107
|
|
|
108
108
|
The npm or shell updater acquires a local install candidate and uses
|
|
109
109
|
`devrites-engine update` to refresh the installed engine and pack offline.
|
|
110
|
+
For a pre-v4 install, do not retry an older engine directly if it reports
|
|
111
|
+
`missing codex/hooks.json`. Run `npx devrites@latest update`, or verify the
|
|
112
|
+
release `install.sh` as shown below and run `bash ./install.sh update`. The v4
|
|
113
|
+
engine updates locally and offline; those adapters acquire its compatible
|
|
114
|
+
candidate first.
|
|
110
115
|
`/rite-upgrade` is the separate native, preservation-first route for reconciling
|
|
111
116
|
an unfinished workspace. It proves a current-contract defect before routing
|
|
112
117
|
Clarify, Plan repair, Converge, Vet, Prove, Polish, Review, or Seal; it never
|
package/docs/cli.md
CHANGED
|
@@ -46,6 +46,12 @@ the exact release candidate selected by the adapter without changing the
|
|
|
46
46
|
installation. Use `npx devrites uninstall --keep-binary` to remove managed host
|
|
47
47
|
artifacts while retaining the shared `devrites-engine` binary.
|
|
48
48
|
|
|
49
|
+
For a pre-v4 install, an older direct `devrites-engine update` can report
|
|
50
|
+
`missing codex/hooks.json`; do not retry that command. Run
|
|
51
|
+
`npx devrites@latest update`, or use a checksum-verified release `install.sh` and
|
|
52
|
+
run `bash ./install.sh update`. The v4 engine update is local and offline; the
|
|
53
|
+
npm or shell adapter acquires the compatible candidate before invoking it.
|
|
54
|
+
|
|
49
55
|
## Checks
|
|
50
56
|
|
|
51
57
|
- `check candidate <slug>` validates the strict `touched-files.md` manifest and
|
|
@@ -93,6 +93,12 @@ var CodexConfigMerge = MarkerMerge{
|
|
|
93
93
|
MarkerText: ".codex/config.toml contains the DevRites read-only-root permission profile.",
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
var LegacyCodexHooksMerge = ManagedMerge{
|
|
97
|
+
MarkerRel: ".claude/devrites.codex-hooks-merge",
|
|
98
|
+
TargetRel: ".codex/hooks.json",
|
|
99
|
+
DryRun: ".codex/hooks.json legacy DevRites hooks",
|
|
100
|
+
}
|
|
101
|
+
|
|
96
102
|
var managedMerges = []ManagedMerge{
|
|
97
103
|
{
|
|
98
104
|
MarkerRel: CodexAgentsMerge.MarkerRel,
|
|
@@ -113,6 +119,7 @@ var managedMerges = []ManagedMerge{
|
|
|
113
119
|
TargetRel: ClaudeSettingsMerge.TargetRel,
|
|
114
120
|
DryRun: ".claude/settings.json DevRites permissions",
|
|
115
121
|
},
|
|
122
|
+
LegacyCodexHooksMerge,
|
|
116
123
|
}
|
|
117
124
|
|
|
118
125
|
func RequiredPayload(withCodex bool) []string {
|
|
@@ -925,7 +925,11 @@ func (r *runner) pruneDropped() error {
|
|
|
925
925
|
if err := r.recheckPath(merge.TargetRel); err != nil {
|
|
926
926
|
return err
|
|
927
927
|
}
|
|
928
|
-
if merge.
|
|
928
|
+
if merge.MarkerRel == hostpack.LegacyCodexHooksMerge.MarkerRel {
|
|
929
|
+
if err := stripHooksPath(filepath.Join(r.target, filepath.FromSlash(merge.TargetRel))); err != nil {
|
|
930
|
+
return fmt.Errorf("strip hooks from %s: %w", merge.TargetRel, err)
|
|
931
|
+
}
|
|
932
|
+
} else if merge.TargetRel == hostpack.ClaudeSettingsMerge.TargetRel {
|
|
929
933
|
_ = r.stripClaudeSettings(filepath.Join(r.target, filepath.FromSlash(merge.TargetRel)), true)
|
|
930
934
|
} else {
|
|
931
935
|
_ = stripMarkerPath(filepath.Join(r.target, filepath.FromSlash(merge.TargetRel)), merge.Begin, merge.End)
|
|
@@ -1023,6 +1027,12 @@ func (r *runner) uninstall() error {
|
|
|
1023
1027
|
if err := r.recheckPath(merge.TargetRel); err != nil {
|
|
1024
1028
|
return err
|
|
1025
1029
|
}
|
|
1030
|
+
if merge.MarkerRel == hostpack.LegacyCodexHooksMerge.MarkerRel {
|
|
1031
|
+
if err := stripHooksPath(filepath.Join(r.target, filepath.FromSlash(merge.TargetRel))); err != nil {
|
|
1032
|
+
return fmt.Errorf("strip hooks from %s: %w", merge.TargetRel, err)
|
|
1033
|
+
}
|
|
1034
|
+
continue
|
|
1035
|
+
}
|
|
1026
1036
|
if merge.TargetRel == hostpack.ClaudeSettingsMerge.TargetRel {
|
|
1027
1037
|
if err := r.stripClaudeSettings(filepath.Join(r.target, filepath.FromSlash(merge.TargetRel)), true); err != nil {
|
|
1028
1038
|
return fmt.Errorf("strip DevRites settings from %s: %w", merge.TargetRel, err)
|
|
@@ -1092,6 +1102,26 @@ func (r *runner) uninstall() error {
|
|
|
1092
1102
|
return nil
|
|
1093
1103
|
}
|
|
1094
1104
|
|
|
1105
|
+
func stripHooksPath(path string) error {
|
|
1106
|
+
current, err := readJSON(path)
|
|
1107
|
+
if err != nil {
|
|
1108
|
+
if errors.Is(err, fs.ErrNotExist) {
|
|
1109
|
+
return nil
|
|
1110
|
+
}
|
|
1111
|
+
return fmt.Errorf("load hooks config: %w", err)
|
|
1112
|
+
}
|
|
1113
|
+
next := stripDevritesHooks(current)
|
|
1114
|
+
if len(next) == 0 {
|
|
1115
|
+
return os.Remove(path)
|
|
1116
|
+
}
|
|
1117
|
+
data, err := json.MarshalIndent(next, "", " ")
|
|
1118
|
+
if err != nil {
|
|
1119
|
+
return fmt.Errorf("encode hooks config: %w", err)
|
|
1120
|
+
}
|
|
1121
|
+
data = append(data, '\n')
|
|
1122
|
+
return fsutil.WriteFileAtomic(path, data, 0o644)
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1095
1125
|
func (r *runner) stripClaudeSettings(path string, preserveEmpty bool) error {
|
|
1096
1126
|
current, err := readJSON(path)
|
|
1097
1127
|
if err != nil {
|
|
@@ -1310,9 +1340,12 @@ func stripDevritesHooks(config map[string]any) map[string]any {
|
|
|
1310
1340
|
}
|
|
1311
1341
|
next[k] = v
|
|
1312
1342
|
}
|
|
1313
|
-
|
|
1343
|
+
rawHooksValue, exists := next["hooks"]
|
|
1344
|
+
if !exists {
|
|
1345
|
+
return next
|
|
1346
|
+
}
|
|
1347
|
+
rawHooks, ok := rawHooksValue.(map[string]any)
|
|
1314
1348
|
if !ok {
|
|
1315
|
-
delete(next, "hooks")
|
|
1316
1349
|
return next
|
|
1317
1350
|
}
|
|
1318
1351
|
hooks := map[string]any{}
|
|
@@ -8,6 +8,7 @@ import (
|
|
|
8
8
|
"os/exec"
|
|
9
9
|
"path/filepath"
|
|
10
10
|
"runtime"
|
|
11
|
+
"slices"
|
|
11
12
|
"strings"
|
|
12
13
|
"testing"
|
|
13
14
|
"time"
|
|
@@ -551,6 +552,106 @@ func TestUpdateInstallsLocalPayload(t *testing.T) {
|
|
|
551
552
|
}
|
|
552
553
|
}
|
|
553
554
|
|
|
555
|
+
func TestUpdateCleansLegacyCodexHooks(t *testing.T) {
|
|
556
|
+
t.Setenv("DEVRITES_NO_BINARY", "1")
|
|
557
|
+
payload := testPayload(t)
|
|
558
|
+
source := testSource(t, "4.0.0")
|
|
559
|
+
|
|
560
|
+
t.Run("preserves user hooks", func(t *testing.T) {
|
|
561
|
+
target := t.TempDir()
|
|
562
|
+
hooks := `{
|
|
563
|
+
"$comment": "DevRites hooks for Codex. Project hooks load only after trust.",
|
|
564
|
+
"hooks": {"Stop":[{"hooks":[
|
|
565
|
+
{"type":"command","command":"devrites-engine hook stop-gate --harness=codex"},
|
|
566
|
+
{"type":"command","command":"echo user"}
|
|
567
|
+
]}]}
|
|
568
|
+
}` + "\n"
|
|
569
|
+
seedLegacyCodexHooksInstall(t, target, hooks)
|
|
570
|
+
|
|
571
|
+
runLocalUpdate(t, target, source, payload)
|
|
572
|
+
|
|
573
|
+
if exists(filepath.Join(target, ".claude", "devrites.codex-hooks-merge")) {
|
|
574
|
+
t.Fatal("update kept the legacy Codex hooks marker")
|
|
575
|
+
}
|
|
576
|
+
got := testutil.ReadFile(t, filepath.Join(target, ".codex", "hooks.json"))
|
|
577
|
+
if strings.Contains(got, "devrites-engine hook") || strings.Contains(got, "DevRites hooks") || !strings.Contains(got, "echo user") {
|
|
578
|
+
t.Fatalf("legacy Codex hook cleanup mismatch:\n%s", got)
|
|
579
|
+
}
|
|
580
|
+
})
|
|
581
|
+
|
|
582
|
+
t.Run("removes empty hooks file", func(t *testing.T) {
|
|
583
|
+
target := t.TempDir()
|
|
584
|
+
hooks := `{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"devrites-engine hook stop-gate --harness=codex"}]}]}}` + "\n"
|
|
585
|
+
seedLegacyCodexHooksInstall(t, target, hooks)
|
|
586
|
+
|
|
587
|
+
runLocalUpdate(t, target, source, payload)
|
|
588
|
+
|
|
589
|
+
if exists(filepath.Join(target, ".codex", "hooks.json")) {
|
|
590
|
+
t.Fatal("update kept an empty legacy Codex hooks file")
|
|
591
|
+
}
|
|
592
|
+
})
|
|
593
|
+
|
|
594
|
+
t.Run("preserves unrecognized user hook shape", func(t *testing.T) {
|
|
595
|
+
target := t.TempDir()
|
|
596
|
+
seedLegacyCodexHooksInstall(t, target, `{"hooks":"user-defined"}`+"\n")
|
|
597
|
+
|
|
598
|
+
runLocalUpdate(t, target, source, payload)
|
|
599
|
+
|
|
600
|
+
got := testutil.ReadFile(t, filepath.Join(target, ".codex", "hooks.json"))
|
|
601
|
+
if !strings.Contains(got, `"hooks": "user-defined"`) {
|
|
602
|
+
t.Fatalf("legacy cleanup removed unrecognized user hooks: %s", got)
|
|
603
|
+
}
|
|
604
|
+
})
|
|
605
|
+
|
|
606
|
+
t.Run("malformed hooks keep ownership marker", func(t *testing.T) {
|
|
607
|
+
target := t.TempDir()
|
|
608
|
+
seedLegacyCodexHooksInstall(t, target, "{\n")
|
|
609
|
+
|
|
610
|
+
err := applyUpdate(target, source, payload)
|
|
611
|
+
if err == nil || !strings.Contains(err.Error(), "load hooks config") {
|
|
612
|
+
t.Fatalf("update error = %v, want malformed legacy hooks error", err)
|
|
613
|
+
}
|
|
614
|
+
if !exists(filepath.Join(target, ".claude", "devrites.codex-hooks-merge")) {
|
|
615
|
+
t.Fatal("failed cleanup removed the legacy ownership marker")
|
|
616
|
+
}
|
|
617
|
+
if got := testutil.ReadFile(t, filepath.Join(target, ".codex", "hooks.json")); got != "{\n" {
|
|
618
|
+
t.Fatalf("failed cleanup changed hooks: %q", got)
|
|
619
|
+
}
|
|
620
|
+
})
|
|
621
|
+
|
|
622
|
+
t.Run("uninstall preserves user hooks", func(t *testing.T) {
|
|
623
|
+
target := t.TempDir()
|
|
624
|
+
hooks := `{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"devrites-engine hook stop-gate"},{"type":"command","command":"echo user"}]}]}}` + "\n"
|
|
625
|
+
seedLegacyCodexHooksInstall(t, target, hooks)
|
|
626
|
+
|
|
627
|
+
runUninstall(t, target)
|
|
628
|
+
|
|
629
|
+
got := testutil.ReadFile(t, filepath.Join(target, ".codex", "hooks.json"))
|
|
630
|
+
if strings.Contains(got, "devrites-engine hook") || !strings.Contains(got, "echo user") {
|
|
631
|
+
t.Fatalf("legacy Codex hook uninstall mismatch:\n%s", got)
|
|
632
|
+
}
|
|
633
|
+
})
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
func TestLegacyCodexHooksMergeIsCleanupOnly(t *testing.T) {
|
|
637
|
+
legacy := hostpack.LegacyCodexHooksMerge
|
|
638
|
+
merge, ok := hostpack.ManagedMergeForMarker(legacy.MarkerRel)
|
|
639
|
+
if !ok || merge != legacy {
|
|
640
|
+
t.Fatalf("legacy marker lookup = %#v, %t", merge, ok)
|
|
641
|
+
}
|
|
642
|
+
if slices.Contains(hostpack.RequiredPayload(true), "codex/hooks.json") {
|
|
643
|
+
t.Fatal("legacy Codex hooks entered the required payload")
|
|
644
|
+
}
|
|
645
|
+
r := runner{opts: DefaultOptions(ModeInstall), payloadFS: os.DirFS(testPayload(t))}
|
|
646
|
+
desired, err := r.desiredInstallPaths()
|
|
647
|
+
if err != nil {
|
|
648
|
+
t.Fatal(err)
|
|
649
|
+
}
|
|
650
|
+
if desired[legacy.MarkerRel] || desired[legacy.TargetRel] || slices.Contains(r.installMergeTargets(), legacy.TargetRel) {
|
|
651
|
+
t.Fatalf("legacy Codex hooks entered active install paths: desired=%v merge-targets=%v", desired, r.installMergeTargets())
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
554
655
|
func TestUpdateRejectsMissingGeneratedPayload(t *testing.T) {
|
|
555
656
|
t.Setenv("DEVRITES_NO_BINARY", "1")
|
|
556
657
|
oldPayload := testPayload(t)
|
|
@@ -939,6 +1040,33 @@ func applyInstall(target, payload string, force, dryRun bool, out *bytes.Buffer)
|
|
|
939
1040
|
return Apply(opts)
|
|
940
1041
|
}
|
|
941
1042
|
|
|
1043
|
+
func applyUpdate(target, source, payload string) error {
|
|
1044
|
+
opts := DefaultOptions(ModeUpdate)
|
|
1045
|
+
opts.Target = target
|
|
1046
|
+
opts.SourceDir = source
|
|
1047
|
+
opts.PayloadDir = payload
|
|
1048
|
+
opts.Stdout = &bytes.Buffer{}
|
|
1049
|
+
opts.Stderr = &bytes.Buffer{}
|
|
1050
|
+
return Apply(opts)
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
func runLocalUpdate(t *testing.T, target, source, payload string) {
|
|
1054
|
+
t.Helper()
|
|
1055
|
+
if err := applyUpdate(target, source, payload); err != nil {
|
|
1056
|
+
t.Fatal(err)
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
func seedLegacyCodexHooksInstall(t *testing.T, target, hooks string) {
|
|
1061
|
+
t.Helper()
|
|
1062
|
+
markerRel := ".claude/devrites.codex-hooks-merge"
|
|
1063
|
+
marker := []byte(".codex/hooks.json contains DevRites managed hook entries.\n")
|
|
1064
|
+
testutil.WriteFile(t, filepath.Join(target, ".codex", "hooks.json"), hooks)
|
|
1065
|
+
testutil.WriteFile(t, filepath.Join(target, filepath.FromSlash(markerRel)), string(marker))
|
|
1066
|
+
testutil.WriteFile(t, filepath.Join(target, ManifestName), "# DevRites install manifest - do not edit by hand.\n# devrites-version: 3.2.27\n# devrites-flags: \n")
|
|
1067
|
+
addManifestRecord(t, filepath.Join(target, ManifestName), markerRel, marker)
|
|
1068
|
+
}
|
|
1069
|
+
|
|
942
1070
|
func addManifestRecord(t *testing.T, manifest, rel string, data []byte) {
|
|
943
1071
|
t.Helper()
|
|
944
1072
|
hash := fmt.Sprintf("sha256:%x", sha256.Sum256(data))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devrites",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.1",
|
|
4
4
|
"description": "DevRites: a disciplined senior-engineer workflow pack for Claude Code and Codex",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"homepage": "https://github.com/ViktorsBaikers/DevRites#readme",
|