clarus-dev-stack 1.0.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/README.md ADDED
@@ -0,0 +1,71 @@
1
+ # Clarus Dev Stack
2
+
3
+ Curated Claude Code skills and guardrail hooks for Salesforce consulting work.
4
+ **Curated and supported by Mike Carter, Clarus Group.**
5
+
6
+ ## Install (or update): one line
7
+
8
+ ```
9
+ npx clarus-dev-stack
10
+ ```
11
+
12
+ That installs/refreshes BOTH streams:
13
+
14
+ 1. **Vendor stream:** the official Salesforce skills library ([forcedotcom/sf-skills](https://github.com/forcedotcom/sf-skills), 69 skills, maintained by Salesforce)
15
+ 2. **Clarus stream:** house skills and enforcement hooks (below)
16
+
17
+ Re-running the same command is the update mechanism. It is idempotent and safe.
18
+
19
+ ## What the Clarus stream adds
20
+
21
+ ### Skills (knowledge that fires automatically)
22
+
23
+ | Skill | What it protects |
24
+ |---|---|
25
+ | `clarus-conventions` | House standards: deploy discipline, FLS pairing rule, Apex standards, "the org must never be ahead of git" |
26
+ | `clarus-test-discipline` | Never mutate production business logic to make a failing Apex test pass. Diagnose test-side first; production changes need explicit human approval |
27
+ | `clarus-org-guard` | Companion to the org-guard hook: explains blocks, builds verified org-lock files |
28
+ | `clarus-deploy-error-translator` | Translates cryptic Salesforce deploy errors into plain English + a fix |
29
+ | `clarus-permset-watcher` | Catches "deployed-but-invisible": new metadata missing FLS/permset grants |
30
+ | `clarus-datapack-guard` | OmniStudio DataPack import chunking; Dev Edition timeout-rollback protection |
31
+ | `clarus-kill-images` | Strips base64 images from conversation history to control context bloat |
32
+
33
+ ### Hooks (deterministic enforcement, not suggestions)
34
+
35
+ | Hook | Event | What it enforces |
36
+ |---|---|---|
37
+ | `clarus-org-guard` | Before every `sf` command | **Blocks** destructive commands unless the target org is allow-listed in the project's `.claude/org-lock.json` AND its real org ID (via `sf org display`) matches the lock. No lock file = no deploys. No `--target-org` = blocked |
38
+ | `clarus-git-sync` | After every deploy | The org must never be ahead of git: warns when a deploy lands with uncommitted/unpushed changes |
39
+ | `clarus-fls-audit` | After every deploy | Scans deployed metadata for missing canonical-permset grants |
40
+ | `clarus-commit-cadence` | End of each turn | Nudges for a checkpoint commit when uncommitted work outlives the cadence window (default 90 min, configurable) |
41
+ | `clarus-precompact-images` | Before compaction | Strips image payloads from the transcript |
42
+
43
+ ## The org-lock file (required for deploys)
44
+
45
+ Each project gets `<project root>/.claude/org-lock.json`:
46
+
47
+ ```json
48
+ {
49
+ "write": { "my-sandbox": "00Dxx0000000001" },
50
+ "readonly": { "my-prod": "00Dxx0000000002" },
51
+ "canonicalPermset": "My_Project_Admin",
52
+ "commitCadenceMinutes": 90
53
+ }
54
+ ```
55
+
56
+ Easiest path: open Claude Code in the project and say **"set up my org lock file"**. The clarus-org-guard skill runs the verified flow (real org IDs from `sf org display`, human confirmation before anything enters the `write` list).
57
+
58
+ ## Requirements
59
+
60
+ - Node 18+ (for `npx`)
61
+ - Claude Code
62
+ - `sf` CLI (for org verification)
63
+ - macOS/Linux (hooks use `python3`, present by default on macOS)
64
+
65
+ ## Architecture: two streams, zero merges
66
+
67
+ Vendor skills are never edited, so Salesforce updates always apply cleanly. Clarus skills are prefixed `clarus-` and live alongside them; at runtime Claude composes both (vendor = the textbook, Clarus = the house style and the tripwires). Per-project specifics stay in each repo's `.claude/org-lock.json` and `CLAUDE.md`.
68
+
69
+ ## Support
70
+
71
+ Questions, false positives, new tripwire candidates: Mike Carter.
package/bin/install.js ADDED
@@ -0,0 +1,152 @@
1
+ #!/usr/bin/env node
2
+ /* Clarus Dev Stack installer
3
+ * - Installs the official Salesforce skills library (forcedotcom/sf-skills)
4
+ * - Installs Clarus skills into ~/.claude/skills/
5
+ * - Installs Clarus hook scripts into ~/.claude/hooks/clarus/
6
+ * - Merges hook registrations into ~/.claude/settings.json (idempotent, backed up)
7
+ * Curated by Mike Carter, Clarus Group.
8
+ */
9
+ const fs = require("fs");
10
+ const os = require("os");
11
+ const path = require("path");
12
+ const { execSync, spawnSync } = require("child_process");
13
+
14
+ const PKG_ROOT = path.resolve(__dirname, "..");
15
+ const HOME = os.homedir();
16
+ const CLAUDE = path.join(HOME, ".claude");
17
+ const SKILLS_DST = path.join(CLAUDE, "skills");
18
+ const HOOKS_DST = path.join(CLAUDE, "hooks", "clarus");
19
+ const SETTINGS = path.join(CLAUDE, "settings.json");
20
+
21
+ const BANNER = `
22
+ =============================================
23
+ CLARUS DEV STACK v1.0.0
24
+ Curated by Mike Carter, Clarus Group
25
+ =============================================
26
+ `;
27
+
28
+ function log(s) { console.log(s); }
29
+
30
+ function copyDir(src, dst) {
31
+ fs.mkdirSync(dst, { recursive: true });
32
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
33
+ const s = path.join(src, entry.name);
34
+ const d = path.join(dst, entry.name);
35
+ if (entry.isDirectory()) copyDir(s, d);
36
+ else fs.copyFileSync(s, d);
37
+ }
38
+ }
39
+
40
+ function ensureHook(settings, event, matcher, command, ifFilter) {
41
+ settings.hooks = settings.hooks || {};
42
+ settings.hooks[event] = settings.hooks[event] || [];
43
+ // Dedupe: skip if any existing hook command in this event already references our script
44
+ const scriptName = path.basename(command.split(" ").pop());
45
+ for (const grp of settings.hooks[event]) {
46
+ for (const h of grp.hooks || []) {
47
+ if (h.command && h.command.includes(scriptName)) return false;
48
+ }
49
+ }
50
+ const hook = { type: "command", command, timeout: 60 };
51
+ if (ifFilter) hook.if = ifFilter;
52
+ const group = { hooks: [hook] };
53
+ if (matcher) group.matcher = matcher;
54
+ settings.hooks[event].push(group);
55
+ return true;
56
+ }
57
+
58
+ function main() {
59
+ log(BANNER);
60
+
61
+ // 0) Preflight
62
+ if (!fs.existsSync(CLAUDE)) fs.mkdirSync(CLAUDE, { recursive: true });
63
+ fs.mkdirSync(SKILLS_DST, { recursive: true });
64
+ fs.mkdirSync(HOOKS_DST, { recursive: true });
65
+
66
+ // 1) Official Salesforce skills (vendor stream)
67
+ log("[1/4] Installing official Salesforce skills (forcedotcom/sf-skills)...");
68
+ const res = spawnSync("npx", ["-y", "skills", "add", "forcedotcom/sf-skills",
69
+ "-g", "-y", "-s", "*", "-a", "claude-code"], { stdio: "inherit" });
70
+ if (res.status !== 0) {
71
+ log(" WARNING: official skills install did not complete cleanly. " +
72
+ "Re-run later with: npx skills add forcedotcom/sf-skills -g -y -s '*' -a claude-code");
73
+ }
74
+
75
+ // 2) Clarus skills (house stream)
76
+ log("[2/4] Installing Clarus skills...");
77
+ const skillsSrc = path.join(PKG_ROOT, "skills");
78
+ let count = 0;
79
+ for (const name of fs.readdirSync(skillsSrc)) {
80
+ copyDir(path.join(skillsSrc, name), path.join(SKILLS_DST, name));
81
+ count++;
82
+ log(" + " + name);
83
+ }
84
+
85
+ // 3) Clarus hook scripts
86
+ log("[3/4] Installing Clarus hook scripts...");
87
+ const hooksSrc = path.join(PKG_ROOT, "hooks");
88
+ for (const f of fs.readdirSync(hooksSrc)) {
89
+ const dst = path.join(HOOKS_DST, f);
90
+ fs.copyFileSync(path.join(hooksSrc, f), dst);
91
+ fs.chmodSync(dst, 0o755);
92
+ log(" + " + f);
93
+ }
94
+
95
+ // 4) Register hooks in settings.json (backup + idempotent merge)
96
+ log("[4/4] Registering hooks in ~/.claude/settings.json...");
97
+ let settings = {};
98
+ if (fs.existsSync(SETTINGS)) {
99
+ const raw = fs.readFileSync(SETTINGS, "utf8");
100
+ try {
101
+ settings = JSON.parse(raw);
102
+ } catch (e) {
103
+ log(" ERROR: settings.json is not valid JSON. Aborting hook registration " +
104
+ "(skills are installed). Fix the file and re-run.");
105
+ process.exit(1);
106
+ }
107
+ const backup = SETTINGS + ".bak." + new Date().toISOString().replace(/[:.]/g, "-");
108
+ fs.copyFileSync(SETTINGS, backup);
109
+ log(" backup: " + backup);
110
+ }
111
+
112
+ const H = (f) => `python3 "${path.join(HOOKS_DST, f)}"`;
113
+ const added = [];
114
+ // Org guard: deterministic wrong-org protection
115
+ if (ensureHook(settings, "PreToolUse", "Bash", H("clarus-org-guard.py"), "Bash(sf *)"))
116
+ added.push("PreToolUse: clarus-org-guard (blocks unverified org writes)");
117
+ // Git sync: org must never be ahead of git
118
+ if (ensureHook(settings, "PostToolUse", "Bash", H("clarus-git-sync.py"), "Bash(sf project deploy *)"))
119
+ added.push("PostToolUse: clarus-git-sync (commit after deploy)");
120
+ // FLS audit: deployed-but-invisible detection
121
+ if (ensureHook(settings, "PostToolUse", "Bash", H("clarus-fls-audit.py"), "Bash(sf project deploy *)"))
122
+ added.push("PostToolUse: clarus-fls-audit (permset gap warnings)");
123
+ // Commit cadence nag
124
+ if (ensureHook(settings, "Stop", null, H("clarus-commit-cadence.py")))
125
+ added.push("Stop: clarus-commit-cadence (checkpoint reminders)");
126
+ // PreCompact image strip — skip if an equivalent strip hook already exists
127
+ const hasStrip = JSON.stringify(settings.hooks?.PreCompact || []).includes("strip_images");
128
+ if (!hasStrip) {
129
+ if (ensureHook(settings, "PreCompact", null, H("clarus-precompact-images.py")))
130
+ added.push("PreCompact: clarus-precompact-images (context bloat control)");
131
+ } else {
132
+ log(" ~ PreCompact image strip already present; skipped duplicate");
133
+ }
134
+
135
+ fs.writeFileSync(SETTINGS, JSON.stringify(settings, null, 2) + "\n");
136
+ added.forEach((a) => log(" + " + a));
137
+
138
+ log(`
139
+ =============================================
140
+ DONE. ${count} Clarus skills + ${added.length} hooks installed,
141
+ plus the official Salesforce skills library.
142
+
143
+ Verify: open Claude Code and type /skills
144
+ Note: org deploys now require a per-project
145
+ .claude/org-lock.json (ask Claude:
146
+ "set up my org lock file")
147
+ Update: re-run npx clarus-dev-stack
148
+ =============================================
149
+ `);
150
+ }
151
+
152
+ main();
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env python3
2
+ """Clarus commit-cadence: Stop hook.
3
+
4
+ At the end of each turn, if the project repo has uncommitted changes AND the
5
+ last commit is older than the cadence window, nudge for a checkpoint commit.
6
+ Cadence is configurable via commitCadenceMinutes in .claude/org-lock.json
7
+ (default 90). Never blocks; reminder only.
8
+ """
9
+ import json
10
+ import os
11
+ import subprocess
12
+ import sys
13
+ import time
14
+
15
+ DEFAULT_CADENCE_MIN = 90
16
+
17
+
18
+ def find_lock(start):
19
+ d = os.path.abspath(start)
20
+ home = os.path.expanduser("~")
21
+ while True:
22
+ p = os.path.join(d, ".claude", "org-lock.json")
23
+ if os.path.isfile(p):
24
+ return p
25
+ if d in ("/", home):
26
+ return None
27
+ d = os.path.dirname(d)
28
+
29
+
30
+ def main():
31
+ try:
32
+ payload = json.load(sys.stdin)
33
+ except Exception:
34
+ payload = {}
35
+ cwd = payload.get("cwd") or os.getcwd()
36
+
37
+ cadence = DEFAULT_CADENCE_MIN
38
+ lock_path = find_lock(cwd)
39
+ if lock_path:
40
+ try:
41
+ with open(lock_path) as fh:
42
+ cadence = int(json.load(fh).get("commitCadenceMinutes", cadence))
43
+ except Exception:
44
+ pass
45
+
46
+ try:
47
+ inside = subprocess.run(
48
+ ["git", "-C", cwd, "rev-parse", "--is-inside-work-tree"],
49
+ capture_output=True, text=True, timeout=10,
50
+ )
51
+ if inside.stdout.strip() != "true":
52
+ sys.exit(0)
53
+ status = subprocess.run(
54
+ ["git", "-C", cwd, "status", "--porcelain"],
55
+ capture_output=True, text=True, timeout=10,
56
+ )
57
+ dirty = [l for l in status.stdout.splitlines() if l.strip()]
58
+ if not dirty:
59
+ sys.exit(0)
60
+ last = subprocess.run(
61
+ ["git", "-C", cwd, "log", "-1", "--format=%ct"],
62
+ capture_output=True, text=True, timeout=10,
63
+ )
64
+ last_ts = int(last.stdout.strip()) if last.returncode == 0 and last.stdout.strip() else 0
65
+ except Exception:
66
+ sys.exit(0)
67
+
68
+ age_min = (time.time() - last_ts) / 60 if last_ts else 10**6
69
+ if age_min < cadence:
70
+ sys.exit(0)
71
+
72
+ hrs = age_min / 60
73
+ age_str = f"{hrs:.1f}h" if hrs >= 1 else f"{int(age_min)}m"
74
+ print(json.dumps({
75
+ "systemMessage": (f"Clarus commit-cadence: {len(dirty)} uncommitted change(s), "
76
+ f"last commit {age_str} ago (cadence {cadence}m). "
77
+ "Checkpoint commit recommended.")
78
+ }))
79
+ sys.exit(0)
80
+
81
+
82
+ if __name__ == "__main__":
83
+ main()
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env python3
2
+ """Clarus FLS audit: PostToolUse hook on Bash.
3
+
4
+ After `sf project deploy start`, checks whether newly deployed fields, objects,
5
+ Apex classes, VF pages, and tabs are granted in the project's canonical
6
+ permission set (canonicalPermset in .claude/org-lock.json). Warns on gaps:
7
+ Salesforce does not auto-grant FLS, so missing grants mean deployed-but-invisible.
8
+ """
9
+ import glob
10
+ import json
11
+ import os
12
+ import re
13
+ import sys
14
+
15
+
16
+ def find_project_file(start, rel):
17
+ d = os.path.abspath(start)
18
+ home = os.path.expanduser("~")
19
+ while True:
20
+ p = os.path.join(d, rel)
21
+ if os.path.exists(p):
22
+ return d, p
23
+ if d in ("/", home):
24
+ return None, None
25
+ d = os.path.dirname(d)
26
+
27
+
28
+ def main():
29
+ try:
30
+ payload = json.load(sys.stdin)
31
+ except Exception:
32
+ sys.exit(0)
33
+ cmd = (payload.get("tool_input") or {}).get("command") or ""
34
+ if not re.search(r"\bsf\s+project\s+deploy\s+start\b", cmd):
35
+ sys.exit(0)
36
+
37
+ cwd = payload.get("cwd") or os.getcwd()
38
+ root, lock_path = find_project_file(cwd, os.path.join(".claude", "org-lock.json"))
39
+ if not lock_path:
40
+ sys.exit(0)
41
+ try:
42
+ with open(lock_path) as fh:
43
+ permset_name = json.load(fh).get("canonicalPermset")
44
+ except Exception:
45
+ sys.exit(0)
46
+ if not permset_name:
47
+ sys.exit(0)
48
+
49
+ permset_path = os.path.join(
50
+ root, "force-app", "main", "default", "permissionsets",
51
+ permset_name + ".permissionset-meta.xml")
52
+ if not os.path.isfile(permset_path):
53
+ sys.exit(0)
54
+ with open(permset_path) as fh:
55
+ permset = fh.read()
56
+
57
+ # Source dirs from the command; default to force-app
58
+ dirs = re.findall(r"--source-dir[=\s]+\"?([^\s\"]+)", cmd) or ["force-app"]
59
+ gaps = []
60
+ for src in dirs:
61
+ for part in src.split(","):
62
+ base = part if os.path.isabs(part) else os.path.join(root, part)
63
+ for f in glob.glob(os.path.join(base, "**", "fields", "*.field-meta.xml"), recursive=True):
64
+ obj = re.search(r"/objects/([^/]+)/fields/", f)
65
+ field = os.path.basename(f).replace(".field-meta.xml", "")
66
+ if obj and f"<field>{obj.group(1)}.{field}</field>" not in permset:
67
+ # formula/required fields don't need FLS, but warn anyway; cheap to dismiss
68
+ gaps.append(f"FLS: {obj.group(1)}.{field}")
69
+ for f in glob.glob(os.path.join(base, "**", "classes", "*.cls-meta.xml"), recursive=True):
70
+ cls = os.path.basename(f).replace(".cls-meta.xml", "")
71
+ if f"<apexClass>{cls}</apexClass>" not in permset:
72
+ gaps.append(f"Apex class access: {cls}")
73
+ for f in glob.glob(os.path.join(base, "**", "pages", "*.page-meta.xml"), recursive=True):
74
+ pg = os.path.basename(f).replace(".page-meta.xml", "")
75
+ if f"<apexPage>{pg}</apexPage>" not in permset:
76
+ gaps.append(f"VF page access: {pg}")
77
+
78
+ if not gaps:
79
+ sys.exit(0)
80
+ shown = gaps[:12]
81
+ more = f" (+{len(gaps) - 12} more)" if len(gaps) > 12 else ""
82
+ msg = (f"Clarus FLS audit: {len(gaps)} resource(s) in this deploy are NOT granted in "
83
+ f"{permset_name}: " + "; ".join(shown) + more +
84
+ ". Deployed-but-invisible risk. Add the grants or confirm they are intentional.")
85
+ print(json.dumps({
86
+ "systemMessage": msg,
87
+ "hookSpecificOutput": {"hookEventName": "PostToolUse", "additionalContext": msg},
88
+ }))
89
+ sys.exit(0)
90
+
91
+
92
+ if __name__ == "__main__":
93
+ main()
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env python3
2
+ """Clarus git-sync: PostToolUse hook on Bash.
3
+
4
+ Fires after `sf project deploy start` commands. Rule: the org must never be
5
+ ahead of git. If the repo is dirty after a deploy, remind agent + human to
6
+ commit and push the deployed source.
7
+ """
8
+ import json
9
+ import os
10
+ import re
11
+ import subprocess
12
+ import sys
13
+
14
+
15
+ def main():
16
+ try:
17
+ payload = json.load(sys.stdin)
18
+ except Exception:
19
+ sys.exit(0)
20
+ cmd = (payload.get("tool_input") or {}).get("command") or ""
21
+ if not re.search(r"\bsf\s+project\s+deploy\s+start\b", cmd):
22
+ sys.exit(0)
23
+ # Skip check-only validates; nothing landed in the org
24
+ if "--dry-run" in cmd or re.search(r"\bdeploy\s+validate\b", cmd):
25
+ sys.exit(0)
26
+
27
+ cwd = payload.get("cwd") or os.getcwd()
28
+ try:
29
+ inside = subprocess.run(
30
+ ["git", "-C", cwd, "rev-parse", "--is-inside-work-tree"],
31
+ capture_output=True, text=True, timeout=10,
32
+ )
33
+ if inside.stdout.strip() != "true":
34
+ sys.exit(0)
35
+ status = subprocess.run(
36
+ ["git", "-C", cwd, "status", "--porcelain"],
37
+ capture_output=True, text=True, timeout=10,
38
+ )
39
+ dirty = [l for l in status.stdout.splitlines() if l.strip()]
40
+ ahead = subprocess.run(
41
+ ["git", "-C", cwd, "rev-list", "--count", "@{u}..HEAD"],
42
+ capture_output=True, text=True, timeout=10,
43
+ )
44
+ unpushed = ahead.stdout.strip() if ahead.returncode == 0 else "?"
45
+ except Exception:
46
+ sys.exit(0)
47
+
48
+ if not dirty and unpushed in ("0", "?"):
49
+ sys.exit(0)
50
+
51
+ parts = []
52
+ if dirty:
53
+ parts.append(f"{len(dirty)} uncommitted change(s)")
54
+ if unpushed not in ("0", "?"):
55
+ parts.append(f"{unpushed} unpushed commit(s)")
56
+ detail = " and ".join(parts)
57
+ msg = (f"Clarus git-sync: deploy completed but the repo has {detail}. "
58
+ "Clarus rule: the org must never be ahead of git. Commit the deployed "
59
+ "source (and push) before moving on.")
60
+ print(json.dumps({
61
+ "systemMessage": msg,
62
+ "hookSpecificOutput": {
63
+ "hookEventName": "PostToolUse",
64
+ "additionalContext": msg,
65
+ },
66
+ }))
67
+ sys.exit(0)
68
+
69
+
70
+ if __name__ == "__main__":
71
+ main()
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env python3
2
+ """Clarus org-guard: PreToolUse hook on Bash.
3
+
4
+ Blocks destructive sf commands unless the target org is explicitly allow-listed
5
+ in the project's .claude/org-lock.json AND its real org ID matches the lock.
6
+ Deterministic enforcement; the companion skill clarus-org-guard handles UX.
7
+ """
8
+ import json
9
+ import os
10
+ import re
11
+ import subprocess
12
+ import sys
13
+
14
+ DESTRUCTIVE = [
15
+ r"\bsf\s+project\s+deploy\b", # deploy + validate (validate runs tests in target)
16
+ r"\bsf\s+data\s+(update|delete|import|upsert|create)\b",
17
+ r"\bsf\s+apex\s+run\b(?!\s+test)", # anonymous apex; `sf apex run test` is excluded
18
+ r"\bsf\s+force:source:(push|deploy)\b",
19
+ r"\bsf\s+project\s+delete\b",
20
+ ]
21
+
22
+
23
+ def deny(reason):
24
+ print(json.dumps({
25
+ "hookSpecificOutput": {
26
+ "hookEventName": "PreToolUse",
27
+ "permissionDecision": "deny",
28
+ "permissionDecisionReason": "CLARUS ORG-GUARD: " + reason,
29
+ }
30
+ }))
31
+ sys.exit(0)
32
+
33
+
34
+ def find_lock(start):
35
+ d = os.path.abspath(start)
36
+ home = os.path.expanduser("~")
37
+ while True:
38
+ p = os.path.join(d, ".claude", "org-lock.json")
39
+ if os.path.isfile(p):
40
+ return p
41
+ if d in ("/", home):
42
+ return None
43
+ d = os.path.dirname(d)
44
+
45
+
46
+ def main():
47
+ try:
48
+ payload = json.load(sys.stdin)
49
+ except Exception:
50
+ sys.exit(0)
51
+ cmd = (payload.get("tool_input") or {}).get("command") or ""
52
+ if "sf " not in cmd and not cmd.strip().startswith("sf"):
53
+ sys.exit(0)
54
+ if not any(re.search(p, cmd) for p in DESTRUCTIVE):
55
+ sys.exit(0) # read-only sf command, pass through
56
+
57
+ cwd = payload.get("cwd") or os.getcwd()
58
+ lock_path = find_lock(cwd)
59
+ if not lock_path:
60
+ deny("destructive sf command in a project with no .claude/org-lock.json. "
61
+ "Create one first (see clarus-org-guard skill): verify each org with "
62
+ "`sf org display --target-org <alias> --json`, confirm with the human, "
63
+ "then write {\"write\": {\"<alias>\": \"<15-char org id>\"}, \"readonly\": {...}}.")
64
+
65
+ try:
66
+ with open(lock_path) as fh:
67
+ lock = json.load(fh)
68
+ except Exception as e:
69
+ deny(f"org-lock.json at {lock_path} is unreadable or invalid JSON ({e}). Fix it before deploying.")
70
+
71
+ m = re.search(r"(?:--target-org|-o)[=\s]+\"?([A-Za-z0-9_.@\-]+)", cmd)
72
+ if not m:
73
+ deny("destructive sf command without an explicit --target-org. "
74
+ "Clarus rule: no implicit default-org writes, ever. Add --target-org <alias>.")
75
+ alias = m.group(1)
76
+
77
+ write_orgs = {k: str(v)[:15] for k, v in (lock.get("write") or {}).items()}
78
+ ro_orgs = {k: str(v)[:15] for k, v in (lock.get("readonly") or {}).items()}
79
+
80
+ if alias in ro_orgs:
81
+ deny(f"'{alias}' is READ-ONLY in {lock_path}. Destructive commands against it are forbidden.")
82
+ if alias not in write_orgs:
83
+ deny(f"'{alias}' is not in the write allowlist in {lock_path}. "
84
+ "If this org genuinely belongs there, follow the verified flow in the "
85
+ "clarus-org-guard skill (human confirmation required).")
86
+
87
+ # Ground-truth verification: does the alias actually point at the locked org?
88
+ try:
89
+ out = subprocess.run(
90
+ ["sf", "org", "display", "--target-org", alias, "--json"],
91
+ capture_output=True, text=True, timeout=45,
92
+ )
93
+ info = json.loads(out.stdout)
94
+ actual = str((info.get("result") or {}).get("id") or "")[:15]
95
+ except Exception as e:
96
+ deny(f"could not verify org identity for '{alias}' ({e}). "
97
+ "Refusing to write to an unverified org. Check `sf org display --target-org "
98
+ f"{alias}` manually.")
99
+
100
+ expected = write_orgs[alias]
101
+ if not actual:
102
+ deny(f"`sf org display` returned no org id for '{alias}'. Refusing unverified write.")
103
+ if actual != expected:
104
+ deny(f"ORG ID MISMATCH for alias '{alias}': lock file expects {expected}, "
105
+ f"but the alias currently points at {actual}. The alias may have been "
106
+ "re-authed to a different org. Do NOT proceed; confirm with the human.")
107
+
108
+ # Verified: allow silently.
109
+ sys.exit(0)
110
+
111
+
112
+ if __name__ == "__main__":
113
+ main()
@@ -0,0 +1,36 @@
1
+ import json, os, glob
2
+
3
+ cwd = os.getcwd().replace('/', '-').lstrip('-')
4
+ base = os.path.expanduser('~/.claude/projects/')
5
+ project_dir = os.path.join(base, '-' + cwd)
6
+ if os.path.isdir(project_dir):
7
+ jsonl_files = [f for f in glob.glob(os.path.join(project_dir, '*.jsonl'))]
8
+ if jsonl_files:
9
+ jsonl_path = max(jsonl_files, key=os.path.getmtime)
10
+ with open(jsonl_path, 'r') as f:
11
+ lines = f.readlines()
12
+ cleaned = []
13
+ stripped = 0
14
+ for line in lines:
15
+ try:
16
+ obj = json.loads(line)
17
+ if isinstance(obj, dict) and 'message' in obj:
18
+ msg = obj['message']
19
+ if isinstance(msg, dict) and 'content' in msg:
20
+ content = msg['content']
21
+ if isinstance(content, list):
22
+ new_content = []
23
+ for block in content:
24
+ if isinstance(block, dict) and block.get('type') == 'image':
25
+ stripped += 1
26
+ new_content.append({'type': 'text', 'text': '[image removed by PreCompact hook]'})
27
+ else:
28
+ new_content.append(block)
29
+ msg['content'] = new_content
30
+ cleaned.append(json.dumps(obj) + '\n')
31
+ except:
32
+ cleaned.append(line)
33
+ with open(jsonl_path, 'w') as f:
34
+ f.writelines(cleaned)
35
+ if stripped > 0:
36
+ print(json.dumps({'systemMessage': f'PreCompact: stripped {stripped} images from JSONL'}))
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "clarus-dev-stack",
3
+ "version": "1.0.0",
4
+ "description": "Clarus Dev Stack: curated Claude Code skills and guardrail hooks for Salesforce consulting work. Curated by Mike Carter, Clarus Group.",
5
+ "bin": {
6
+ "clarus-dev-stack": "bin/install.js"
7
+ },
8
+ "files": [
9
+ "bin/",
10
+ "skills/",
11
+ "hooks/",
12
+ "README.md"
13
+ ],
14
+ "keywords": [
15
+ "claude-code",
16
+ "salesforce",
17
+ "agent-skills",
18
+ "clarus"
19
+ ],
20
+ "author": "Mike Carter <mcarter@clarusgroup.com>",
21
+ "license": "MIT",
22
+ "engines": {
23
+ "node": ">=18"
24
+ }
25
+ }
@@ -0,0 +1,42 @@
1
+ ---
2
+ name: clarus-conventions
3
+ description: |
4
+ Clarus Group house conventions for Salesforce work. This skill SUPPLEMENTS the official Salesforce skills (generating-apex, deploying-metadata, generating-custom-field, etc.) with firm-level standards. Where guidance conflicts, Clarus conventions win for Clarus engagement work.
5
+
6
+ TRIGGER when: generating or modifying any Salesforce metadata (Apex, fields, objects, flows, permsets, LWC), deploying to any org, or starting work in a Salesforce project.
7
+
8
+ SKIP when: the work is not Salesforce-related, or the project is explicitly not a Clarus engagement.
9
+ ---
10
+
11
+ # Clarus Conventions
12
+
13
+ These are firm standards. They encode scar tissue from real engagements. Apply them on top of whatever the official Salesforce skills recommend.
14
+
15
+ ## Deploy discipline
16
+
17
+ 1. **Every `sf` command carries an explicit `--target-org`.** Never rely on the default org. The default org is how wrong-org accidents happen.
18
+ 2. **Never use `--ignore-conflicts` against a client org.** Conflicts are information.
19
+ 3. **Validate before deploying to any production or UAT org** when the change set is non-trivial: check-only first, then deploy.
20
+ 4. **"Unchanged" in a deploy result does NOT mean a component is accessible or working.** It only means metadata matched.
21
+
22
+ ## The FLS pairing rule (the #1 Salesforce deploy gotcha)
23
+
24
+ Salesforce does NOT auto-grant field-level security or object access when metadata deploys. Every new field, object, Apex class, VF page, or tab MUST ship with its permission set grant in the same change, or it will be deployed-but-invisible ("No such column" errors while the metadata looks fine). When a field is unexpectedly inaccessible, diagnose in this order, do not skip ahead: (1) FLS, (2) object CRUD, (3) record ownership/sharing, (4) field actually exists in the org (describe-confirm, not repo-confirm), (5) guest/site user context. Only then consider caching or platform bugs.
25
+
26
+ ## Apex standards
27
+
28
+ 1. No hardcoded record IDs, org IDs, or URLs in Apex. Use Custom Metadata, Custom Settings, or Named Credentials.
29
+ 2. Bulkify everything. Assume 200+ records in every trigger context.
30
+ 3. Test data comes from a TestDataFactory pattern, never from `SeeAllData=true`.
31
+ 4. One trigger per object, logic in handler classes.
32
+
33
+ ## Source control
34
+
35
+ 1. **The org must never be ahead of git.** After every successful deploy to a client org, commit the deployed source and push to the remote in the same work session. A deployed-but-uncommitted change is invisible to the rest of the team and unrecoverable if the sandbox refreshes.
36
+ 2. Commit at meaningful checkpoints, not just at the end of the day. If the working tree has substantial uncommitted changes and the last commit is hours old, stop and commit.
37
+ 3. Commit messages describe the business change, not just the file change.
38
+
39
+ ## Org safety
40
+
41
+ 1. Org allowlists live in each project's `.claude/org-lock.json` (see clarus-org-guard). If a project has no lock file, create one before the first deploy.
42
+ 2. Production orgs are read-only for comparison unless the engagement explicitly includes a production deployment window.
@@ -0,0 +1,23 @@
1
+ ---
2
+ name: clarus-datapack-guard
3
+ description: |
4
+ Clarus tripwire for OmniStudio DataPack imports, especially in Developer Editions and scratch orgs. The official deploying-omnistudio-datapacks skill covers the vlocity CLI path and settings/dependency errors; this skill covers the UI import wizard timeout-and-rollback failure mode it does not encode (gap verified 2026-06-12).
5
+
6
+ TRIGGER when: importing or deploying OmniStudio/Vlocity DataPacks (any path: UI import wizard, vlocity CLI, packDeploy), especially into Developer Edition or scratch orgs; when a DataPack import times out, rolls back, or partially activates; when bulk-activating Data Mappers or OmniScripts after import.
7
+
8
+ SKIP when: standard sf metadata deploys (use deploying-metadata), or OmniStudio authoring work (use the building-omnistudio-* skills).
9
+ ---
10
+
11
+ # Clarus DataPack Guard
12
+
13
+ ## The failure mode this protects against
14
+
15
+ Large DataPack imports into Developer Edition orgs can hit activation timeouts and ROLL BACK the whole import, silently losing work and leaving partial state. The UI import wizard is most vulnerable; it gives no chunking control by default.
16
+
17
+ ## The rules
18
+
19
+ 1. **Chunk imports by component type.** Import Data Mappers SEPARATELY from OmniScripts. Never one giant combined import into a Dev Edition.
20
+ 2. **Activate in passes, not in bulk through the wizard.** If post-import activation times out, activate via Apex DML on `OmniDataTransform` (core runtime) or the package equivalent. Pattern that works: query the inactive records, set `IsActive = true` in batches via anonymous Apex.
21
+ 3. **Know your runtime before you start.** Core runtime (OmniProcess / OmniDataTransform standard objects) vs managed package (`omnistudio__` / `vlocity_*` namespaced objects) changes every API name in your activation scripts. Detect first (the official analyzing-omnistudio-dependencies skill does namespace detection).
22
+ 4. **Inventory before import.** Hand-inventory the DataPack JSON contents (component counts by type) so you know what "complete" looks like and can verify nothing silently rolled back.
23
+ 5. **After any timeout: verify, never assume.** Query actual record counts and activation status against the org. A timed-out import may have partially committed.
@@ -0,0 +1,23 @@
1
+ ---
2
+ name: clarus-deploy-error-translator
3
+ description: Auto-fire after any `sf project deploy` that returns "Status: Failed" or "Component Failures". Reads the error message and translates Salesforce-speak into plain English with a specific suggested fix. Especially useful for cryptic platform errors like "An unexpected error occurred", "setup object in use", "license doesn't allow", or LightningTypeBundle / GenAi* failures.
4
+ ---
5
+
6
+ When a deploy fails:
7
+
8
+ 1. Extract the error message text and component name from the deploy output.
9
+ 2. Match against known patterns:
10
+
11
+ | Pattern | Plain English | Fix |
12
+ |---|---|---|
13
+ | "setup object in use" | Something else still references it | Find consumer (Bot? Plugin? Apex? Permset?) and delete in dependency order |
14
+ | "license doesn't allow the permission" | Permset license can't grant this perm | Match permset license to user license; remove the perm or change license |
15
+ | "license can't be updated" | Can't change license on existing permset | Delete and recreate |
16
+ | "no such column" | Field exists in metadata but lacks FLS | Add field to a permset's fieldPermissions |
17
+ | "Element X is duplicated" | XML schema expects each entry in own wrapper | Each X needs its own outer element, not multiple inside one |
18
+ | "An unexpected error occurred. ErrorId: N" | Platform-side opaque error | Capture ErrorId; check if related metadata is in broken state; consider Salesforce Support |
19
+ | "LightningTypeBundle ... could not be found" | GenAiFunction lacks input/output schema files | Add `input/schema.json` and `output/schema.json` next to the GenAiFunction XML |
20
+ | "An action with developer name X already exists" | Standalone GenAiFunction conflicts with bundle's plannerActions | Either delete standalone or rename bundle action |
21
+ | "Property 'X' not valid in version Y" | sourceApiVersion in sfdx-project.json too low | Bump to 65.0 or whatever the message says |
22
+
23
+ 3. Output: 2-3 sentence translation + the specific next step.
@@ -0,0 +1,95 @@
1
+ ---
2
+ name: clarus-kill-images
3
+ description: |
4
+ Strip base64 image data from ALL JSONL files in the current Claude Code project (conversations + subagents) to reduce file size and prevent context bloat. Replaces `image` content blocks with a short text placeholder, preserving structure while removing the huge base64 payload.
5
+
6
+ TRIGGER when the user asks to strip, clean, remove, or purge images from conversation history or JSONL files. Also trigger on phrases like "kill images," "clean up conversation history," "reduce project size," "my JSONL files are too big," or when a conversation mentions that image content is bloating the transcript/context.
7
+
8
+ SKIP when the user wants to preserve images for reference, when operating outside a Claude Code project directory, or when the request is about actual image files (PNG/JPG/etc.) rather than base64 content inside JSONL.
9
+ ---
10
+
11
+ # Kill Images
12
+
13
+ ## Instructions
14
+
15
+ 1. Find the current project's JSONL files:
16
+ - Look in `~/.claude/projects/` for a directory matching the current working directory (sanitized with dashes)
17
+ - Process ALL `.jsonl` files: top-level conversations AND subagent files in `*/subagents/` subdirectories
18
+
19
+ 2. Run this Python script via Bash to strip images:
20
+
21
+ ```python
22
+ import json, os, sys, glob
23
+
24
+ # Find project dir matching cwd
25
+ cwd = os.getcwd().replace('/', '-').lstrip('-')
26
+ base = os.path.expanduser('~/.claude/projects/')
27
+ project_dir = os.path.join(base, '-' + cwd)
28
+
29
+ if not os.path.isdir(project_dir):
30
+ print(f"No project dir found at {project_dir}")
31
+ sys.exit(1)
32
+
33
+ # Find ALL .jsonl files: top-level + subagents
34
+ jsonl_files = glob.glob(os.path.join(project_dir, '*.jsonl')) + \
35
+ glob.glob(os.path.join(project_dir, '*/subagents/*.jsonl'))
36
+
37
+ if not jsonl_files:
38
+ print("No JSONL files found")
39
+ sys.exit(1)
40
+
41
+ total_stripped = 0
42
+ total_saved = 0
43
+
44
+ class Counter:
45
+ def __init__(self):
46
+ self.n = 0
47
+
48
+ for jsonl_path in jsonl_files:
49
+ orig_size = os.path.getsize(jsonl_path)
50
+ with open(jsonl_path, 'r') as f:
51
+ lines = f.readlines()
52
+
53
+ cleaned = []
54
+ counter = Counter()
55
+ for line in lines:
56
+ try:
57
+ obj = json.loads(line)
58
+ def replace_images(o, c=counter):
59
+ if isinstance(o, list):
60
+ new_list = []
61
+ for item in o:
62
+ if isinstance(item, dict) and item.get('type') == 'image':
63
+ c.n += 1
64
+ new_list.append({'type': 'text', 'text': '[image removed by kill-images]'})
65
+ else:
66
+ replace_images(item, c)
67
+ new_list.append(item)
68
+ o.clear()
69
+ o.extend(new_list)
70
+ elif isinstance(o, dict):
71
+ for v in o.values():
72
+ if isinstance(v, (dict, list)):
73
+ replace_images(v, c)
74
+ replace_images(obj)
75
+ cleaned.append(json.dumps(obj) + '\n')
76
+ except:
77
+ cleaned.append(line)
78
+
79
+ stripped = counter.n
80
+ if stripped > 0:
81
+ with open(jsonl_path, 'w') as f:
82
+ f.writelines(cleaned)
83
+ new_size = os.path.getsize(jsonl_path)
84
+ saved = orig_size - new_size
85
+ total_stripped += stripped
86
+ total_saved += saved
87
+ print(f"{os.path.basename(jsonl_path)}: {stripped} images stripped, saved {saved / 1024 / 1024:.1f}MB")
88
+
89
+ if total_stripped == 0:
90
+ print(f"Scanned {len(jsonl_files)} file(s) — no images found.")
91
+ else:
92
+ print(f"\nTotal: {total_stripped} images stripped across {len(jsonl_files)} file(s), saved {total_saved / 1024 / 1024:.1f}MB")
93
+ ```
94
+
95
+ 3. Report the results to the user (files touched, images stripped, MB saved).
@@ -0,0 +1,58 @@
1
+ ---
2
+ name: clarus-org-guard
3
+ description: |
4
+ Companion skill to the Clarus org-guard HOOK (the deterministic enforcer). Explains org-lock blocks, helps create and maintain .claude/org-lock.json files, and enforces org identity verification practices. The hook physically blocks destructive sf commands against unlisted or mismatched orgs; this skill handles the human-facing side.
5
+
6
+ TRIGGER when: a command is blocked by the Clarus org guard, a project needs an org-lock.json created, the user asks about org safety or wrong-org protection, a new org alias enters the project, or any work begins in a Salesforce project that has no .claude/org-lock.json.
7
+
8
+ SKIP when: non-Salesforce projects.
9
+ ---
10
+
11
+ # Clarus Org Guard (companion skill)
12
+
13
+ ## What the hook does (so you can explain it)
14
+
15
+ A PreToolUse hook intercepts every Bash command containing `sf`. For DESTRUCTIVE commands (deploy, data update/delete/import/upsert/create, apex run), it enforces:
16
+
17
+ 1. An explicit `--target-org` is present. No implicit default-org writes, ever.
18
+ 2. The alias is in the project's `.claude/org-lock.json` under `write`.
19
+ 3. The alias's ACTUAL org ID (via `sf org display`) matches the locked org ID. This catches re-authed aliases silently pointing at the wrong org.
20
+ 4. Orgs under `readonly` allow retrieves/queries but never writes.
21
+ 5. No lock file in the project = destructive commands are blocked until one exists.
22
+
23
+ The hook's decision is deterministic. Do not attempt to work around a block; resolve it.
24
+
25
+ ## The lock file format
26
+
27
+ `<project root>/.claude/org-lock.json`:
28
+
29
+ ```json
30
+ {
31
+ "write": {
32
+ "my-sandbox-alias": "00DgP00000215qv"
33
+ },
34
+ "readonly": {
35
+ "my-prod-alias": "00DDp000001Q5P3"
36
+ },
37
+ "canonicalPermset": "My_Project_Admin",
38
+ "commitCadenceMinutes": 90
39
+ }
40
+ ```
41
+
42
+ Org IDs are the 15-character prefix (the 18-char form is also accepted; comparison uses the first 15). `canonicalPermset` and `commitCadenceMinutes` are optional and consumed by other Clarus hooks.
43
+
44
+ ## Creating a lock file (the verified way)
45
+
46
+ NEVER write an org ID into the lock file from memory or from documentation alone. Verify against ground truth:
47
+
48
+ 1. Run `sf org display --target-org <alias> --json` and read the actual `id`.
49
+ 2. Show the human the alias, username, instance URL, and org ID.
50
+ 3. Ask the human to confirm which orgs belong in `write` vs `readonly`.
51
+ 4. Write the file only after explicit confirmation. Adding an org to `write` is a human decision, always.
52
+
53
+ ## When a block fires
54
+
55
+ 1. Read the block reason to the human in plain language.
56
+ 2. If the org genuinely belongs in the lock file, run the verified creation flow above.
57
+ 3. If the command targeted the wrong org, say so plainly: the guard just did its job.
58
+ 4. Never suggest bypassing the hook, removing the lock file, or running the command with a different alias to evade the block.
@@ -0,0 +1,43 @@
1
+ ---
2
+ name: clarus-permset-watcher
3
+ description: |
4
+ Auto-fire when ANY Salesforce metadata file is written/edited that creates new accessibility surface (custom fields, custom objects, Apex classes, Visualforce pages, custom tabs, GenAi actions). Salesforce does NOT auto-grant FLS/object access to deployed metadata. The result is "deployed but invisible": fields show in metadata API and FieldDefinition but cause "No such column" errors in SOQL/UI. THIS IS THE #1 SALESFORCE DEPLOY GOTCHA. CHECK FOR IT FIRST.
5
+
6
+ TRIGGER on edits to `*.field-meta.xml`, `*.object-meta.xml`, `*.cls-meta.xml`, `*.page-meta.xml`, `*.tab-meta.xml`, `*.genAiFunction-meta.xml`, or `*.flow-meta.xml`. Remind the developer to add the new resource to the project's canonical permission set before they waste time debugging "missing fields".
7
+
8
+ SKIP when: the edit removes a resource, or the resource is already granted in the canonical permset.
9
+ ---
10
+
11
+ # Clarus Permset Watcher
12
+
13
+ ## Finding the canonical permission set (in order)
14
+
15
+ 1. `canonicalPermset` key in `<project root>/.claude/org-lock.json`
16
+ 2. A permset named in the project's CLAUDE.md
17
+ 3. If neither exists: list the permsets in `force-app/main/default/permissionsets/` and ASK the human which one is canonical for this project, then record the answer in org-lock.json so this question never repeats.
18
+
19
+ ## What needs which grant
20
+
21
+ | File pattern | Permission needed | Add to canonical permset as |
22
+ |---|---|---|
23
+ | `objects/*/fields/*.field-meta.xml` | FLS | `<fieldPermissions>` with `field`, `readable`, `editable` |
24
+ | `objects/*.object-meta.xml` (new object) | Object access | `<objectPermissions>` with allowRead/Edit/Create/Delete |
25
+ | `classes/*.cls-meta.xml` | Apex class access | `<classAccesses>` with `apexClass`, `enabled=true` |
26
+ | `pages/*.page-meta.xml` | VF page access | `<pageAccesses>` with `apexPage`, `enabled=true` |
27
+ | `tabs/*.tab-meta.xml` | Tab visibility | `<tabSettings>` with visibility |
28
+ | `genAiFunctions/*/*.genAiFunction-meta.xml` | The Apex wrapper class needs class access | `<classAccesses>` for the `<invocationTarget>` Apex class |
29
+ | `flows/*.flow-meta.xml` (active flows) | Flow access (only if profile-restricted) | usually inherited but check |
30
+
31
+ ## Behavior
32
+
33
+ 1. Detect the type from the file path.
34
+ 2. Read the affected resource's name (field name, object name, class name, etc.).
35
+ 3. Resolve the canonical permset (see above) and read it.
36
+ 4. If the resource is NOT in the permset, warn:
37
+
38
+ ```
39
+ FLS/Permset gap on <resource_name>. This deploy will succeed but the resource will be
40
+ INVISIBLE to users until you add it to <canonical permset>. Fix: <exact XML block>.
41
+ ```
42
+
43
+ 5. Offer to add the grant in the same change. The grant ships WITH the metadata, not after.
@@ -0,0 +1,48 @@
1
+ ---
2
+ name: clarus-test-discipline
3
+ description: |
4
+ Clarus guardrail: never change production business logic just to make a failing Apex test pass. Protects business logic from silent mutation during test-fix loops.
5
+
6
+ TRIGGER when: an Apex test fails and a fix is being attempted, when iterating on test classes against existing production code, when coverage requirements force changes, or when any edit to a non-test class happens during a test-fixing session.
7
+
8
+ SKIP when: writing brand-new code with brand-new tests (no existing behavior to protect), or when the human has explicitly stated the production logic itself is wrong and approved changing it.
9
+ ---
10
+
11
+ # Clarus Test Discipline
12
+
13
+ ## The rule
14
+
15
+ A failing test is a question, not an instruction. It asks: "is the test wrong, or is the code wrong?" Answer the question BEFORE changing anything. The most common and most dangerous failure mode in agent-assisted Apex work is silently rewriting production business logic until the test goes green.
16
+
17
+ ## When a test fails, diagnose in this order
18
+
19
+ 1. **Test data.** Is the test creating valid, complete records? Missing required fields, missing parent records, and validation rule collisions cause most failures. Fix: the test's data setup.
20
+ 2. **Test assumptions.** Does the assertion reflect the actual business requirement? A wrong expected value is a test bug. Fix: the assertion, with a comment explaining the requirement.
21
+ 3. **Test isolation.** Order-of-execution issues, missing Test.startTest/stopTest, governor limits inside the test, mocking gaps. Fix: the test structure.
22
+ 4. **Environment.** Org-specific config the test assumes (custom settings, metadata records, feature flags). Fix: the test's setup or a TestDataFactory addition.
23
+ 5. **ONLY THEN consider that the production code is wrong.**
24
+
25
+ ## If the production code appears wrong
26
+
27
+ STOP. Do not edit it. Present the case to the human:
28
+
29
+ ```
30
+ The test expects X. The production code does Y.
31
+ I believe the code is wrong because <reason>.
32
+ Changing <class.method> would alter live business behavior: <what changes for users>.
33
+ Approve the production change, or should the test reflect current behavior?
34
+ ```
35
+
36
+ Only proceed after explicit approval. The human owns business behavior; the agent owns test mechanics.
37
+
38
+ ## Forbidden moves (never do these to reach green)
39
+
40
+ 1. Weakening an assertion (`assertEquals` to `assertNotEquals`, removing asserts, asserting true).
41
+ 2. Adding `Test.isRunningTest()` branches to production code to bypass logic under test.
42
+ 3. Changing field values, formulas, or conditional logic in production classes without approval.
43
+ 4. Commenting out or deleting the failing test.
44
+ 5. Catch-and-swallow in production code to suppress the exception the test exposes.
45
+
46
+ ## Coverage pressure
47
+
48
+ When coverage is below threshold, the fix is MORE test scenarios, never trimmed production logic. Dead code discovered during coverage work gets reported to the human, not silently deleted.