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.
- package/CHANGELOG.md +12 -0
- package/NOTICE.md +0 -13
- package/README.md +4 -3
- package/docs/adr/0029-v5-workspace-schema-and-native-migration.md +62 -0
- package/docs/adr/README.md +3 -2
- package/docs/architecture.md +1 -1
- package/docs/cli.md +2 -2
- package/docs/command-map.md +2 -2
- package/docs/engine/commands.md +9 -0
- package/docs/engine/state-schema.md +2 -2
- package/docs/engine/workspace-schema.md +9 -3
- package/docs/usage.md +1 -1
- package/engine/commands.go +23 -0
- package/engine/internal/fsutil/copy_test.go +193 -0
- package/engine/internal/gate/gate_test.go +7 -7
- package/engine/internal/gate/readiness_binding_test.go +8 -2
- package/engine/internal/install/apply.go +314 -0
- package/engine/internal/install/binary.go +255 -0
- package/engine/internal/install/install.go +0 -1596
- package/engine/internal/install/manifest.go +109 -0
- package/engine/internal/install/merge.go +468 -0
- package/engine/internal/install/preflight.go +234 -0
- package/engine/internal/install/uninstall.go +119 -0
- package/engine/internal/install/update.go +184 -0
- package/engine/internal/lib/candidate_test.go +3 -0
- package/engine/internal/lib/closeout.go +4 -0
- package/engine/internal/lib/closeout_test.go +3 -3
- package/engine/internal/lib/migrate.go +219 -0
- package/engine/internal/lib/migrate_test.go +154 -0
- package/engine/internal/lib/resolve.go +3 -0
- package/engine/internal/lib/resolve_remediation_test.go +1 -1
- package/engine/internal/lib/secretscan_test.go +1 -0
- package/engine/internal/lib/taskgraph_test.go +2 -2
- package/engine/internal/parallel/ops.go +67 -16
- package/engine/internal/parallel/ops_test.go +211 -0
- package/engine/internal/state/cmd/workflowmanifest/main.go +24 -13
- package/engine/internal/state/cmd/workflowmanifest/main_test.go +96 -0
- package/engine/internal/state/cursor.go +74 -0
- package/engine/internal/state/schema.go +3 -2
- package/engine/internal/state/state_test.go +2 -2
- package/engine/internal/state/workflow_manifest.json +1 -1
- package/engine/internal/state/workspaceschema.go +58 -0
- package/engine/internal/state/workspaceschema_test.go +67 -0
- package/engine/main.go +7 -31
- package/engine/root_routing_test.go +10 -2
- package/engine/testdata/fixtures/basic/devrites-root/work/auth-tokens/state.md +1 -0
- package/engine/testdata/fixtures/basic/devrites-root/work/search-ranking/state.md +1 -0
- package/engine/testdata/golden/TestParityResolve/answer/state.md.golden +1 -0
- package/engine/testdata/golden/TestParityResolve/drop/state.md.golden +1 -0
- package/engine/tests/parity_closeout_test.go +3 -0
- package/engine/tests/parity_resolve_test.go +1 -0
- package/pack/.claude/agents/devrites-code-reviewer.md +8 -0
- package/pack/.claude/agents/devrites-devex-reviewer.md +7 -0
- package/pack/.claude/agents/devrites-doubt-reviewer.md +7 -0
- package/pack/.claude/agents/devrites-evidence-scout.md +4 -0
- package/pack/.claude/agents/devrites-frontend-reviewer.md +8 -0
- package/pack/.claude/agents/devrites-performance-reviewer.md +7 -0
- package/pack/.claude/agents/devrites-plan-reviewer.md +8 -0
- package/pack/.claude/agents/devrites-retrospector.md +2 -0
- package/pack/.claude/agents/devrites-security-auditor.md +7 -0
- package/pack/.claude/agents/devrites-simplifier-reviewer.md +7 -0
- package/pack/.claude/agents/devrites-spec-reviewer.md +7 -0
- package/pack/.claude/agents/devrites-strategy-reviewer.md +7 -0
- package/pack/.claude/agents/devrites-test-analyst.md +7 -0
- package/pack/.claude/skills/devrites-frontend-craft/SKILL.md +4 -7
- package/pack/.claude/skills/devrites-frontend-craft/reference/quality-standards.md +9 -4
- package/pack/.claude/skills/devrites-lib/reference/intent-map.md +7 -0
- package/pack/.claude/skills/devrites-lib/reference/parallel-dispatch.md +1 -1
- package/pack/.claude/skills/devrites-lib/reference/reply-contract.md +9 -7
- package/pack/.claude/skills/devrites-lib/reference/standards/agents.md +1 -1
- package/pack/.claude/skills/devrites-lib/reference/standards/code-review.md +6 -1
- package/pack/.claude/skills/devrites-lib/reference/standards/coding-style.md +4 -3
- package/pack/.claude/skills/devrites-lib/reference/standards/context-hygiene.md +4 -0
- package/pack/.claude/skills/devrites-lib/reference/standards/core.md +3 -6
- package/pack/.claude/skills/devrites-lib/reference/standards/data-integrity.md +4 -0
- package/pack/.claude/skills/devrites-lib/reference/standards/development-workflow.md +8 -0
- package/pack/.claude/skills/devrites-lib/reference/standards/documentation.md +6 -0
- package/pack/.claude/skills/devrites-lib/reference/standards/edge-case-trace.md +3 -1
- package/pack/.claude/skills/devrites-lib/reference/standards/elicitation.md +5 -0
- package/pack/.claude/skills/devrites-lib/reference/standards/error-handling.md +14 -0
- package/pack/.claude/skills/devrites-lib/reference/standards/integration-reliability.md +18 -6
- package/pack/.claude/skills/devrites-lib/reference/standards/patterns.md +16 -0
- package/pack/.claude/skills/devrites-lib/reference/standards/performance.md +4 -0
- package/pack/.claude/skills/devrites-lib/reference/standards/repository-topology.md +6 -0
- package/pack/.claude/skills/devrites-lib/reference/standards/security-checklist.md +3 -0
- package/pack/.claude/skills/devrites-lib/reference/standards/security.md +10 -2
- package/pack/.claude/skills/devrites-lib/reference/standards/skill-authoring.md +8 -0
- package/pack/.claude/skills/devrites-lib/reference/standards/spec-grammar.md +5 -6
- package/pack/.claude/skills/devrites-lib/reference/standards/testing.md +2 -2
- package/pack/.claude/skills/devrites-lib/reference/standards/tooling.md +1 -1
- package/pack/.claude/skills/rite-define/reference/plan-template.md +5 -2
- package/pack/.claude/skills/rite-learn/SKILL.md +6 -0
- package/pack/.claude/skills/rite-polish/SKILL.md +4 -0
- package/pack/.claude/skills/rite-polish/reference/anti-ai-slop.md +12 -53
- package/pack/.claude/skills/rite-pr-feedback/SKILL.md +7 -2
- package/pack/.claude/skills/rite-spec/reference/spec-template.md +5 -4
- package/pack/.claude/skills/rite-status/SKILL.md +2 -0
- package/pack/generated/claude/agents/devrites-code-reviewer.md +8 -0
- package/pack/generated/claude/agents/devrites-devex-reviewer.md +7 -0
- package/pack/generated/claude/agents/devrites-doubt-reviewer.md +7 -0
- package/pack/generated/claude/agents/devrites-evidence-scout.md +4 -0
- package/pack/generated/claude/agents/devrites-frontend-reviewer.md +8 -0
- package/pack/generated/claude/agents/devrites-performance-reviewer.md +7 -0
- package/pack/generated/claude/agents/devrites-plan-reviewer.md +8 -0
- package/pack/generated/claude/agents/devrites-retrospector.md +2 -0
- package/pack/generated/claude/agents/devrites-security-auditor.md +7 -0
- package/pack/generated/claude/agents/devrites-simplifier-reviewer.md +7 -0
- package/pack/generated/claude/agents/devrites-spec-reviewer.md +7 -0
- package/pack/generated/claude/agents/devrites-strategy-reviewer.md +7 -0
- package/pack/generated/claude/agents/devrites-test-analyst.md +7 -0
- package/pack/generated/claude/skills/devrites-frontend-craft/SKILL.md +4 -7
- package/pack/generated/claude/skills/devrites-frontend-craft/reference/quality-standards.md +9 -4
- package/pack/generated/claude/skills/devrites-lib/reference/intent-map.md +7 -0
- package/pack/generated/claude/skills/devrites-lib/reference/parallel-dispatch.md +1 -1
- package/pack/generated/claude/skills/devrites-lib/reference/reply-contract.md +9 -7
- package/pack/generated/claude/skills/devrites-lib/reference/standards/agents.md +1 -1
- package/pack/generated/claude/skills/devrites-lib/reference/standards/code-review.md +6 -1
- package/pack/generated/claude/skills/devrites-lib/reference/standards/coding-style.md +4 -3
- package/pack/generated/claude/skills/devrites-lib/reference/standards/context-hygiene.md +4 -0
- package/pack/generated/claude/skills/devrites-lib/reference/standards/core.md +3 -6
- package/pack/generated/claude/skills/devrites-lib/reference/standards/data-integrity.md +4 -0
- package/pack/generated/claude/skills/devrites-lib/reference/standards/development-workflow.md +8 -0
- package/pack/generated/claude/skills/devrites-lib/reference/standards/documentation.md +6 -0
- package/pack/generated/claude/skills/devrites-lib/reference/standards/edge-case-trace.md +3 -1
- package/pack/generated/claude/skills/devrites-lib/reference/standards/elicitation.md +5 -0
- package/pack/generated/claude/skills/devrites-lib/reference/standards/error-handling.md +14 -0
- package/pack/generated/claude/skills/devrites-lib/reference/standards/integration-reliability.md +18 -6
- package/pack/generated/claude/skills/devrites-lib/reference/standards/patterns.md +16 -0
- package/pack/generated/claude/skills/devrites-lib/reference/standards/performance.md +4 -0
- package/pack/generated/claude/skills/devrites-lib/reference/standards/repository-topology.md +6 -0
- package/pack/generated/claude/skills/devrites-lib/reference/standards/security-checklist.md +3 -0
- package/pack/generated/claude/skills/devrites-lib/reference/standards/security.md +10 -2
- package/pack/generated/claude/skills/devrites-lib/reference/standards/skill-authoring.md +8 -0
- package/pack/generated/claude/skills/devrites-lib/reference/standards/spec-grammar.md +5 -6
- package/pack/generated/claude/skills/devrites-lib/reference/standards/testing.md +2 -2
- package/pack/generated/claude/skills/devrites-lib/reference/standards/tooling.md +1 -1
- package/pack/generated/claude/skills/rite-define/reference/plan-template.md +5 -2
- package/pack/generated/claude/skills/rite-learn/SKILL.md +6 -0
- package/pack/generated/claude/skills/rite-polish/SKILL.md +4 -0
- package/pack/generated/claude/skills/rite-polish/reference/anti-ai-slop.md +12 -53
- package/pack/generated/claude/skills/rite-pr-feedback/SKILL.md +7 -2
- package/pack/generated/claude/skills/rite-spec/reference/spec-template.md +5 -4
- package/pack/generated/claude/skills/rite-status/SKILL.md +2 -0
- package/pack/generated/codex/agents/devrites-code-reviewer.toml +8 -0
- package/pack/generated/codex/agents/devrites-devex-reviewer.toml +7 -0
- package/pack/generated/codex/agents/devrites-doubt-reviewer.toml +7 -0
- package/pack/generated/codex/agents/devrites-evidence-scout.toml +4 -0
- package/pack/generated/codex/agents/devrites-frontend-reviewer.toml +8 -0
- package/pack/generated/codex/agents/devrites-performance-reviewer.toml +7 -0
- package/pack/generated/codex/agents/devrites-plan-reviewer.toml +8 -0
- package/pack/generated/codex/agents/devrites-retrospector.toml +2 -0
- package/pack/generated/codex/agents/devrites-security-auditor.toml +7 -0
- package/pack/generated/codex/agents/devrites-simplifier-reviewer.toml +7 -0
- package/pack/generated/codex/agents/devrites-spec-reviewer.toml +7 -0
- package/pack/generated/codex/agents/devrites-strategy-reviewer.toml +7 -0
- package/pack/generated/codex/agents/devrites-test-analyst.toml +7 -0
- package/pack/generated/codex/skills/devrites-frontend-craft/SKILL.md +4 -7
- package/pack/generated/codex/skills/devrites-frontend-craft/reference/quality-standards.md +9 -4
- package/pack/generated/codex/skills/devrites-lib/reference/intent-map.md +7 -0
- package/pack/generated/codex/skills/devrites-lib/reference/parallel-dispatch.md +1 -1
- package/pack/generated/codex/skills/devrites-lib/reference/reply-contract.md +9 -7
- package/pack/generated/codex/skills/devrites-lib/reference/standards/agents.md +1 -1
- package/pack/generated/codex/skills/devrites-lib/reference/standards/code-review.md +6 -1
- package/pack/generated/codex/skills/devrites-lib/reference/standards/coding-style.md +4 -3
- package/pack/generated/codex/skills/devrites-lib/reference/standards/context-hygiene.md +4 -0
- package/pack/generated/codex/skills/devrites-lib/reference/standards/core.md +3 -6
- package/pack/generated/codex/skills/devrites-lib/reference/standards/data-integrity.md +4 -0
- package/pack/generated/codex/skills/devrites-lib/reference/standards/development-workflow.md +8 -0
- package/pack/generated/codex/skills/devrites-lib/reference/standards/documentation.md +6 -0
- package/pack/generated/codex/skills/devrites-lib/reference/standards/edge-case-trace.md +3 -1
- package/pack/generated/codex/skills/devrites-lib/reference/standards/elicitation.md +5 -0
- package/pack/generated/codex/skills/devrites-lib/reference/standards/error-handling.md +14 -0
- package/pack/generated/codex/skills/devrites-lib/reference/standards/integration-reliability.md +18 -6
- package/pack/generated/codex/skills/devrites-lib/reference/standards/patterns.md +16 -0
- package/pack/generated/codex/skills/devrites-lib/reference/standards/performance.md +4 -0
- package/pack/generated/codex/skills/devrites-lib/reference/standards/repository-topology.md +6 -0
- package/pack/generated/codex/skills/devrites-lib/reference/standards/security-checklist.md +3 -0
- package/pack/generated/codex/skills/devrites-lib/reference/standards/security.md +10 -2
- package/pack/generated/codex/skills/devrites-lib/reference/standards/skill-authoring.md +8 -0
- package/pack/generated/codex/skills/devrites-lib/reference/standards/spec-grammar.md +5 -6
- package/pack/generated/codex/skills/devrites-lib/reference/standards/testing.md +2 -2
- package/pack/generated/codex/skills/devrites-lib/reference/standards/tooling.md +1 -1
- package/pack/generated/codex/skills/rite-define/reference/plan-template.md +5 -2
- package/pack/generated/codex/skills/rite-learn/SKILL.md +6 -0
- package/pack/generated/codex/skills/rite-polish/SKILL.md +4 -0
- package/pack/generated/codex/skills/rite-polish/reference/anti-ai-slop.md +12 -53
- package/pack/generated/codex/skills/rite-pr-feedback/SKILL.md +7 -2
- package/pack/generated/codex/skills/rite-spec/reference/spec-template.md +5 -4
- package/pack/generated/codex/skills/rite-status/SKILL.md +2 -0
- package/package.json +1 -1
- 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
|
+
}
|