devrites 2.5.1 → 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.
@@ -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 Claude Code build populates agent_id for subagents (older builds may not;
15
- # if the log flags wright edits, agent_id is unavailable here — stay in observe).
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
- 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||{};const fp=ti.file_path||ti.path||"";process.stdout.write([j.tool_name||"",fp,j.agent_id||""].join("\t"))}catch(e){}})' 2>/dev/null)"
51
- [ -n "$parsed" ] || exit 0
52
- IFS=$'\t' read -r tool fpath agent <<EOF
53
- $parsed
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. agent_id is present only inside a subagent.
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
- [ -n "$fpath" ] || exit 0
64
- case "$fpath" in /*) abs="$fpath" ;; *) abs="$root/$fpath" ;; esac
65
-
66
- # Bookkeeping under .devrites/ is the orchestrator's own legit target → allow.
67
- case "$abs" in "$root/.devrites/"*) exit 0 ;; esac
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((j.tool_name||"")+""+(ti.command||""))}catch(e){}})' 2>/dev/null)"
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
- tool="${parsed%%$'\001'*}"; cmd="${parsed#*$'\001'}"
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
- 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.file_path||ti.path||""))}catch(e){}})' 2>/dev/null)"
20
- [ -n "$parsed" ] || exit 0
21
- tool="${parsed%%$'\001'*}"; fpath="${parsed#*$'\001'}"
22
- case "$tool" in Edit|Write|MultiEdit|NotebookEdit) : ;; *) exit 0 ;; esac
23
- [ -n "$fpath" ] || exit 0
24
- case "$fpath" in *.devrites/*) exit 0 ;; esac
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
- rel="${fpath#"$root"/}"
27
- if grep -qF "$rel" "$tf" 2>/dev/null || grep -qF "$fpath" "$tf" 2>/dev/null; then exit 0; fi
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 '?')" "$fpath" >> "$d/.wright-scope.log" 2>/dev/null || true
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
@@ -14,13 +14,17 @@ The same ladder captures a **developer-facing docs / getting-started page** for
14
14
  commands match what actually runs, and note the result in `browser-evidence.md` / `devex.md`.
15
15
 
16
16
  ## Ladder (top-down)
17
- 1. **browser-harness** — detect `command -v browser-harness`. Connects to the user's
18
- Chrome over CDP. Pattern: `new_tab(url)` `wait_for_load()` `capture_screenshot()`
19
- read the pixel → `click_at_xy(x,y)` re-screenshot. Coordinate clicks pass through
20
- iframes/shadow/cross-origin. `print(page_info())` for liveness. Don't launch a new
21
- browser; don't auto-install.
22
- 2. **Chrome DevTools MCP** (if configured) screenshots, DOM, console, network,
23
- performance, accessibility tree.
17
+ 1. **Playwright MCP** (preferred) — detect by tool availability (the `browser_*` tools are
18
+ present, e.g. `browser_navigate`); detect, don't install. Drives a Playwright-managed
19
+ browser. Pattern: `browser_navigate(url)` → `browser_snapshot()` (the accessibility tree
20
+ is the primary perception) → `browser_click` / `browser_type` on a **ref from the
21
+ snapshot** `browser_take_screenshot()`. Read `browser_console_messages()` and
22
+ `browser_network_requests()` for console/network evidence; `browser_resize(w,h)` for each
23
+ responsive viewport. Act on snapshot refs, not pixel coordinates.
24
+ 2. **Chrome DevTools MCP** (when configured — use **alongside** Playwright MCP for more
25
+ detail) — screenshots, DOM, console, network, performance trace, accessibility tree, and
26
+ `lighthouse_audit`. Playwright MCP drives the flow; DevTools MCP adds Lighthouse + the perf
27
+ trace Playwright can't produce.
24
28
  3. **Claude Code `/run` + `/verify`** (if available) — launch + observe the app.
25
29
  4. **Project-native E2E** (only if present) — Playwright/Cypress/Capybara/Selenium via
26
30
  the project's existing commands. Don't add a new framework.
@@ -31,11 +35,13 @@ If `spec.md` carries a perf budget — or a frontend regression risk is visible
31
35
  CWV numbers here so the perf reviewer judges real data instead of guessing. Use the highest
32
36
  rung available; **detect, don't install**.
33
37
 
34
- 1. **Chrome DevTools MCP** (if configured) — `lighthouse_audit` for LCP/INP/CLS + the
35
- Lighthouse performance score. Source label: **Lab (Lighthouse)**. A `performance_*` trace
36
- gives **Trace (DevTools)** attribution.
37
- 2. **browser-harness** — drive the route, capture a performance trace over CDP. Source
38
- label: **Trace (DevTools)**.
38
+ 1. **Chrome DevTools MCP** (preferred for CWV when configured) — `lighthouse_audit` for
39
+ LCP/INP/CLS + the Lighthouse performance score. Source label: **Lab (Lighthouse)**. A
40
+ `performance_*` trace gives **Trace (DevTools)** attribution.
41
+ 2. **Playwright MCP** — `browser_navigate` the route, then `browser_evaluate` the web-vitals
42
+ library to read LCP/INP/CLS off the live page. Source label: **Trace (DevTools)** (a
43
+ real-page number, not a Lighthouse score). Use **alongside** rung 1 when both are present —
44
+ the lab score and the trace corroborate each other.
39
45
  3. **CrUX / PageSpeed Insights** — only if the user supplied an API key. Field data, p75.
40
46
  Source label: **Field (CrUX)**.
41
47
  4. **None available** → mark CWV **pending (manual)** and name the exact command
@@ -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. ACTIVE pointer if set, it must name a real workspace.
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, and broken hook wiring. 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`).
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
 
@@ -91,7 +91,7 @@ If the project has the tools, run them; capture evidence in `evidence.md`.
91
91
  - `prefers-reduced-motion`: emulate in DevTools → re-verify motion.
92
92
  - Throttle to "Slow 3G" → re-verify loading + error states.
93
93
  - Lighthouse / axe / pa11y → re-verify accessibility floor.
94
- - Browser-harness or Chrome DevTools MCP (per `devrites-browser-proof`) → capture
94
+ - Playwright MCP or Chrome DevTools MCP (per `devrites-browser-proof`) → capture
95
95
  screenshots of the worst-case input states above.
96
96
 
97
97
  ## Reporting in `/rite-polish`
@@ -14,7 +14,7 @@ workspace first; if none, run `/rite-spec <feature>`.
14
14
  > single change; `/run` launches the app. `/rite-prove` is feature-scoped:
15
15
  > it walks `spec.md` acceptance criteria one-by-one, runs the full relevant
16
16
  > test suite + build/typecheck/lint, ascends the browser-proof ladder
17
- > (browser-harness → DevTools MCP → `/run`+`/verify` → project E2E →
17
+ > (Playwright MCP → DevTools MCP → `/run`+`/verify` → project E2E →
18
18
  > manual), and writes `evidence.md` + `browser-evidence.md` keyed to the
19
19
  > active `.devrites/work/<slug>/`. Use `/verify` or `/run` on their own
20
20
  > when there is no DevRites feature workspace.
@@ -5,7 +5,7 @@ What `browser-evidence.md` must capture for a UI slice. Delegates to
5
5
 
6
6
  ```markdown
7
7
  ## Slice <N> — <name> (<date>)
8
- - Tooling: browser-harness | devtools-mcp | /run+/verify | <e2e> | manual
8
+ - Tooling: playwright-mcp | devtools-mcp | /run+/verify | <e2e> | manual
9
9
  - Route(s): <url(s) exercised>
10
10
  - Viewports: 375 / 768 / 1280 (note which were checked)
11
11
  - Screenshots: <path(s)> — described: <what is actually visible>
@@ -3,12 +3,14 @@
3
3
  Prefer real runtime evidence. For UI/browser work, try tools top-down and record which
4
4
  rung you used. Detect, don't install — browser tooling setup is the user's call.
5
5
 
6
- 1. **browser-harness** (preferred when installed & connected)
7
- - Detect: `command -v browser-harness`. Use its doctor/connection check only when
8
- safe. Capabilities: navigation, screenshots, coordinate clicks, console logs, DOM
9
- reads, network, raw CDP. Connects to the user's Chrome — don't launch a new browser.
10
- 2. **Chrome DevTools MCP** (if configured) — screenshots, DOM, console, network,
11
- performance, accessibility tree.
6
+ 1. **Playwright MCP** (preferred when configured)
7
+ - Detect: the Playwright MCP `browser_*` tools are available (e.g. `browser_navigate`).
8
+ Capabilities: navigation, accessibility-tree snapshots, screenshots, ref-based
9
+ clicks/typing, console messages, network requests, `browser_evaluate`. Drives a
10
+ Playwright-managed browser; detect, don't install.
11
+ 2. **Chrome DevTools MCP** (when configured — pairs with Playwright MCP for Lighthouse, the
12
+ perf trace, and deeper DOM) — screenshots, DOM, console, network, performance,
13
+ accessibility tree.
12
14
  3. **Claude Code `/run` and `/verify`** (if available) — launch and observe the app;
13
15
  prefer a project-specific run/verify skill if one exists.
14
16
  4. **Project-native E2E** (only if already present) — Playwright, Cypress, Capybara,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "devrites",
3
- "version": "2.5.1",
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 \$HOME/.claude.
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 ~/.claude"
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"
@@ -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, the user's global ~/.claude?
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`);