devrites 2.5.2 → 2.6.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 +6 -0
- package/README.md +24 -13
- package/bin/devrites.mjs +2 -1
- package/docs/cli-mcp.md +9 -4
- package/docs/skills.md +2 -2
- package/install.sh +558 -9
- package/mcp/devrites-mcp.mjs +2 -0
- package/pack/.claude/hooks/devrites-a1-guard.sh +34 -16
- package/pack/.claude/hooks/devrites-reviewer-readonly.sh +11 -2
- package/pack/.claude/hooks/devrites-wright-scope.sh +38 -9
- package/pack/.claude/skills/devrites-lib/scripts/doctor.sh +68 -4
- package/pack/.claude/skills/rite-doctor/SKILL.md +2 -1
- package/package.json +3 -2
- package/scripts/check-no-global-writes.sh +4 -4
- package/scripts/install-lib.sh +11 -1
- package/scripts/merge-codex-hooks.mjs +78 -0
- package/scripts/pin.sh +42 -5
- package/uninstall.sh +60 -0
package/mcp/devrites-mcp.mjs
CHANGED
|
@@ -68,6 +68,8 @@ function handle(msg) {
|
|
|
68
68
|
protocolVersion: (params && params.protocolVersion) || '2024-11-05',
|
|
69
69
|
capabilities: { tools: {} },
|
|
70
70
|
serverInfo: { name: 'devrites', version: '1.0.0' },
|
|
71
|
+
instructions:
|
|
72
|
+
'DevRites exposes deterministic workflow state and gates for the current repo. Use devrites_status/orient to inspect the active feature, devrites_ready before build work, devrites_evidence_fresh and devrites_acceptance before seal/ship, and devrites_use only when the user asks to switch the active feature.',
|
|
71
73
|
});
|
|
72
74
|
case 'notifications/initialized':
|
|
73
75
|
case 'initialized':
|
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
#
|
|
7
7
|
# Safety model: FAIL-OPEN, and OBSERVE by default.
|
|
8
8
|
# - It NEVER blocks unless ALL hold: a build window is open, the edit is to a source file,
|
|
9
|
-
# it came from the MAIN thread (no agent_id), and enforce mode is on.
|
|
9
|
+
# it came from the MAIN thread (no agent_id/agent_type), and enforce mode is on.
|
|
10
10
|
# - On any doubt — no active workspace, no open window, no node, unparseable payload, a
|
|
11
11
|
# subagent caller, a .devrites/ path, the inline fallback — it exits 0 and stays silent.
|
|
12
12
|
# - Default mode is "observe": it LOGS what it would block (to .a1-guard.log) and allows.
|
|
13
13
|
# Flip to enforce only AFTER confirming the log never flags the wright's own edits — that
|
|
14
|
-
# confirms this
|
|
15
|
-
# if the log flags wright edits,
|
|
14
|
+
# confirms this agent surface populates agent_id/agent_type for subagents (older builds may not;
|
|
15
|
+
# if the log flags wright edits, subagent identity is unavailable here — stay in observe).
|
|
16
16
|
#
|
|
17
17
|
# Enable enforce (either one):
|
|
18
18
|
# export DEVRITES_A1_HOOK=enforce # session-wide
|
|
@@ -47,24 +47,42 @@ d="$root/.devrites/work/$slug"
|
|
|
47
47
|
|
|
48
48
|
# Parse with node (ships with Claude Code). No node or a parse failure → fail open.
|
|
49
49
|
command -v node >/dev/null 2>&1 || exit 0
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
$
|
|
54
|
-
EOF
|
|
50
|
+
tool="$(printf '%s' "$input" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{try{process.stdout.write(JSON.parse(s).tool_name||"")}catch(e){}})' 2>/dev/null)"
|
|
51
|
+
fpath="$(printf '%s' "$input" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{try{const ti=JSON.parse(s).tool_input||{};process.stdout.write(ti.file_path||ti.path||"")}catch(e){}})' 2>/dev/null)"
|
|
52
|
+
agent="$(printf '%s' "$input" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{try{const p=JSON.parse(s);process.stdout.write(p.agent_id||p.agent_type||"")}catch(e){}})' 2>/dev/null)"
|
|
53
|
+
[ -n "$tool" ] || exit 0
|
|
55
54
|
|
|
56
55
|
# Only Edit-family writes can touch source.
|
|
57
|
-
case "$tool" in Edit|Write|MultiEdit|NotebookEdit) : ;; *) exit 0 ;; esac
|
|
56
|
+
case "$tool" in Edit|Write|MultiEdit|NotebookEdit|apply_patch) : ;; *) exit 0 ;; esac
|
|
58
57
|
|
|
59
|
-
# Subagent caller (the wright) → allow.
|
|
58
|
+
# Subagent caller (the wright) → allow. Claude uses agent_id; Codex uses agent_type.
|
|
60
59
|
[ -n "$agent" ] && exit 0
|
|
61
60
|
|
|
62
|
-
# Can't tell the path → fail open.
|
|
63
|
-
[
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
61
|
+
# Can't tell the path or patch target → fail open.
|
|
62
|
+
if [ "$tool" = "apply_patch" ]; then
|
|
63
|
+
source_patch="$(printf '%s' "$input" | ROOT="$root" node -e '
|
|
64
|
+
let s=""; process.stdin.on("data", d => s += d).on("end", () => {
|
|
65
|
+
try {
|
|
66
|
+
const root = process.env.ROOT || "";
|
|
67
|
+
const cmd = (JSON.parse(s).tool_input || {}).command || "";
|
|
68
|
+
const paths = [...cmd.matchAll(/^\*\*\* (?:(?:Add|Update|Delete) File|Move to): (.+)$/gm)].map(m => m[1]);
|
|
69
|
+
if (!paths.length) return;
|
|
70
|
+
const source = paths.some(p => {
|
|
71
|
+
const abs = p.startsWith("/") ? p : `${root}/${p}`;
|
|
72
|
+
return !abs.startsWith(`${root}/.devrites/`);
|
|
73
|
+
});
|
|
74
|
+
process.stdout.write(source ? "1" : "0");
|
|
75
|
+
} catch {}
|
|
76
|
+
});
|
|
77
|
+
' 2>/dev/null)"
|
|
78
|
+
[ "$source_patch" = "1" ] || exit 0
|
|
79
|
+
abs="apply_patch"
|
|
80
|
+
else
|
|
81
|
+
[ -n "$fpath" ] || exit 0
|
|
82
|
+
case "$fpath" in /*) abs="$fpath" ;; *) abs="$root/$fpath" ;; esac
|
|
83
|
+
# Bookkeeping under .devrites/ is the orchestrator's own legit target → allow.
|
|
84
|
+
case "$abs" in "$root/.devrites/"*) exit 0 ;; esac
|
|
85
|
+
fi
|
|
68
86
|
|
|
69
87
|
# Reaching here = the MAIN thread is about to edit a SOURCE file mid-build. That is the A1
|
|
70
88
|
# breach. Observe (default) logs and allows; enforce denies.
|
|
@@ -8,9 +8,18 @@ set -u
|
|
|
8
8
|
input="$(cat)"
|
|
9
9
|
case "$input" in *'"tool_name"'*) : ;; *) exit 0 ;; esac
|
|
10
10
|
command -v node >/dev/null 2>&1 || exit 0
|
|
11
|
-
parsed="$(printf '%s' "$input" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{try{const j=JSON.parse(s);const ti=j.tool_input||{};process.stdout.write(
|
|
11
|
+
parsed="$(printf '%s' "$input" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{try{const j=JSON.parse(s);const ti=j.tool_input||{};process.stdout.write([j.tool_name||"",ti.command||"",j.agent_type||""].join("\u0001"))}catch(e){}})' 2>/dev/null)"
|
|
12
12
|
[ -n "$parsed" ] || exit 0
|
|
13
|
-
|
|
13
|
+
IFS=$'\001' read -r tool cmd agent_type <<EOF
|
|
14
|
+
$parsed
|
|
15
|
+
EOF
|
|
16
|
+
if [ "${DEVRITES_REVIEWER_AGENT_REQUIRED:-0}" = "1" ]; then
|
|
17
|
+
case "$agent_type" in
|
|
18
|
+
devrites-slice-wright|"") exit 0 ;;
|
|
19
|
+
devrites-*) : ;;
|
|
20
|
+
*) exit 0 ;;
|
|
21
|
+
esac
|
|
22
|
+
fi
|
|
14
23
|
[ "$tool" = "Bash" ] || exit 0
|
|
15
24
|
[ -n "$cmd" ] || exit 0
|
|
16
25
|
|
|
@@ -16,20 +16,49 @@ tf="$d/touched-files.md"
|
|
|
16
16
|
[ -f "$tf" ] || exit 0
|
|
17
17
|
|
|
18
18
|
command -v node >/dev/null 2>&1 || exit 0
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
[
|
|
24
|
-
|
|
19
|
+
tool="$(printf '%s' "$input" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{try{process.stdout.write(JSON.parse(s).tool_name||"")}catch(e){}})' 2>/dev/null)"
|
|
20
|
+
fpath="$(printf '%s' "$input" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{try{const ti=JSON.parse(s).tool_input||{};process.stdout.write(ti.file_path||ti.path||"")}catch(e){}})' 2>/dev/null)"
|
|
21
|
+
agent_type="$(printf '%s' "$input" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{try{process.stdout.write(JSON.parse(s).agent_type||"")}catch(e){}})' 2>/dev/null)"
|
|
22
|
+
[ -n "$tool" ] || exit 0
|
|
23
|
+
if [ "${DEVRITES_WRIGHT_AGENT_REQUIRED:-0}" = "1" ] && [ "$agent_type" != "devrites-slice-wright" ]; then
|
|
24
|
+
exit 0
|
|
25
|
+
fi
|
|
26
|
+
case "$tool" in Edit|Write|MultiEdit|NotebookEdit|apply_patch) : ;; *) exit 0 ;; esac
|
|
25
27
|
|
|
26
|
-
|
|
27
|
-
if
|
|
28
|
+
bad_paths=""
|
|
29
|
+
if [ "$tool" = "apply_patch" ]; then
|
|
30
|
+
bad_paths="$(printf '%s' "$input" | TF="$tf" ROOT="$root" node -e '
|
|
31
|
+
const fs = require("fs");
|
|
32
|
+
let s = ""; process.stdin.on("data", d => s += d).on("end", () => {
|
|
33
|
+
try {
|
|
34
|
+
const root = process.env.ROOT || "";
|
|
35
|
+
const tf = process.env.TF || "";
|
|
36
|
+
const touched = fs.existsSync(tf) ? fs.readFileSync(tf, "utf8") : "";
|
|
37
|
+
const cmd = (JSON.parse(s).tool_input || {}).command || "";
|
|
38
|
+
const paths = [...cmd.matchAll(/^\*\*\* (?:(?:Add|Update|Delete) File|Move to): (.+)$/gm)].map(m => m[1]);
|
|
39
|
+
const bad = paths.filter(p => {
|
|
40
|
+
if (p.startsWith(".devrites/") || p.includes("/.devrites/")) return false;
|
|
41
|
+
const rel = p.startsWith(`${root}/`) ? p.slice(root.length + 1) : p;
|
|
42
|
+
return !touched.includes(rel) && !touched.includes(p);
|
|
43
|
+
});
|
|
44
|
+
process.stdout.write(bad.join(", "));
|
|
45
|
+
} catch {}
|
|
46
|
+
});
|
|
47
|
+
' 2>/dev/null)"
|
|
48
|
+
else
|
|
49
|
+
[ -n "$fpath" ] || exit 0
|
|
50
|
+
case "$fpath" in *.devrites/*|.devrites/*) exit 0 ;; esac
|
|
51
|
+
rel="${fpath#"$root"/}"
|
|
52
|
+
if ! grep -qF "$rel" "$tf" 2>/dev/null && ! grep -qF "$fpath" "$tf" 2>/dev/null; then
|
|
53
|
+
bad_paths="$fpath"
|
|
54
|
+
fi
|
|
55
|
+
fi
|
|
56
|
+
[ -n "$bad_paths" ] || exit 0
|
|
28
57
|
|
|
29
58
|
mode="${DEVRITES_WRIGHT_SCOPE:-observe}"
|
|
30
59
|
if [ "$mode" = "enforce" ]; then
|
|
31
60
|
printf '%s\n' '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"DevRites scope: this path is not in touched-files.md. Build only the slice contract; if the slice genuinely needs this file, return an Escalation so the orchestrator routes it through the Spec Drift Guard — do not widen scope yourself. (devrites-wright-scope)"}}'
|
|
32
61
|
exit 0
|
|
33
62
|
fi
|
|
34
|
-
printf '%s\tWOULD-BLOCK\t%s\n' "$(date '+%Y-%m-%dT%H:%M:%S' 2>/dev/null || echo '?')" "$
|
|
63
|
+
printf '%s\tWOULD-BLOCK\t%s\n' "$(date '+%Y-%m-%dT%H:%M:%S' 2>/dev/null || echo '?')" "$bad_paths" >> "$d/.wright-scope.log" 2>/dev/null || true
|
|
35
64
|
exit 0
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
#
|
|
4
4
|
# Checks install integrity + the active .devrites/ workspace for the inconsistencies that
|
|
5
5
|
# silently waste a session: a stale ACTIVE pointer, a corrupt workspace, an orphaned gate,
|
|
6
|
-
# or broken hook wiring. Two surfaces wrap this one core:
|
|
6
|
+
# or broken hook wiring across Claude Code and Codex. Two surfaces wrap this one core:
|
|
7
7
|
# - the SessionStart orient hook calls it and surfaces issues only when there are any
|
|
8
8
|
# (silent-when-healthy);
|
|
9
9
|
# - the /rite-doctor skill calls it with --verbose for a full report on demand.
|
|
@@ -52,16 +52,80 @@ else
|
|
|
52
52
|
ok "devrites-lib core scripts present"
|
|
53
53
|
fi
|
|
54
54
|
|
|
55
|
-
# 2. Broken hook wiring — every devrites hook referenced in settings.json must exist on disk.
|
|
55
|
+
# 2. Broken Claude hook wiring — every devrites hook referenced in settings.json must exist on disk.
|
|
56
56
|
SET="$ROOT/.claude/settings.json"
|
|
57
57
|
if [ -f "$SET" ]; then
|
|
58
58
|
for h in $(grep -oE 'devrites-[a-z-]+\.sh' "$SET" 2>/dev/null | sort -u); do
|
|
59
59
|
[ -f "$ROOT/.claude/hooks/$h" ] || issue "settings.json wires missing hook: .claude/hooks/$h — reinstall or remove the hook entry"
|
|
60
60
|
done
|
|
61
|
-
ok "hook wiring checked"
|
|
61
|
+
ok "Claude hook wiring checked"
|
|
62
62
|
fi
|
|
63
63
|
|
|
64
|
-
# 3.
|
|
64
|
+
# 3. Codex support is optional. Only diagnose the Codex surface when DevRites
|
|
65
|
+
# manages it, so unrelated project AGENTS.md/.codex files and --no-codex installs
|
|
66
|
+
# do not produce false failures.
|
|
67
|
+
MF="$ROOT/.claude/devrites.manifest"
|
|
68
|
+
DR_FLAGS=""
|
|
69
|
+
[ -f "$MF" ] && DR_FLAGS="$(sed -n 's/^# devrites-flags:[[:space:]]*//p' "$MF" 2>/dev/null | head -n1)"
|
|
70
|
+
has_flag() {
|
|
71
|
+
case " $DR_FLAGS " in *" $1 "*) return 0 ;; *) return 1 ;; esac
|
|
72
|
+
}
|
|
73
|
+
manifest_has() {
|
|
74
|
+
[ -f "$MF" ] && grep -Eq -- "$1" "$MF" 2>/dev/null
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
codex_managed=0
|
|
78
|
+
if manifest_has '^(\.agents/|\.codex/|AGENTS\.md$|\.claude/devrites\.(agents|codex-config)-merge$)'; then
|
|
79
|
+
codex_managed=1
|
|
80
|
+
fi
|
|
81
|
+
has_flag --no-codex && codex_managed=0
|
|
82
|
+
|
|
83
|
+
if [ "$codex_managed" -eq 1 ]; then
|
|
84
|
+
if ! has_flag --no-skills && manifest_has '^\.agents/skills/'; then
|
|
85
|
+
[ -f "$ROOT/.agents/skills/rite/SKILL.md" ] \
|
|
86
|
+
|| issue "Codex skill mirror incomplete — missing .agents/skills/rite/SKILL.md — reinstall DevRites"
|
|
87
|
+
fi
|
|
88
|
+
if ! has_flag --no-rules && manifest_has '^\.agents/devrites/rules/'; then
|
|
89
|
+
[ -f "$ROOT/.agents/devrites/rules/core.md" ] \
|
|
90
|
+
|| issue "Codex rules mirror incomplete — missing .agents/devrites/rules/core.md — reinstall DevRites"
|
|
91
|
+
fi
|
|
92
|
+
if ! has_flag --no-agents && manifest_has '^\.codex/agents/'; then
|
|
93
|
+
[ -f "$ROOT/.codex/agents/devrites-code-reviewer.toml" ] \
|
|
94
|
+
|| issue "Codex custom agents incomplete — missing .codex/agents/devrites-code-reviewer.toml — reinstall DevRites"
|
|
95
|
+
fi
|
|
96
|
+
if manifest_has '^\.codex/hooks\.json$|^\.codex/hooks/|^\.claude/devrites\.codex-hooks-merge$'; then
|
|
97
|
+
[ -f "$ROOT/.codex/hooks.json" ] \
|
|
98
|
+
|| issue "Codex hooks incomplete — missing .codex/hooks.json — reinstall DevRites or install with --no-codex"
|
|
99
|
+
if [ -f "$ROOT/.codex/hooks.json" ]; then
|
|
100
|
+
grep -q 'devrites-' "$ROOT/.codex/hooks.json" 2>/dev/null \
|
|
101
|
+
|| issue "Codex hooks incomplete — .codex/hooks.json does not reference DevRites hooks — reinstall DevRites"
|
|
102
|
+
for h in $(grep -oE 'devrites-[a-z-]+\.sh' "$ROOT/.codex/hooks.json" 2>/dev/null | sort -u); do
|
|
103
|
+
[ -f "$ROOT/.codex/hooks/$h" ] || issue "Codex hooks.json wires missing hook: .codex/hooks/$h — reinstall DevRites"
|
|
104
|
+
done
|
|
105
|
+
fi
|
|
106
|
+
fi
|
|
107
|
+
if manifest_has '^\.codex/mcp/|^\.codex/config\.toml$|^\.claude/devrites\.codex-config-merge$'; then
|
|
108
|
+
[ -f "$ROOT/.codex/mcp/devrites-mcp.mjs" ] \
|
|
109
|
+
|| issue "Codex MCP incomplete — missing .codex/mcp/devrites-mcp.mjs — reinstall DevRites or install with --no-codex"
|
|
110
|
+
if [ -f "$ROOT/.codex/config.toml" ]; then
|
|
111
|
+
grep -q 'mcp_servers.devrites' "$ROOT/.codex/config.toml" 2>/dev/null \
|
|
112
|
+
|| issue "Codex MCP config missing DevRites server — merge the DevRites block into .codex/config.toml or reinstall DevRites"
|
|
113
|
+
else
|
|
114
|
+
issue "Codex config incomplete — missing .codex/config.toml with DevRites MCP server — reinstall DevRites or install with --no-codex"
|
|
115
|
+
fi
|
|
116
|
+
fi
|
|
117
|
+
if manifest_has '^AGENTS\.md$|^\.claude/devrites\.agents-merge$'; then
|
|
118
|
+
if [ -f "$ROOT/AGENTS.md" ]; then
|
|
119
|
+
grep -q 'DevRites' "$ROOT/AGENTS.md" 2>/dev/null \
|
|
120
|
+
|| issue "AGENTS.md does not include the DevRites Codex bridge — reinstall with --force or merge the DevRites AGENTS.md guidance"
|
|
121
|
+
else
|
|
122
|
+
issue "Codex guidance incomplete — missing AGENTS.md bridge — reinstall DevRites"
|
|
123
|
+
fi
|
|
124
|
+
fi
|
|
125
|
+
ok "Codex support wiring checked"
|
|
126
|
+
fi
|
|
127
|
+
|
|
128
|
+
# 4. ACTIVE pointer — if set, it must name a real workspace.
|
|
65
129
|
slug="$(cat "$DR/ACTIVE" 2>/dev/null | tr -d '[:space:]')"
|
|
66
130
|
if [ -z "$slug" ]; then
|
|
67
131
|
ok "no active feature (ACTIVE empty)"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: rite-doctor
|
|
3
|
-
description: Diagnose DevRites install + workspace health on demand and print a full report — install integrity, a stale `.devrites/ACTIVE` pointer, a corrupt workspace, orphaned gates
|
|
3
|
+
description: Diagnose DevRites install + workspace health on demand and print a full report — install integrity, Codex/Claude hook wiring, Codex support mirrors, a stale `.devrites/ACTIVE` pointer, a corrupt workspace, and orphaned gates. Use when the user says "rite doctor", "check my DevRites install", or "why isn't the workflow picking up my feature". Not for debugging the user's own code (use `devrites-debug-recovery`) or for feature progress / next-action (use `/rite-status`).
|
|
4
4
|
argument-hint: ""
|
|
5
5
|
user-invocable: true
|
|
6
6
|
---
|
|
@@ -10,6 +10,7 @@ user-invocable: true
|
|
|
10
10
|
The on-demand deep report. The same checks run **silently at session start** (the orient
|
|
11
11
|
hook surfaces issues only when there are any); `/rite-doctor` runs them **verbosely** —
|
|
12
12
|
printing every check, pass or fail — so you can inspect health even when nothing is broken.
|
|
13
|
+
It covers both Claude Code wiring and optional Codex mirrors/hooks when those files are present.
|
|
13
14
|
|
|
14
15
|
It is **read-only**: it never edits the workspace, never advances a phase, never blocks.
|
|
15
16
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devrites",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "DevRites — disciplined senior-engineer workflow skills pack for Claude Code",
|
|
3
|
+
"version": "2.6.0",
|
|
4
|
+
"description": "DevRites — disciplined senior-engineer workflow skills pack for Claude Code and Codex",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"homepage": "https://github.com/ViktorsBaikers/DevRites#readme",
|
|
7
7
|
"repository": {
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
},
|
|
14
14
|
"keywords": [
|
|
15
15
|
"claude-code",
|
|
16
|
+
"codex",
|
|
16
17
|
"skills",
|
|
17
18
|
"agent-skills",
|
|
18
19
|
"workflow",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# check-no-global-writes.sh — static guard that the installer/uninstaller never
|
|
3
|
-
# writes to the user's global ~/.claude, and that the global guard is present.
|
|
3
|
+
# writes to the user's global ~/.claude or ~/.codex, and that the global guard is present.
|
|
4
4
|
# Exits non-zero on violation.
|
|
5
5
|
|
|
6
6
|
set -u
|
|
@@ -18,12 +18,12 @@ else
|
|
|
18
18
|
echo "ok: install.sh contains the no-global guard"
|
|
19
19
|
fi
|
|
20
20
|
|
|
21
|
-
# 2) no write operation may target ~/.claude or
|
|
21
|
+
# 2) no write operation may target ~/.claude, ~/.codex, or their $HOME forms.
|
|
22
22
|
# Write verbs are actual file-writing commands / redirections. ('install' is
|
|
23
23
|
# intentionally excluded: the installer uses cp/mkdir, and "install" appears in
|
|
24
24
|
# prose like "install into a project", which is not a write.)
|
|
25
25
|
WRITE_RE='(\bmkdir\b|\bcp\b|\btee\b|\brsync\b|\bmv\b|\bln\b|>>?)'
|
|
26
|
-
GLOBAL_RE='(\$HOME/\.claude|~/\.claude)'
|
|
26
|
+
GLOBAL_RE='(\$HOME/\.(claude|codex)|~/\.(claude|codex))'
|
|
27
27
|
for f in $SCRIPTS; do
|
|
28
28
|
[ -f "$f" ] || continue
|
|
29
29
|
# lines that contain a global-claude path AND a write verb, excluding comments
|
|
@@ -34,7 +34,7 @@ for f in $SCRIPTS; do
|
|
|
34
34
|
fail=1
|
|
35
35
|
fi
|
|
36
36
|
done
|
|
37
|
-
[ "$fail" -eq 0 ] && echo "ok: no write operations target
|
|
37
|
+
[ "$fail" -eq 0 ] && echo "ok: no write operations target global agent homes"
|
|
38
38
|
|
|
39
39
|
if [ "$fail" -eq 0 ]; then
|
|
40
40
|
echo "PASS: no global-write risks detected"
|
package/scripts/install-lib.sh
CHANGED
|
@@ -48,7 +48,7 @@ dr_manifest_contains() {
|
|
|
48
48
|
# ---- misc ----------------------------------------------------------------
|
|
49
49
|
dr_lc() { printf '%s' "$1" | tr '[:upper:]' '[:lower:]'; }
|
|
50
50
|
|
|
51
|
-
# Is $1 (canonical) equal to, or inside,
|
|
51
|
+
# Is $1 (canonical) equal to, or inside, a global agent home?
|
|
52
52
|
# Used to refuse global writes. No write verbs here — read-only comparison.
|
|
53
53
|
dr_is_global_claude() {
|
|
54
54
|
_t="$1"
|
|
@@ -60,6 +60,16 @@ dr_is_global_claude() {
|
|
|
60
60
|
return 1
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
dr_is_global_codex() {
|
|
64
|
+
_t="$1"
|
|
65
|
+
_home_codex="$HOME/.codex"
|
|
66
|
+
[ "$_t" = "$_home_codex" ] && return 0
|
|
67
|
+
case "$_t/" in
|
|
68
|
+
"$_home_codex"/*) return 0 ;;
|
|
69
|
+
esac
|
|
70
|
+
return 1
|
|
71
|
+
}
|
|
72
|
+
|
|
63
73
|
# ---- alias wrapper template ----------------------------------------------
|
|
64
74
|
# Generate a thin SKILL.md that delegates to a DevRites skill. Used by:
|
|
65
75
|
# - install.sh (--short-aliases=all → /define, /build, /prove, /seal)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
|
|
4
|
+
const [, , mode, ...args] = process.argv;
|
|
5
|
+
let existingPath;
|
|
6
|
+
let devritesPath;
|
|
7
|
+
let outPath;
|
|
8
|
+
|
|
9
|
+
if (mode === 'merge') {
|
|
10
|
+
[existingPath, devritesPath, outPath] = args;
|
|
11
|
+
} else if (mode === 'strip') {
|
|
12
|
+
[existingPath, outPath] = args;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (!['merge', 'strip'].includes(mode) || !existingPath || !outPath || (mode === 'merge' && !devritesPath)) {
|
|
16
|
+
console.error('usage: merge-codex-hooks.mjs merge EXISTING DEVRITES OUT');
|
|
17
|
+
console.error(' or: merge-codex-hooks.mjs strip EXISTING OUT');
|
|
18
|
+
process.exit(2);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function readJson(path) {
|
|
22
|
+
return JSON.parse(fs.readFileSync(path, 'utf8'));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isDevRitesHook(value) {
|
|
26
|
+
const s = JSON.stringify(value);
|
|
27
|
+
return s.includes('.codex/hooks/devrites-') || s.includes('DevRites:') || s.includes('DEVRITES_');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function stripDevRitesHooks(config) {
|
|
31
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) return {};
|
|
32
|
+
const next = { ...config };
|
|
33
|
+
if (typeof next.$comment === 'string' && next.$comment.includes('DevRites hooks for Codex')) {
|
|
34
|
+
delete next.$comment;
|
|
35
|
+
}
|
|
36
|
+
const hooks = next.hooks && typeof next.hooks === 'object' && !Array.isArray(next.hooks)
|
|
37
|
+
? { ...next.hooks }
|
|
38
|
+
: {};
|
|
39
|
+
|
|
40
|
+
for (const [event, entries] of Object.entries(hooks)) {
|
|
41
|
+
const kept = Array.isArray(entries) ? entries.filter((entry) => !isDevRitesHook(entry)) : entries;
|
|
42
|
+
if (Array.isArray(kept) && kept.length === 0) {
|
|
43
|
+
delete hooks[event];
|
|
44
|
+
} else {
|
|
45
|
+
hooks[event] = kept;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (Object.keys(hooks).length) {
|
|
50
|
+
next.hooks = hooks;
|
|
51
|
+
} else {
|
|
52
|
+
delete next.hooks;
|
|
53
|
+
}
|
|
54
|
+
return next;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const existing = stripDevRitesHooks(readJson(existingPath));
|
|
58
|
+
|
|
59
|
+
if (mode === 'merge') {
|
|
60
|
+
const devrites = readJson(devritesPath);
|
|
61
|
+
const hooks = existing.hooks && typeof existing.hooks === 'object' && !Array.isArray(existing.hooks)
|
|
62
|
+
? existing.hooks
|
|
63
|
+
: {};
|
|
64
|
+
const devritesHooks = devrites.hooks && typeof devrites.hooks === 'object' && !Array.isArray(devrites.hooks)
|
|
65
|
+
? devrites.hooks
|
|
66
|
+
: {};
|
|
67
|
+
|
|
68
|
+
for (const [event, entries] of Object.entries(devritesHooks)) {
|
|
69
|
+
hooks[event] = [
|
|
70
|
+
...(Array.isArray(hooks[event]) ? hooks[event] : []),
|
|
71
|
+
...(Array.isArray(entries) ? entries : []),
|
|
72
|
+
];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
existing.hooks = hooks;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
fs.writeFileSync(outPath, `${JSON.stringify(existing, null, 2)}\n`);
|
package/scripts/pin.sh
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
#
|
|
4
4
|
# Adopts the same "thin wrapper SKILL.md that delegates" pattern as install.sh
|
|
5
5
|
# uses for --short-aliases=all, but exposes it as a runtime verb so users can
|
|
6
|
-
# add / remove arbitrary aliases against any rite-*
|
|
6
|
+
# add / remove arbitrary aliases against any rite-* skill without re-running
|
|
7
7
|
# the installer.
|
|
8
8
|
#
|
|
9
9
|
# Usage:
|
|
@@ -18,11 +18,12 @@
|
|
|
18
18
|
# ./scripts/pin.sh remove b
|
|
19
19
|
#
|
|
20
20
|
# Targets:
|
|
21
|
-
# - Default: $PWD (the installed project, which holds .claude/skills/
|
|
21
|
+
# - Default: $PWD (the installed project, which holds .claude/skills/
|
|
22
|
+
# and may also hold .agents/skills/ for Codex).
|
|
22
23
|
# - --target <dir> to operate on a project elsewhere.
|
|
23
24
|
#
|
|
24
25
|
# Safety:
|
|
25
|
-
# - Refuses to write inside
|
|
26
|
+
# - Refuses to write inside global Claude/Codex homes (DevRites is project-local only).
|
|
26
27
|
# - Refuses to overwrite a non-alias skill at .claude/skills/<alias>/.
|
|
27
28
|
# - Refuses if <target> is not a known rite-* skill in the target's pack.
|
|
28
29
|
# - Manifest-managed: pinned wrappers are recorded in .claude/devrites.manifest
|
|
@@ -66,8 +67,10 @@ done
|
|
|
66
67
|
|
|
67
68
|
TARGET="$(dr_abspath_dir "$TARGET")" || dr_die "target dir not found: $TARGET"
|
|
68
69
|
dr_is_global_claude "$TARGET" && dr_die "refusing to operate on ~/.claude — DevRites is project-local only"
|
|
70
|
+
dr_is_global_codex "$TARGET" && dr_die "refusing to operate on ~/.codex — DevRites is project-local only"
|
|
69
71
|
|
|
70
72
|
SKILLS_DIR="$TARGET/.claude/skills"
|
|
73
|
+
CODEX_SKILLS_DIR="$TARGET/.agents/skills"
|
|
71
74
|
MF="$TARGET/$DR_MANIFEST_NAME"
|
|
72
75
|
|
|
73
76
|
[ -d "$SKILLS_DIR" ] || dr_die "no .claude/skills at $TARGET — run install.sh first?"
|
|
@@ -97,6 +100,11 @@ is_pinned_alias() {
|
|
|
97
100
|
grep -q 'description: Alias of DevRites /' "$SKILLS_DIR/$1/SKILL.md"
|
|
98
101
|
}
|
|
99
102
|
|
|
103
|
+
is_pinned_alias_file() {
|
|
104
|
+
[ -f "$1" ] || return 1
|
|
105
|
+
grep -q 'description: Alias of DevRites /' "$1"
|
|
106
|
+
}
|
|
107
|
+
|
|
100
108
|
# ---- subcommands ---------------------------------------------------------
|
|
101
109
|
do_add() {
|
|
102
110
|
valid_alias_name "$ALIAS" || dr_die "invalid alias name '$ALIAS' (lowercase / digits / hyphens; not 'rite' or 'rite-*')"
|
|
@@ -106,6 +114,9 @@ do_add() {
|
|
|
106
114
|
ALIAS_DIR="$SKILLS_DIR/$ALIAS"
|
|
107
115
|
ALIAS_FILE="$ALIAS_DIR/SKILL.md"
|
|
108
116
|
ALIAS_REL=".claude/skills/$ALIAS/SKILL.md"
|
|
117
|
+
CODEX_ALIAS_DIR="$CODEX_SKILLS_DIR/$ALIAS"
|
|
118
|
+
CODEX_ALIAS_FILE="$CODEX_ALIAS_DIR/SKILL.md"
|
|
119
|
+
CODEX_ALIAS_REL=".agents/skills/$ALIAS/SKILL.md"
|
|
109
120
|
|
|
110
121
|
if [ -e "$ALIAS_FILE" ]; then
|
|
111
122
|
if is_pinned_alias "$ALIAS"; then
|
|
@@ -114,15 +125,33 @@ do_add() {
|
|
|
114
125
|
dr_die "$ALIAS_FILE exists and is NOT a pinned alias — refusing to overwrite"
|
|
115
126
|
fi
|
|
116
127
|
fi
|
|
128
|
+
if [ -d "$CODEX_SKILLS_DIR" ] && [ -e "$CODEX_ALIAS_FILE" ]; then
|
|
129
|
+
if is_pinned_alias_file "$CODEX_ALIAS_FILE"; then
|
|
130
|
+
dr_warn "already pinned for Codex: /$ALIAS — overwriting"
|
|
131
|
+
else
|
|
132
|
+
dr_die "$CODEX_ALIAS_FILE exists and is NOT a pinned alias — refusing to overwrite"
|
|
133
|
+
fi
|
|
134
|
+
fi
|
|
117
135
|
|
|
118
136
|
mkdir -p "$ALIAS_DIR"
|
|
119
137
|
dr_gen_alias_wrapper "$ALIAS" "$DEST" "$ALIAS_FILE"
|
|
138
|
+
if [ -d "$CODEX_SKILLS_DIR" ]; then
|
|
139
|
+
mkdir -p "$CODEX_ALIAS_DIR"
|
|
140
|
+
dr_gen_alias_wrapper "$ALIAS" "$DEST" "$CODEX_ALIAS_FILE"
|
|
141
|
+
fi
|
|
120
142
|
|
|
121
143
|
if ! dr_manifest_contains "$MF" "$ALIAS_REL"; then
|
|
122
144
|
printf '%s\n' "$ALIAS_REL" >> "$MF"
|
|
123
145
|
fi
|
|
146
|
+
if [ -d "$CODEX_SKILLS_DIR" ] && ! dr_manifest_contains "$MF" "$CODEX_ALIAS_REL"; then
|
|
147
|
+
printf '%s\n' "$CODEX_ALIAS_REL" >> "$MF"
|
|
148
|
+
fi
|
|
124
149
|
|
|
125
|
-
|
|
150
|
+
if [ -d "$CODEX_SKILLS_DIR" ]; then
|
|
151
|
+
dr_ok "pinned: /$ALIAS → /$DEST ($ALIAS_FILE, $CODEX_ALIAS_FILE)"
|
|
152
|
+
else
|
|
153
|
+
dr_ok "pinned: /$ALIAS → /$DEST ($ALIAS_FILE)"
|
|
154
|
+
fi
|
|
126
155
|
}
|
|
127
156
|
|
|
128
157
|
do_remove() {
|
|
@@ -130,16 +159,24 @@ do_remove() {
|
|
|
130
159
|
ALIAS_DIR="$SKILLS_DIR/$ALIAS"
|
|
131
160
|
ALIAS_FILE="$ALIAS_DIR/SKILL.md"
|
|
132
161
|
ALIAS_REL=".claude/skills/$ALIAS/SKILL.md"
|
|
162
|
+
CODEX_ALIAS_DIR="$CODEX_SKILLS_DIR/$ALIAS"
|
|
163
|
+
CODEX_ALIAS_FILE="$CODEX_ALIAS_DIR/SKILL.md"
|
|
164
|
+
CODEX_ALIAS_REL=".agents/skills/$ALIAS/SKILL.md"
|
|
133
165
|
|
|
134
166
|
[ -f "$ALIAS_FILE" ] || dr_die "no pinned alias at $ALIAS_FILE"
|
|
135
167
|
is_pinned_alias "$ALIAS" || dr_die "$ALIAS_FILE exists but is not a pinned alias — refusing to remove"
|
|
136
168
|
|
|
137
169
|
rm -f "$ALIAS_FILE"
|
|
138
170
|
rmdir "$ALIAS_DIR" 2>/dev/null || true
|
|
171
|
+
if [ -f "$CODEX_ALIAS_FILE" ]; then
|
|
172
|
+
is_pinned_alias_file "$CODEX_ALIAS_FILE" || dr_die "$CODEX_ALIAS_FILE exists but is not a pinned alias — refusing to remove"
|
|
173
|
+
rm -f "$CODEX_ALIAS_FILE"
|
|
174
|
+
rmdir "$CODEX_ALIAS_DIR" 2>/dev/null || true
|
|
175
|
+
fi
|
|
139
176
|
|
|
140
177
|
# Drop the alias line from the manifest (preserve header + the rest)
|
|
141
178
|
TMP="$(mktemp)"
|
|
142
|
-
grep -Fvx "$ALIAS_REL" "$MF" > "$TMP" && mv "$TMP" "$MF"
|
|
179
|
+
grep -Fvx "$ALIAS_REL" "$MF" | grep -Fvx "$CODEX_ALIAS_REL" > "$TMP" && mv "$TMP" "$MF"
|
|
143
180
|
|
|
144
181
|
dr_ok "unpinned: /$ALIAS"
|
|
145
182
|
}
|
package/uninstall.sh
CHANGED
|
@@ -86,10 +86,70 @@ PRUNE_TMP="$(mktemp 2>/dev/null || echo "${TMPDIR:-/tmp}/devrites-prune.$$")"
|
|
|
86
86
|
: > "$PRUNE_TMP"
|
|
87
87
|
trap 'rm -f "$PRUNE_TMP"' EXIT
|
|
88
88
|
|
|
89
|
+
# If DevRites was merged into a pre-existing AGENTS.md, remove only the managed
|
|
90
|
+
# marker block. Fresh AGENTS.md files created by DevRites are still removed
|
|
91
|
+
# below through the ordinary manifest entry.
|
|
92
|
+
if [ -f "$TARGET/.claude/devrites.agents-merge" ] && [ -f "$TARGET/AGENTS.md" ]; then
|
|
93
|
+
if [ "$DRYRUN" -eq 1 ]; then
|
|
94
|
+
dr_say " [merge-remove] AGENTS.md DevRites block"
|
|
95
|
+
else
|
|
96
|
+
tmp_agents="$(mktemp 2>/dev/null || echo "${TMPDIR:-/tmp}/devrites-agents.$$")"
|
|
97
|
+
awk '
|
|
98
|
+
$0 == "<!-- BEGIN DEVRITES CODEX -->" { in_block = 1; next }
|
|
99
|
+
$0 == "<!-- END DEVRITES CODEX -->" { in_block = 0; next }
|
|
100
|
+
in_block { next }
|
|
101
|
+
{ print }
|
|
102
|
+
' "$TARGET/AGENTS.md" > "$tmp_agents" && cp "$tmp_agents" "$TARGET/AGENTS.md"
|
|
103
|
+
rm -f "$tmp_agents"
|
|
104
|
+
grep -q '[^[:space:]]' "$TARGET/AGENTS.md" 2>/dev/null || rm -f "$TARGET/AGENTS.md"
|
|
105
|
+
fi
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
if [ -f "$TARGET/.claude/devrites.codex-config-merge" ] && [ -f "$TARGET/.codex/config.toml" ]; then
|
|
109
|
+
if [ "$DRYRUN" -eq 1 ]; then
|
|
110
|
+
dr_say " [merge-remove] .codex/config.toml DevRites MCP block"
|
|
111
|
+
else
|
|
112
|
+
tmp_codex_config="$(mktemp 2>/dev/null || echo "${TMPDIR:-/tmp}/devrites-codex-config.$$")"
|
|
113
|
+
awk '
|
|
114
|
+
$0 == "# BEGIN DEVRITES CODEX MCP" { in_block = 1; next }
|
|
115
|
+
$0 == "# END DEVRITES CODEX MCP" { in_block = 0; next }
|
|
116
|
+
in_block { next }
|
|
117
|
+
{ print }
|
|
118
|
+
' "$TARGET/.codex/config.toml" > "$tmp_codex_config" && cp "$tmp_codex_config" "$TARGET/.codex/config.toml"
|
|
119
|
+
rm -f "$tmp_codex_config"
|
|
120
|
+
grep -q '[^[:space:]]' "$TARGET/.codex/config.toml" 2>/dev/null || rm -f "$TARGET/.codex/config.toml"
|
|
121
|
+
fi
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
if [ -f "$TARGET/.claude/devrites.codex-hooks-merge" ] && [ -f "$TARGET/.codex/hooks.json" ]; then
|
|
125
|
+
if [ "$DRYRUN" -eq 1 ]; then
|
|
126
|
+
dr_say " [merge-remove] .codex/hooks.json DevRites hooks"
|
|
127
|
+
else
|
|
128
|
+
if grep -q 'DevRites hooks for Codex' "$TARGET/.codex/hooks.json" 2>/dev/null \
|
|
129
|
+
&& ! grep '"command"[[:space:]]*:' "$TARGET/.codex/hooks.json" 2>/dev/null | grep -vq 'devrites-\|DEVRITES_\|DevRites:'; then
|
|
130
|
+
rm -f "$TARGET/.codex/hooks.json" || dr_die "cannot remove .codex/hooks.json"
|
|
131
|
+
else
|
|
132
|
+
command -v node >/dev/null 2>&1 || dr_die "node is required to remove DevRites hooks from .codex/hooks.json"
|
|
133
|
+
tmp_codex_hooks="$(mktemp 2>/dev/null || echo "${TMPDIR:-/tmp}/devrites-codex-hooks.$$")"
|
|
134
|
+
node "$SELF_DIR/scripts/merge-codex-hooks.mjs" strip "$TARGET/.codex/hooks.json" "$tmp_codex_hooks" \
|
|
135
|
+
|| dr_die "cannot remove DevRites hooks from .codex/hooks.json"
|
|
136
|
+
cp "$tmp_codex_hooks" "$TARGET/.codex/hooks.json" || dr_die "cannot write .codex/hooks.json"
|
|
137
|
+
rm -f "$tmp_codex_hooks"
|
|
138
|
+
[ "$(tr -d '[:space:]' < "$TARGET/.codex/hooks.json" 2>/dev/null)" != "{}" ] || rm -f "$TARGET/.codex/hooks.json"
|
|
139
|
+
fi
|
|
140
|
+
[ -e "$TARGET/.codex/hooks.json" ] || printf '%s\n' "$TARGET/.codex" >> "$PRUNE_TMP"
|
|
141
|
+
fi
|
|
142
|
+
fi
|
|
143
|
+
|
|
89
144
|
# Remove each manifest-listed file.
|
|
90
145
|
while IFS= read -r rel; do
|
|
91
146
|
case "$rel" in ''|\#*) continue ;; esac # skip blanks/comments
|
|
92
147
|
case "$rel" in /*|*..*) dr_warn "ignoring suspicious manifest entry: $rel"; continue ;; esac
|
|
148
|
+
case "$rel" in
|
|
149
|
+
AGENTS.md) [ -f "$TARGET/.claude/devrites.agents-merge" ] && continue ;;
|
|
150
|
+
.codex/config.toml) [ -f "$TARGET/.claude/devrites.codex-config-merge" ] && continue ;;
|
|
151
|
+
.codex/hooks.json) [ -f "$TARGET/.claude/devrites.codex-hooks-merge" ] && continue ;;
|
|
152
|
+
esac
|
|
93
153
|
dest="$TARGET/$rel"
|
|
94
154
|
if [ -e "$dest" ] || [ -L "$dest" ]; then
|
|
95
155
|
if [ "$DRYRUN" -eq 1 ]; then
|