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,109 @@
1
+ package install
2
+
3
+ import (
4
+ "crypto/sha256"
5
+ "encoding/hex"
6
+ "encoding/json"
7
+ "errors"
8
+ "fmt"
9
+ "io/fs"
10
+ "os"
11
+ "path/filepath"
12
+ "strings"
13
+
14
+ "github.com/devrites/devrites/internal/version"
15
+ )
16
+
17
+ func readManifest(path string) (map[string]managedRecord, error) {
18
+ records, _, err := parseManifest(path)
19
+ return records, err
20
+ }
21
+
22
+ func readManifestList(path string) ([]string, error) {
23
+ _, entries, err := parseManifest(path)
24
+ return entries, err
25
+ }
26
+
27
+ func parseManifest(path string) (map[string]managedRecord, []string, error) {
28
+ data, err := os.ReadFile(path)
29
+ if err != nil {
30
+ if errors.Is(err, fs.ErrNotExist) {
31
+ return map[string]managedRecord{}, nil, nil
32
+ }
33
+ return nil, nil, fmt.Errorf("read manifest %s: %w", path, err)
34
+ }
35
+ hashes := map[string]string{}
36
+ var out []string
37
+ for _, line := range strings.Split(string(data), "\n") {
38
+ line = strings.TrimSpace(line)
39
+ if strings.HasPrefix(line, "# managed: ") {
40
+ record := strings.TrimPrefix(line, "# managed: ")
41
+ i := strings.LastIndex(record, " sha256:")
42
+ if i > 0 {
43
+ rel := strings.TrimSpace(record[:i])
44
+ hash := "sha256:" + strings.ToLower(strings.TrimSpace(record[i+len(" sha256:"):]))
45
+ if filepath.IsLocal(filepath.FromSlash(rel)) && validManagedHash(hash) {
46
+ hashes[rel] = hash
47
+ }
48
+ }
49
+ continue
50
+ }
51
+ if line == "" || strings.HasPrefix(line, "#") {
52
+ continue
53
+ }
54
+ // The manifest is hand-editable; an entry that escapes the target
55
+ // (absolute or ..-traversal) must never be overwritten or removed.
56
+ if !filepath.IsLocal(filepath.FromSlash(line)) {
57
+ continue
58
+ }
59
+ out = append(out, line)
60
+ }
61
+ records := make(map[string]managedRecord, len(out))
62
+ for _, rel := range out {
63
+ records[rel] = managedRecord{Hash: hashes[rel]}
64
+ }
65
+ return records, out, nil
66
+ }
67
+
68
+ func validManagedHash(hash string) bool {
69
+ const prefix = "sha256:"
70
+ if !strings.HasPrefix(hash, prefix) || len(hash) != len(prefix)+sha256.Size*2 {
71
+ return false
72
+ }
73
+ _, err := hex.DecodeString(strings.TrimPrefix(hash, prefix))
74
+ return err == nil
75
+ }
76
+
77
+ func manifestHeader(path, key string) string {
78
+ data, err := os.ReadFile(path)
79
+ if err != nil {
80
+ return ""
81
+ }
82
+ prefix := "# " + key + ":"
83
+ for _, line := range strings.Split(string(data), "\n") {
84
+ if strings.HasPrefix(line, prefix) {
85
+ return strings.TrimSpace(strings.TrimPrefix(line, prefix))
86
+ }
87
+ }
88
+ return ""
89
+ }
90
+
91
+ func installedVersion(source string) string {
92
+ if source != "" {
93
+ data, err := os.ReadFile(filepath.Join(source, "package.json"))
94
+ if err != nil {
95
+ return "unknown"
96
+ }
97
+ var pkg struct {
98
+ Version string `json:"version"`
99
+ }
100
+ if json.Unmarshal(data, &pkg) != nil || pkg.Version == "" {
101
+ return "unknown"
102
+ }
103
+ return pkg.Version
104
+ }
105
+ if version.Version != "" && version.Version != "dev" {
106
+ return strings.TrimPrefix(version.Version, "v")
107
+ }
108
+ return "unknown"
109
+ }
@@ -0,0 +1,468 @@
1
+ package install
2
+
3
+ import (
4
+ "bytes"
5
+ "encoding/json"
6
+ "errors"
7
+ "fmt"
8
+ "io/fs"
9
+ "os"
10
+ "path/filepath"
11
+ "strings"
12
+
13
+ "github.com/devrites/devrites/internal/fsutil"
14
+ "github.com/devrites/devrites/internal/hostpack"
15
+ )
16
+
17
+ func (r *runner) installClaudeSettingsMarker(ownsDefaultMode bool) error {
18
+ ownership := "preexisting"
19
+ if ownsDefaultMode {
20
+ ownership = "added"
21
+ }
22
+ text := hostpack.ClaudeSettingsMerge.MarkerText + "\ndefault-mode=" + ownership
23
+ return r.installMarker(hostpack.ClaudeSettingsMerge.MarkerRel, text)
24
+ }
25
+
26
+ func (r *runner) claudeDefaultModeOwned() bool {
27
+ data, err := os.ReadFile(filepath.Join(r.target, filepath.FromSlash(hostpack.ClaudeSettingsMerge.MarkerRel)))
28
+ return err == nil && strings.Contains(string(data), "\ndefault-mode=added\n")
29
+ }
30
+
31
+ func (r *runner) mergeMarkerFile(merge hostpack.MarkerMerge) error {
32
+ block, err := fs.ReadFile(r.payloadFS, merge.PayloadRel)
33
+ if err != nil {
34
+ return fmt.Errorf("read payload %s: %w", merge.PayloadRel, err)
35
+ }
36
+ dest := filepath.Join(r.target, filepath.FromSlash(merge.TargetRel))
37
+ if r.opts.DryRun {
38
+ verb := "create DevRites block"
39
+ current, readErr := os.ReadFile(dest)
40
+ if readErr == nil {
41
+ if bytes.Contains(current, []byte(merge.Begin)) {
42
+ verb = "refresh DevRites block"
43
+ } else {
44
+ verb = "append DevRites block"
45
+ }
46
+ } else if !errors.Is(readErr, fs.ErrNotExist) {
47
+ return fmt.Errorf("cannot read %s: %w", merge.TargetRel, readErr)
48
+ }
49
+ fmt.Fprintf(r.opts.Stdout, " [merge] %s (%s)\n", merge.TargetRel, verb)
50
+ } else {
51
+ if err := r.recheckPath(merge.TargetRel); err != nil {
52
+ return err
53
+ }
54
+ next := block
55
+ current, readErr := os.ReadFile(dest)
56
+ if readErr == nil {
57
+ next = hostpack.MergeMarkerBlock(current, block, merge.Begin, merge.End)
58
+ } else if !errors.Is(readErr, fs.ErrNotExist) {
59
+ return fmt.Errorf("cannot read %s: %w", merge.TargetRel, readErr)
60
+ }
61
+ if err := fsutil.WriteFileAtomic(dest, next, 0o644); err != nil {
62
+ return fmt.Errorf("cannot write %s: %w", merge.TargetRel, err)
63
+ }
64
+ }
65
+ return r.installMarker(merge.MarkerRel, merge.MarkerText)
66
+ }
67
+
68
+ func (r *runner) mergeCodexConfig() error {
69
+ merge := hostpack.CodexConfigMerge
70
+ block, err := fs.ReadFile(r.payloadFS, merge.PayloadRel)
71
+ if err != nil {
72
+ return fmt.Errorf("read payload %s: %w", merge.PayloadRel, err)
73
+ }
74
+ dest := filepath.Join(r.target, filepath.FromSlash(merge.TargetRel))
75
+ if r.opts.DryRun {
76
+ fmt.Fprintf(r.opts.Stdout, " [merge] %s (prepend DevRites permission block)\n", merge.TargetRel)
77
+ return r.installMarker(merge.MarkerRel, merge.MarkerText)
78
+ }
79
+ if err := r.recheckPath(merge.TargetRel); err != nil {
80
+ return err
81
+ }
82
+ var current []byte
83
+ if data, readErr := os.ReadFile(dest); readErr == nil {
84
+ current = stripMarkerBlock(data, merge.Begin, merge.End)
85
+ current = stripMarkerBlock(current, "# BEGIN DEVRITES CODEX MCP", "# END DEVRITES CODEX MCP")
86
+ current = stripMarkerBlock(current, "### BEGIN DEVRITES CODEX MCP", "### END DEVRITES CODEX MCP")
87
+ if hasTopLevelTOMLKey(current, "default_permissions") {
88
+ return fmt.Errorf("%s already sets top-level default_permissions; remove that project override before installing the DevRites read-only-root profile", merge.TargetRel)
89
+ }
90
+ } else if !errors.Is(readErr, fs.ErrNotExist) {
91
+ return fmt.Errorf("cannot read %s: %w", merge.TargetRel, readErr)
92
+ }
93
+ next := append([]byte(nil), block...)
94
+ if len(next) == 0 || next[len(next)-1] != '\n' {
95
+ next = append(next, '\n')
96
+ }
97
+ if len(bytes.TrimSpace(current)) > 0 {
98
+ next = append(next, '\n')
99
+ next = append(next, current...)
100
+ }
101
+ if err := fsutil.WriteFileAtomic(dest, next, 0o644); err != nil {
102
+ return fmt.Errorf("cannot write %s: %w", merge.TargetRel, err)
103
+ }
104
+ return r.installMarker(merge.MarkerRel, merge.MarkerText)
105
+ }
106
+
107
+ func (r *runner) mergeClaudeSettings(merge hostpack.JSONMerge) error {
108
+ dest := filepath.Join(r.target, filepath.FromSlash(merge.TargetRel))
109
+ devrites, err := readJSONFS(r.payloadFS, merge.PayloadRel)
110
+ if err != nil {
111
+ return fmt.Errorf("load Claude settings payload: %w", err)
112
+ }
113
+ current := map[string]any{}
114
+ if data, readErr := readJSON(dest); readErr == nil {
115
+ current = data
116
+ } else if !errors.Is(readErr, fs.ErrNotExist) {
117
+ return fmt.Errorf("load existing Claude settings: %w", readErr)
118
+ }
119
+ next, ownsDefaultMode, err := mergeClaudeSettingsConfig(current, devrites, r.claudeDefaultModeOwned())
120
+ if err != nil {
121
+ return err
122
+ }
123
+ if r.opts.DryRun {
124
+ fmt.Fprintf(r.opts.Stdout, " [merge] %s\n", merge.DryRunText)
125
+ return r.installClaudeSettingsMarker(ownsDefaultMode)
126
+ }
127
+ if err := r.recheckPath(merge.TargetRel); err != nil {
128
+ return err
129
+ }
130
+ data, err := json.MarshalIndent(next, "", " ")
131
+ if err != nil {
132
+ return fmt.Errorf("encode merged Claude settings: %w", err)
133
+ }
134
+ data = append(data, '\n')
135
+ if err := fsutil.WriteFileAtomic(dest, data, 0o644); err != nil {
136
+ return fmt.Errorf("cannot write %s: %w", merge.TargetRel, err)
137
+ }
138
+ return r.installClaudeSettingsMarker(ownsDefaultMode)
139
+ }
140
+
141
+ func (r *runner) seedClaudeSettings() error {
142
+ return r.mergeClaudeSettings(hostpack.ClaudeSettingsMerge)
143
+ }
144
+
145
+ func stripHooksPath(path string) error {
146
+ current, err := readJSON(path)
147
+ if err != nil {
148
+ if errors.Is(err, fs.ErrNotExist) {
149
+ return nil
150
+ }
151
+ return fmt.Errorf("load hooks config: %w", err)
152
+ }
153
+ next := stripDevritesHooks(current)
154
+ if len(next) == 0 {
155
+ return os.Remove(path)
156
+ }
157
+ data, err := json.MarshalIndent(next, "", " ")
158
+ if err != nil {
159
+ return fmt.Errorf("encode hooks config: %w", err)
160
+ }
161
+ data = append(data, '\n')
162
+ return fsutil.WriteFileAtomic(path, data, 0o644)
163
+ }
164
+
165
+ func (r *runner) stripClaudeSettings(path string, preserveEmpty bool) error {
166
+ current, err := readJSON(path)
167
+ if err != nil {
168
+ if errors.Is(err, fs.ErrNotExist) {
169
+ return nil
170
+ }
171
+ return fmt.Errorf("load Claude settings: %w", err)
172
+ }
173
+ next := stripDevritesSettings(current, r.claudeDefaultModeOwned())
174
+ if len(next) == 0 && !preserveEmpty {
175
+ return os.Remove(path)
176
+ }
177
+ data, err := json.MarshalIndent(next, "", " ")
178
+ if err != nil {
179
+ return fmt.Errorf("encode Claude settings: %w", err)
180
+ }
181
+ data = append(data, '\n')
182
+ return fsutil.WriteFileAtomic(path, data, 0o644)
183
+ }
184
+
185
+ func stripMarkerPath(path, begin, end string) error {
186
+ data, err := os.ReadFile(path)
187
+ if err != nil {
188
+ if os.IsNotExist(err) {
189
+ return nil
190
+ }
191
+ return fmt.Errorf("read %s: %w", path, err)
192
+ }
193
+ next := stripMarkerBlock(data, begin, end)
194
+ if strings.TrimSpace(string(next)) == "" {
195
+ return os.Remove(path)
196
+ }
197
+ return fsutil.WriteFileAtomic(path, next, 0o644)
198
+ }
199
+
200
+ func stripMarkerBlock(data []byte, begin, end string) []byte {
201
+ var out strings.Builder
202
+ inBlock := false
203
+ for _, line := range strings.SplitAfter(string(data), "\n") {
204
+ trim := strings.TrimSuffix(line, "\n")
205
+ trim = strings.TrimSuffix(trim, "\r")
206
+ switch trim {
207
+ case begin:
208
+ inBlock = true
209
+ continue
210
+ case end:
211
+ inBlock = false
212
+ continue
213
+ }
214
+ if !inBlock {
215
+ out.WriteString(line)
216
+ }
217
+ }
218
+ return []byte(out.String())
219
+ }
220
+
221
+ func hasTopLevelTOMLKey(data []byte, key string) bool {
222
+ for _, line := range strings.Split(string(data), "\n") {
223
+ line = strings.TrimSpace(line)
224
+ if line == "" || strings.HasPrefix(line, "#") {
225
+ continue
226
+ }
227
+ if strings.HasPrefix(line, "[") {
228
+ return false
229
+ }
230
+ name, _, ok := strings.Cut(line, "=")
231
+ if ok && strings.TrimSpace(name) == key {
232
+ return true
233
+ }
234
+ }
235
+ return false
236
+ }
237
+
238
+ func readJSON(path string) (map[string]any, error) {
239
+ data, err := os.ReadFile(path)
240
+ if err != nil {
241
+ return nil, fmt.Errorf("read %s: %w", path, err)
242
+ }
243
+ return decodeJSON(data)
244
+ }
245
+
246
+ func readJSONFS(src fs.FS, path string) (map[string]any, error) {
247
+ data, err := fs.ReadFile(src, path)
248
+ if err != nil {
249
+ return nil, fmt.Errorf("read %s: %w", path, err)
250
+ }
251
+ return decodeJSON(data)
252
+ }
253
+
254
+ func decodeJSON(data []byte) (map[string]any, error) {
255
+ var out map[string]any
256
+ if err := json.Unmarshal(data, &out); err != nil {
257
+ return nil, fmt.Errorf("decode JSON: %w", err)
258
+ }
259
+ return out, nil
260
+ }
261
+
262
+ func isDevritesHookCommand(v any) bool {
263
+ hook, ok := v.(map[string]any)
264
+ if !ok {
265
+ return false
266
+ }
267
+ command, _ := hook["command"].(string)
268
+ return strings.Contains(command, "devrites-engine hook ") ||
269
+ strings.Contains(command, ".claude/hooks/devrites-") ||
270
+ strings.Contains(command, ".codex/hooks/devrites-") ||
271
+ (strings.Contains(command, "printf ") && strings.Contains(command, "DevRites:"))
272
+ }
273
+
274
+ func isDevritesHooksComment(comment string) bool {
275
+ return comment == "DevRites hooks" ||
276
+ strings.HasPrefix(comment, "DevRites hooks: every event invokes the global `devrites-engine` engine binary") ||
277
+ strings.HasPrefix(comment, "DevRites hooks: auto-approve the read-only orientation/gate scripts") ||
278
+ strings.HasPrefix(comment, "DevRites hooks — every event invokes the global `devrites-engine` engine binary") ||
279
+ strings.HasPrefix(comment, "DevRites hooks — auto-approve the read-only orientation/gate scripts") ||
280
+ strings.HasPrefix(comment, "DevRites hooks for Codex. Project hooks load only after")
281
+ }
282
+
283
+ func isDevritesSettingsComment(comment string) bool {
284
+ return isDevritesHooksComment(comment) ||
285
+ strings.HasPrefix(comment, "DevRites keeps the root orchestration context source-read-only.")
286
+ }
287
+
288
+ func stripDevritesHooks(config map[string]any) map[string]any {
289
+ next := map[string]any{}
290
+ for k, v := range config {
291
+ if k == "$comment" {
292
+ if s, ok := v.(string); ok && isDevritesHooksComment(s) {
293
+ continue
294
+ }
295
+ }
296
+ if k == "statusLine" && isDevritesHookCommand(v) {
297
+ continue
298
+ }
299
+ next[k] = v
300
+ }
301
+ rawHooksValue, exists := next["hooks"]
302
+ if !exists {
303
+ return next
304
+ }
305
+ rawHooks, ok := rawHooksValue.(map[string]any)
306
+ if !ok {
307
+ return next
308
+ }
309
+ hooks := map[string]any{}
310
+ for event, entries := range rawHooks {
311
+ arr, ok := entries.([]any)
312
+ if !ok {
313
+ hooks[event] = entries
314
+ continue
315
+ }
316
+ var kept []any
317
+ for _, entry := range arr {
318
+ entryMap, ok := entry.(map[string]any)
319
+ if !ok {
320
+ kept = append(kept, entry)
321
+ continue
322
+ }
323
+ commands, ok := entryMap["hooks"].([]any)
324
+ if !ok {
325
+ kept = append(kept, entry)
326
+ continue
327
+ }
328
+ var keptCommands []any
329
+ for _, command := range commands {
330
+ if !isDevritesHookCommand(command) {
331
+ keptCommands = append(keptCommands, command)
332
+ }
333
+ }
334
+ if len(keptCommands) > 0 {
335
+ nextEntry := make(map[string]any, len(entryMap))
336
+ for k, v := range entryMap {
337
+ nextEntry[k] = v
338
+ }
339
+ nextEntry["hooks"] = keptCommands
340
+ kept = append(kept, nextEntry)
341
+ }
342
+ }
343
+ if len(kept) > 0 {
344
+ hooks[event] = kept
345
+ }
346
+ }
347
+ if len(hooks) == 0 {
348
+ delete(next, "hooks")
349
+ } else {
350
+ next["hooks"] = hooks
351
+ }
352
+ return next
353
+ }
354
+
355
+ func stripDevritesSettings(config map[string]any, removeOwnedDefaultMode bool) map[string]any {
356
+ next := stripDevritesHooks(config)
357
+ if comment, ok := next["$comment"].(string); ok && isDevritesSettingsComment(comment) {
358
+ delete(next, "$comment")
359
+ }
360
+
361
+ rawPermissions, exists := next["permissions"]
362
+ if !exists {
363
+ return next
364
+ }
365
+ permissions, ok := rawPermissions.(map[string]any)
366
+ if !ok {
367
+ return next
368
+ }
369
+ clean := make(map[string]any, len(permissions))
370
+ for key, value := range permissions {
371
+ switch key {
372
+ case "allow":
373
+ rules, ok := value.([]any)
374
+ if !ok {
375
+ clean[key] = value
376
+ continue
377
+ }
378
+ kept := make([]any, 0, len(rules))
379
+ for _, rule := range rules {
380
+ if !isDevritesPermissionRule(rule) {
381
+ kept = append(kept, rule)
382
+ }
383
+ }
384
+ if len(kept) > 0 {
385
+ clean[key] = kept
386
+ }
387
+ case "defaultMode":
388
+ if removeOwnedDefaultMode && value == "plan" {
389
+ continue
390
+ }
391
+ clean[key] = value
392
+ default:
393
+ clean[key] = value
394
+ }
395
+ }
396
+ if len(clean) == 0 {
397
+ delete(next, "permissions")
398
+ } else {
399
+ next["permissions"] = clean
400
+ }
401
+ return next
402
+ }
403
+
404
+ func mergeClaudeSettingsConfig(current, desired map[string]any, defaultModeOwned bool) (map[string]any, bool, error) {
405
+ next := stripDevritesSettings(current, false)
406
+ permissions := map[string]any{}
407
+ if raw, exists := next["permissions"]; exists {
408
+ var ok bool
409
+ permissions, ok = raw.(map[string]any)
410
+ if !ok {
411
+ return nil, false, fmt.Errorf("existing Claude permissions must be a JSON object")
412
+ }
413
+ }
414
+
415
+ switch mode, exists := permissions["defaultMode"]; {
416
+ case !exists:
417
+ defaultModeOwned = true
418
+ case mode == "plan":
419
+ case mode != "plan":
420
+ return nil, false, fmt.Errorf("existing Claude permissions.defaultMode is %q; DevRites requires plan mode for the read-only root orchestrator", mode)
421
+ }
422
+ permissions["defaultMode"] = "plan"
423
+
424
+ desiredPermissions, ok := desired["permissions"].(map[string]any)
425
+ if !ok || desiredPermissions["defaultMode"] != "plan" {
426
+ return nil, false, fmt.Errorf("DevRites Claude settings payload must declare permissions.defaultMode=plan")
427
+ }
428
+ desiredAllow, ok := desiredPermissions["allow"].([]any)
429
+ if !ok {
430
+ return nil, false, fmt.Errorf("DevRites Claude settings payload permissions.allow must be an array")
431
+ }
432
+ var allow []any
433
+ if existingAllow, exists := permissions["allow"]; exists {
434
+ allow, ok = existingAllow.([]any)
435
+ if !ok {
436
+ return nil, false, fmt.Errorf("existing Claude permissions.allow must be an array")
437
+ }
438
+ }
439
+ seen := map[string]bool{}
440
+ for _, rule := range allow {
441
+ if value, ok := rule.(string); ok {
442
+ seen[value] = true
443
+ }
444
+ }
445
+ for _, rule := range desiredAllow {
446
+ value, ok := rule.(string)
447
+ if !ok {
448
+ return nil, false, fmt.Errorf("DevRites Claude permission rules must be strings")
449
+ }
450
+ if !seen[value] {
451
+ allow = append(allow, value)
452
+ seen[value] = true
453
+ }
454
+ }
455
+ permissions["allow"] = allow
456
+ next["permissions"] = permissions
457
+ if _, exists := next["$comment"]; !exists {
458
+ if comment, ok := desired["$comment"].(string); ok {
459
+ next["$comment"] = comment
460
+ }
461
+ }
462
+ return next, defaultModeOwned, nil
463
+ }
464
+
465
+ func isDevritesPermissionRule(value any) bool {
466
+ rule, ok := value.(string)
467
+ return ok && strings.HasPrefix(strings.TrimSpace(rule), "Bash(devrites-engine ")
468
+ }