claude-dev-env 2.14.0 → 2.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/AGENTS.md +106 -32
  2. package/agents/AGENTS.md +0 -5
  3. package/agents/test_agent_frontmatter.py +7 -7
  4. package/bin/AGENTS.md +6 -4
  5. package/bin/install-constants.mjs +51 -0
  6. package/bin/install.codex-rules.test.mjs +173 -0
  7. package/bin/install.cursor-rules.test.mjs +103 -0
  8. package/bin/install.mjs +130 -19
  9. package/bin/install.profile-root.test.mjs +8 -0
  10. package/bin/install.prune.test.mjs +2 -1
  11. package/bin/install.test.mjs +3 -3
  12. package/bin/install.transaction.test.mjs +1 -0
  13. package/bin/install.uninstall-transaction.test.mjs +1 -0
  14. package/bin/resolve-install-root.mjs +43 -10
  15. package/codex-rules/claude-dev-env.rules +12 -0
  16. package/commands/AGENTS.md +0 -10
  17. package/hooks/blocking/test_claude_md_orphan_file_blocker.py +1 -1
  18. package/hooks/diagnostic/AGENTS.md +32 -0
  19. package/hooks/diagnostic/hook_log_init.py +2 -2
  20. package/output-styles/AGENTS.md +1 -1
  21. package/package.json +2 -1
  22. package/scripts/sync_to_cursor/AGENTS.md +3 -3
  23. package/scripts/sync_to_cursor/canonical_docs.py +11 -11
  24. package/scripts/sync_to_cursor/config/__init__.py +8 -0
  25. package/scripts/sync_to_cursor/engine.py +26 -1
  26. package/scripts/sync_to_cursor/rules.py +76 -5
  27. package/scripts/test_active_capability_references.py +2 -2
  28. package/scripts/tests/AGENTS.md +2 -0
  29. package/scripts/tests/test_engine.py +102 -0
  30. package/scripts/tests/test_rules.py +79 -0
  31. package/skills/anthropic-plan/AGENTS.md +1 -1
  32. package/skills/anthropic-plan/SKILL.md +1 -1
  33. package/skills/anthropic-plan/test_skill_contract.py +8 -6
  34. package/skills/prototype/SKILL.md +1 -2
  35. package/skills/prototype/reference/promotion-tasks.md +1 -1
  36. package/skills/prototype/workflows/promotion.md +1 -1
  37. package/agents/caveman.md +0 -73
  38. package/agents/clasp-deployment-orchestrator.md +0 -608
  39. package/agents/code-advisor.md +0 -23
  40. package/agents/deep-research.md +0 -152
  41. package/agents/docs-agent.md +0 -85
  42. package/commands/commit.md +0 -28
  43. package/commands/docupdate.md +0 -322
  44. package/commands/hook-log-extract.md +0 -70
  45. package/commands/hook-log-init.md +0 -76
  46. package/commands/implement.md +0 -102
  47. package/commands/plan.md +0 -14
  48. package/commands/pr-comments.md +0 -47
  49. package/commands/review-plan.md +0 -5
  50. package/commands/right-size.md +0 -15
  51. package/commands/sum.md +0 -30
  52. package/scripts/sync_to_cursor/config.py +0 -5
package/AGENTS.md CHANGED
@@ -1,61 +1,135 @@
1
- # Development Assistant
1
+ # Scope
2
2
 
3
- ## Communication
3
+ Every rule in this file governs all text everywhere: chat replies, tool-call sentences, plans, questions you ask, code, code comments, test names, commit subjects, pull request and issue bodies, documentation, and every file you write. No rule stops at the edge of a chat message.
4
4
 
5
- Reply shape and length: follow `~/.claude/rules/eli11-replies.md`. Word choice: follow `~/.claude/rules/plain-language.md`. Progress and finals: follow `~/.claude/rules/opus5-communication-contract.md` (`opus5-communication-contract-v1`). State claims affirmatively.
5
+ A rule that names a form in order to forbid it passes its own check, and so does a two-column table that teaches a rewrite.
6
6
 
7
- ## Security
7
+ Ask when ambiguity materially changes scope or implementation. Collect credentials through secure UI only; never request secrets in chat.
8
8
 
9
- Collect credentials through secure UI only; never request secrets in chat.
9
+ ## Documentation
10
10
 
11
- A runtime value that is itself private a host, an SSH user or port, an owner scope, an account ID — lives in git-ignored local configuration with a committed placeholder in its place. Source files never carry the real value.
11
+ Describe only the current system state. Keep documentation self-contained and free of historical, transitional, conversational, or version-transition language. Never use negative prose or antipatterns. Always state what to do, specifically.
12
+ Code and tests
12
13
 
13
- ## Advisors
14
+ Tests must exercise real behavior, real data, and production paths. Test theater is forbidden.
14
15
 
15
- | Path | Holds |
16
- |---|---|
17
- | `~/.claude/docs/references/advisor-tool.md` | When to call a stronger reviewer, hard rule before first write, how to treat advice |
18
- | `/team-advisor` skill | Standing warm advisor bind (map: `docs/references/team-advisor-skill.md`) |
19
- | `~/.claude/_shared/advisor/advisor-protocol.md` | Host bind, model floor, lifecycle |
16
+ For multi-step code tasks:
20
17
 
21
- Use `/team-advisor` under the rules in `advisor-tool.md` for every advisor consultation.
18
+ Coders consult a warm session-advisor when blocked (Sol xHigh).
19
+ Repair reported findings when that review mode is selected.
22
20
 
23
- ## Files and workspaces
21
+ Research and delegation
22
+ Delegate fact extraction when multiple files or search patterns are required. Request precise file-and-line answers.
24
23
 
25
- Put all work in an isolated worktree under the repo's `.claude/worktrees/`.
24
+ Use warm & reusable parallel luna (you decide effort level per task) fast subagents for unrelated questions; threaded & named appropriately.
26
25
 
27
- Default to Edit for existing files; reach for Write only when the path is genuinely new.
26
+ Read or search directly only in files you will modify via es.exe.
28
27
 
29
- ## Code and tests
28
+ For code navigation, prefer es.exe, then content search or globbing.
30
29
 
31
- Tests must exercise real behavior, real data, and production paths.
30
+ Scope every es.exe search.
32
31
 
33
- Keep changes within scope. Prefer durable systemic fixes for reusable behavior.
32
+ Never scan an entire drive or network share.
34
33
 
35
- Do not rewrite entire files or rename public parameters without need.
34
+ Task tracking
35
+ Track every task using `update_plan`.
36
36
 
37
- ## Reviews
37
+ ## Definitions
38
+ Warm agent: Any agent who has acted within the past 30 minutes.
39
+
40
+ # Response and working style
41
+
42
+ Mid-run and closing narration follow `rules/opus5-communication-contract.md` (`opus5-communication-contract-v1`): first progress update is one sentence; later updates only for important discoveries or direction changes; the final starts with the outcome.
43
+
44
+ Keep responses focused, brief, and concise. Keep disclaimers and caveats short, and spend most of the response on the main answer. When asked to explain something, give a high-level summary unless an in-depth explanation is specifically requested.
45
+
46
+ # Word budget
47
+
48
+ Say it in the fewest words that stay accurate and complete. Before sending, cut every sentence that does not change what the reader thinks or does.
49
+
50
+ Cut these on sight:
51
+
52
+ - Deliberation. State the decision, not the reasoning that reached it, unless the reader has to weigh it themselves.
53
+ - Why you did not do something. Say what you did; add the reason only if the reader must decide whether to do it.
54
+ - Incidental findings from your own process. Report one only when the reader must act on it, and give it one line.
55
+ - Any sentence that restates a fact already stated in a heading, a list, or an earlier line.
56
+
57
+ When you have more than two facts of the same kind, use a list or a table. Prose paragraphs hide facts; rows expose them.
58
+
59
+ # No contrast framing
60
+
61
+ Write the claim. Never prop it up against what it is not.
38
62
 
39
- Verify every sub-agent file list, count, description, and finding against the repository and diff.
63
+ The banned shape is a claim paired with a rejected alternative, in any wording:
40
64
 
41
- Do not commit untracked files unless explicitly instructed.
65
+ | Banned | Write instead |
66
+ |---|---|
67
+ | Verified against the remote, not just locally | Verified against the remote |
68
+ | This is a design flaw, not a typo | This is a design flaw |
69
+ | Not a copy of the shared script, but an ad |
70
+ | Rather than patching the caller, the fix moves into the helper | The fix moves into the helper |
71
+ | Instead of three passes, it runs one | It runs one pass |
72
+ | It is not only faster; it is correct | It is correct and faster |
73
+ | This is less a bug than a missing feature
74
+ | Let me read the log rather than guessing | Reading the log. |
75
+ | I'll patch the helper instead of the caller | Patching the helper. |
42
76
 
43
- ## Package communication contract
77
+ Every wording of the shape is banned, including `X, not Y`, `not Y but X`, `rather than Y, X`, `instead of Y, X`, `X over Y`, `not just X — Y`, `less X than Y`, and a negated sentence followed by its po
78
+ ──── (152 lines hidden) ─────────────────────────────────────────────────────────────────────────────────────────────
79
+ the name.
44
80
 
45
- Use `opus5-communication-contract-v1` for package communication.
81
+ | Written on the day | Named for the subject |
82
+ |---|---|
83
+ | `august_cert_failures.py` | `cert_rejections.py` |
84
+ | `fix_august_bug()` | `normalize_calendar_color()` |
85
+ | `AUGUST_REJECTION_CODES` | `REJECTION_CODE
86
+ | `test_august_failures` | `test_rejects_wrong_calendar_color` |
87
+ | `jira4821_validator.py` | `manifest_validator.py` |
88
+ | `q3_migration/` | `add_tenant_id_column/` |
89
+ | `v2_client.py` | `retrying_client.py` |
90
+ | `legacy_export.py` | `csv_export.py` |
91
+ | `temp_fix.py` | `unicode_path_workaround.p
92
+ | "Fix August cert failures" | "Fix calendar color mismatch in cert export" |
46
93
 
47
- ## Delegation
94
+ A branch name is a name. It carries no date,ither. Someone reads it to decide whether to check the branch out, so it has to say what the work does.
48
95
 
49
- Request precise file-and-line answers from research subagents.
96
+ | Written on the day | Named for the subject |
97
+ |---|---|
98
+ | `fix/cert-2026-08-b3-calendar-widget-color` | `fix-calendar-widget-color` |
99
+ | `parse-rejection-emails-cert-2026-08-b3` |
50
100
 
51
- ## Task tracking
101
+ One prefix spreads. Once `august_` sits in one name, the next name matches it for consistency, and within a week the month reads as a real domain concept that forty places depend on. Rename it the hour you notice it.
52
102
 
53
- Track multi-step work with the `task-build` skill.
103
+ # Change size
54
104
 
55
- ## Repository rule
105
+ When planning work or opening a pull request, size the change first: one self-contained change, around 100 lines, with its tests. Read the small-changelists guide for the numbers, the allowed exceptions, and how to split.
56
106
 
57
- Before changing skill, rule, or hook installation in the claude-dev-env repo, read `docs/references/skill-install-system.md`.
107
+ ## Execution and delegation
108
+
109
+ Delegate all task work to Tier 3 agents.
110
+
111
+ Draft a separate assignment for each agent. Each assignment must be clear, concise, tightly scoped, independently executable, and explicit about ownership, constraints, deliverables, and verification.
112
+
113
+ Run independent assignments in parallel. Keep overlapping work sequential. The primary agent coordinates agents, resolves dependencies, verifies results, and reports outcomes.
58
114
 
59
115
  ## Definitions
60
116
 
61
- Warm agent: active within the past 59 minutes.
117
+ Tier 3 agent: A strong execution specialist that independently completes a bounded assignment, follows repository contracts, repairs routine failures, tests production behavior, and escalates decisions that materially affect architecture or scope.
118
+
119
+ Warm agent: An agent that has acted within the past 30 minutes. Reuse warm agents for related follow-up work.
120
+
121
+ # Corrections
122
+
123
+ Only correct an earlier statement when the ecode, conclusions, or decisions. Statecorrections plainly and briefly, then continue the task. For slips that change nothing for the user, make the fix and move on without noting it.
124
+
125
+ # Tool calls and output hygiene
126
+
127
+ When you use a tool, you may say a brief sentence first. If no tool can express what the user asked for, say so. Do not include internal or system XML tags in your response.
128
+
129
+ # Code review
130
+
131
+ When reviewing code, report everything you find. Filtering belongs in a separate pass.
132
+
133
+ <tone_preference>
134
+ Keep outputs reasonably concise.
135
+ </tone_preference>
package/agents/AGENTS.md CHANGED
@@ -6,13 +6,8 @@ Agent definition files installed into `~/.claude/agents/` by `bin/install.mjs`.
6
6
 
7
7
  | File | Agent name | Role |
8
8
  |---|---|---|
9
- | `caveman.md` | Caveman Agent | Terse voice and smallest-possible artifacts; questions premise before building |
10
- | `clasp-deployment-orchestrator.md` | Clasp Deployment Orchestrator | Creates and deploys Google Apps Script projects with multiple files |
11
9
  | `clean-coder.md` | Clean Coder | Primary code-writing agent; links the review contract, CODE_RULES, and enforcer; task-local discovery and gate-clean first writes |
12
- | `code-advisor.md` | Code Advisor | Single-executor mid-run advisor (PLAN/CORRECTION/STOP as final text); distinct from session-advisor |
13
10
  | `code-quality-agent.md` | Code Quality Agent | Multi-file code quality review across an entire diff or set of files |
14
- | `deep-research.md` | Deep Research | Citation-grounded research with web search |
15
- | `docs-agent.md` | Docs Agent | Documentation authoring and maintenance |
16
11
  | `git-commit-crafter.md` | Git Commit Crafter | Stages changes, writes conventional commit messages, creates commits |
17
12
  | `issue-tracker.md` | Issue Tracker | Primary handler for one GitHub issue action per spawn; loads the issue-tracker skill (plain-brief); returns issue numbers and URLs |
18
13
  | `plan-packet-validator.md` | Plan Packet Validator | Fresh-context validator for workflow-generated plan packets under `docs/plans/` |
@@ -42,8 +42,8 @@ non-empty string, and a `name` equal to its file stem — a mapping that loads
42
42
  but binds `description` to nothing, or names an agent the file does not,
43
43
  registers a subagent the caller cannot spawn::
44
44
 
45
- ok: docs-agent.md -> name: docs-agent
46
- flag: docs-agent.md -> name: doc-manager <- wrong spawn id
45
+ ok: clean-coder.md -> name: clean-coder
46
+ flag: clean-coder.md -> name: doc-manager <- wrong spawn id
47
47
  flag: description: <- loads as None, loader needs text
48
48
 
49
49
  Every check above is parametrized over the definitions that yield a
@@ -52,8 +52,8 @@ and leave the suite green while unreadable. The block is what the fence lines
52
52
  delimit, so the file that opens no fence or never closes one is exactly the
53
53
  broken file these checks exist to catch::
54
54
 
55
- ok: docs-agent.md -> --- name/description --- <- block found
56
- flag: docs-agent.md -> --- name/description <- no closing fence,
55
+ ok: clean-coder.md -> --- name/description --- <- block found
56
+ flag: clean-coder.md -> --- name/description <- no closing fence,
57
57
  silently uncovered
58
58
 
59
59
  `test_every_agent_definition_yields_a_frontmatter_block` holds that floor: it
@@ -218,8 +218,8 @@ def _agent_name_problem(parsed_frontmatter: object, expected_name: str) -> str |
218
218
  A subagent registers under the name in its frontmatter, so a name that is
219
219
  not the file stem is spawned by an id no caller uses::
220
220
 
221
- docs-agent.md -> name: docs-agent -> ok: None
222
- docs-agent.md -> name: doc-manager -> flag: wrong spawn id
221
+ clean-coder.md -> name: clean-coder -> ok: None
222
+ clean-coder.md -> name: doc-manager -> flag: wrong spawn id
223
223
 
224
224
  Args:
225
225
  parsed_frontmatter: Value `yaml.safe_load` produced for the block.
@@ -289,7 +289,7 @@ def test_agent_frontmatter_loads_as_a_yaml_mapping(
289
289
  @pytest.mark.parametrize(
290
290
  "agent_file_name",
291
291
  (
292
- "docs-agent.md",
292
+ "clean-coder.md",
293
293
  "issue-tracker.md",
294
294
  "skill-writer-agent.md",
295
295
  ),
package/bin/AGENTS.md CHANGED
@@ -1,20 +1,22 @@
1
1
  # bin
2
2
 
3
- The installer and its companion modules. Running `npx claude-dev-env` (or `node bin/install.mjs`) copies package files into the managed root (`~/.claude/` by default; `CLAUDE_CONFIG_DIR` or `--target` selects another), merges hook entries into that root's `settings.json`, installs Git hooks, and writes `~/.mypy.ini` under the process home.
3
+ The installer and its companion modules. Running `npx claude-dev-env` (or `node bin/install.mjs`) copies package files into the managed root (`~/.claude/` by default; `CLAUDE_CONFIG_DIR` or `--target` selects another), merges hook entries into that root's `settings.json`, installs Git hooks, writes `~/.mypy.ini` under the process home, and copies Codex exec-policy files into `~/.codex/rules` (`CODEX_HOME/rules` when that variable is set), and generates Cursor `.mdc` files into `~/.cursor/rules` from the installed Claude rules.
4
4
 
5
5
  ## Files
6
6
 
7
7
  | File | Purpose |
8
8
  |---|---|
9
9
  | `install.mjs` | Main installer: builds a read-only plan via `install-plan.mjs`, then runs mutations inside `install-transaction.mjs` recovery (copy content directories, merge hooks, install skills, prune, git hooks, mypy.ini); routes `CLAUDE_HOME`, the manifest path, and `~/.mypy.ini` through `resolve-install-root.mjs`; resolves single or multi-profile targets before mutation and writes one ownership manifest per target |
10
- | `resolve-install-root.mjs` | Pure install-root resolver: precedence `--target` > `CLAUDE_CONFIG_DIR` > `~/.claude`, separator-boundary containment, and the declared external allowlist for `~/.mypy.ini` |
10
+ | `resolve-install-root.mjs` | Pure install-root resolver: precedence `--target` > `CLAUDE_CONFIG_DIR` > `~/.claude`, separator-boundary containment, and the declared external allowlist for `~/.mypy.ini` plus files under the Codex rules directory and the Cursor home |
11
11
  | `select-install-targets.mjs` | Pure target selection for main-default, explicit `--target`, and `--profile`/`--profiles`; rejects ambiguous or duplicate targets; builds per-target manifest records with `targetIdentity` and `managedRoot` |
12
12
  | `install-plan.mjs` | Read-only install and uninstall plans: install preflight (managed root, source conflicts, Python, settings when hooks install) and uninstall preflight (settings JSON before removal, removable vs skipped manifest records), freezes plans E2/F execute |
13
13
  | `install-transaction.mjs` | Install, update, and uninstall transaction journal: captures prior settings, manifest, managed files, and `core.hooksPath`, restores them on failure, and supports fault injection phases for recovery tests |
14
14
  | `install.transaction.test.mjs` | Unit and sandbox installer tests for snapshot/restore and fault phases (`after_file_staging`, `after_settings_write`, `after_git_config`, `after_manifest_write`) |
15
15
  | `install.uninstall-transaction.test.mjs` | Uninstall plan preflight and recovery: malformed/non-object settings fail before removal, each fault phase restores files/settings/manifest/`core.hooksPath`, retry succeeds, selected-root containment |
16
16
  | `install.profile-root.test.mjs` | Contract tests for the install-root resolver: precedence, containment boundary, external allowlist, and the install.mjs import smoke check |
17
- | `install-constants.mjs` | The named values `install.mjs` reads: `SKIPPED_SOURCE_ENTRY_NAMES` and `SKIPPED_SOURCE_FILE_EXTENSIONS` for the build artifacts the source walk leaves behind, `RUN_BACKUP_DIRECTORY_NAME_PATTERN` for the timestamp shape a run backup directory carries, `MANAGED_SKILLS_DIRECTORY_NAME` and `MANAGED_HOOKS_DIRECTORY_NAME` for the directory name each of those trees carries in a package source and under `~/.claude` read by the copy loops, the hooks.json reads, the git-hook shims, the mypy configuration, and the prunes alike — `SETTINGS_FILE_NAME` for the settings file the merge, the retired-hook prune, and the uninstall purge share, and `MYPY_INI_FILE_NAME` for the home-directory file `install_mypy_ini.mjs` writes (also imported by `resolve-install-root.mjs`) |
17
+ | `install.codex-rules.test.mjs` | Tests that Codex exec-policy files copy to `~/.codex/rules`, honor `CODEX_HOME`, skip `--only journal`, and uninstall without touching `default.rules` |
18
+ | `install.cursor-rules.test.mjs` | Tests that Cursor `.mdc` files generate into `~/.cursor/rules` from Claude rules, skip `--only journal`, and leave a local extra `.mdc` in place |
19
+ | `install-constants.mjs` | The named values `install.mjs` reads: `SKIPPED_SOURCE_ENTRY_NAMES` and `SKIPPED_SOURCE_FILE_EXTENSIONS` for the build artifacts the source walk leaves behind, `RUN_BACKUP_DIRECTORY_NAME_PATTERN` for the timestamp shape a run backup directory carries, `MANAGED_SKILLS_DIRECTORY_NAME` and `MANAGED_HOOKS_DIRECTORY_NAME` for the directory name each of those trees carries in a package source and under `~/.claude` — read by the copy loops, the hooks.json reads, the git-hook shims, the mypy configuration, and the prunes alike — `SETTINGS_FILE_NAME` for the settings file the merge, the retired-hook prune, and the uninstall purge share, and `MYPY_INI_FILE_NAME` for the home-directory file `install_mypy_ini.mjs` writes, plus the Codex home and rules directory names `resolve-install-root.mjs` uses |
18
20
  | `ever-shipped-skills.mjs` | Static `EVER_SHIPPED_SKILL_NAMES` set of every top-level skill directory name the package has shipped; the installer subtracts the current skill set from it to prune retired skills left under `~/.claude/skills` |
19
21
  | `expand_home_directory_tokens.mjs` | Expands residual `$HOME` / `${HOME}` / `~/` tokens in settings.json hook and statusLine commands to absolute home paths at install time (literal-safe for homes that contain `$`) |
20
22
  | `git_hooks_installer.mjs` | Installs or updates the `pre-commit`, `pre-push`, and `post-commit` Git hooks in the user's git config; writes hook scripts that delegate to the installed Python hooks |
@@ -78,7 +80,7 @@ A run that moves nothing sweeps nothing, so every recovery point the user holds
78
80
 
79
81
  `--uninstall` builds a read-only uninstall plan, captures a recovery snapshot, then removes each file the plan lists.
80
82
 
81
- Settings JSON is validated before any removal. A malformed or non-object `settings.json` fails closed with the managed files still on disk. Each manifest record passes a containment guard: the path resolves under `~/.claude`, or it names the `~/.mypy.ini` the install writes in the home directory. Every other record is skipped with a warning and counted. Skipping keeps one malformed record — hand-edited, or written by an installer that ran against a different home — from stranding the user with a half-removed install. The purge removes every legitimate record, clears the manifest, and reports the skipped count.
83
+ Settings JSON is validated before any removal. A malformed or non-object `settings.json` fails closed with the managed files still on disk. Each manifest record passes a containment guard: the path resolves under `~/.claude`, or it names the `~/.mypy.ini` the install writes in the home directory, or it sits under the Codex rules directory. Every other record is skipped with a warning and counted. Skipping keeps one malformed record — hand-edited, or written by an installer that ran against a different home — from stranding the user with a half-removed install. The purge removes every legitimate record, clears the manifest, and reports the skipped count.
82
84
 
83
85
  The uninstall runs inside the same snapshot/restore journal as install: prior settings, manifest, managed files, and `core.hooksPath` restore when a later phase fails, so a retry starts from a complete ownership record. The journal is discarded only after a successful commit.
84
86
 
@@ -85,3 +85,54 @@ export const SETTINGS_FILE_NAME = 'settings.json';
85
85
  * removes the file the install created.
86
86
  */
87
87
  export const MYPY_INI_FILE_NAME = '.mypy.ini';
88
+
89
+ /**
90
+ * Environment variable Codex uses for its config home. When unset, Codex reads
91
+ * `~/.codex`. The installer copies shipped exec-policy files into
92
+ * `<that home>/rules`.
93
+ */
94
+ export const CODEX_HOME_ENVIRONMENT_VARIABLE = 'CODEX_HOME';
95
+
96
+ /**
97
+ * Directory name Codex uses under the user home when `CODEX_HOME` is unset.
98
+ */
99
+ export const DEFAULT_CODEX_DIRECTORY_NAME = '.codex';
100
+
101
+ /**
102
+ * Directory name under the Codex home that holds `*.rules` exec-policy files.
103
+ * Codex loads every file in that directory; see `load_exec_policy` in Codex.
104
+ */
105
+ export const CODEX_RULES_DIRECTORY_NAME = 'rules';
106
+
107
+ /**
108
+ * Package subdirectory that holds the shipped Codex exec-policy files. The
109
+ * installer copies this tree into the Codex rules directory, not into
110
+ * `~/.claude/`.
111
+ */
112
+ export const CODEX_RULES_PACKAGE_DIRECTORY_NAME = 'codex-rules';
113
+
114
+ /**
115
+ * Shipped exec-policy file name. A distinct name keeps a local `default.rules`
116
+ * file in place.
117
+ */
118
+ export const CODEX_RULES_SHIPPED_FILE_NAME = 'claude-dev-env.rules';
119
+
120
+ /**
121
+ * Directory name Cursor uses under the user home for editor config.
122
+ */
123
+ export const DEFAULT_CURSOR_DIRECTORY_NAME = '.cursor';
124
+
125
+ /**
126
+ * Directory name under the Cursor home that holds generated `.mdc` rule files.
127
+ */
128
+ export const CURSOR_RULES_DIRECTORY_NAME = 'rules';
129
+
130
+ /**
131
+ * Installed script that writes Cursor `.mdc` files from Claude rules.
132
+ */
133
+ export const CURSOR_SYNC_SCRIPT_FILE_NAME = 'sync_to_cursor.py';
134
+
135
+ /**
136
+ * Windows Python launcher command the installer may bake into hook settings.
137
+ */
138
+ export const WINDOWS_PYTHON_LAUNCHER_COMMAND = 'py -3';
@@ -0,0 +1,173 @@
1
+ import { test } from 'node:test';
2
+ import { strict as assert } from 'node:assert';
3
+ import { execFileSync } from 'node:child_process';
4
+ import {
5
+ mkdtempSync,
6
+ mkdirSync,
7
+ writeFileSync,
8
+ readFileSync,
9
+ existsSync,
10
+ rmSync,
11
+ } from 'node:fs';
12
+ import { tmpdir } from 'node:os';
13
+ import { dirname, join } from 'node:path';
14
+ import { fileURLToPath } from 'node:url';
15
+ import {
16
+ resolveInstallRoot,
17
+ isAllowedInstallDestination,
18
+ } from './resolve-install-root.mjs';
19
+ import {
20
+ CODEX_HOME_ENVIRONMENT_VARIABLE,
21
+ CODEX_RULES_DIRECTORY_NAME,
22
+ CODEX_RULES_PACKAGE_DIRECTORY_NAME,
23
+ CODEX_RULES_SHIPPED_FILE_NAME,
24
+ DEFAULT_CODEX_DIRECTORY_NAME,
25
+ } from './install-constants.mjs';
26
+ import { CONTENT_DIRECTORIES, INSTALL_GROUPS } from './install.mjs';
27
+
28
+ const THIS_DIRECTORY = dirname(fileURLToPath(import.meta.url));
29
+ const INSTALLER_PATH = join(THIS_DIRECTORY, 'install.mjs');
30
+ const PACKAGE_DIRECTORY = dirname(THIS_DIRECTORY);
31
+ const SHIPPED_RULES_SOURCE_PATH = join(
32
+ PACKAGE_DIRECTORY,
33
+ CODEX_RULES_PACKAGE_DIRECTORY_NAME,
34
+ CODEX_RULES_SHIPPED_FILE_NAME,
35
+ );
36
+
37
+ function runInstaller(homeDirectory, extraArguments) {
38
+ return execFileSync('node', [INSTALLER_PATH, ...extraArguments], {
39
+ cwd: PACKAGE_DIRECTORY,
40
+ encoding: 'utf8',
41
+ env: {
42
+ ...process.env,
43
+ HOME: homeDirectory,
44
+ USERPROFILE: homeDirectory,
45
+ GIT_CONFIG_GLOBAL: join(homeDirectory, '.gitconfig'),
46
+ [CODEX_HOME_ENVIRONMENT_VARIABLE]: join(homeDirectory, DEFAULT_CODEX_DIRECTORY_NAME),
47
+ },
48
+ });
49
+ }
50
+
51
+ test('CONTENT_DIRECTORIES omits codex-rules because that tree installs to the Codex home', () => {
52
+ assert.equal(CONTENT_DIRECTORIES.includes(CODEX_RULES_PACKAGE_DIRECTORY_NAME), false);
53
+ });
54
+
55
+ test('the core group installs Codex exec-policy files', () => {
56
+ assert.equal(INSTALL_GROUPS.core.includeCodexRules, true);
57
+ });
58
+
59
+ test('resolveInstallRoot names ~/.codex/rules and allows files under it', () => {
60
+ const homeDirectory = join(tmpdir(), 'cdev-codex-rules-home');
61
+ const resolution = resolveInstallRoot({
62
+ homeDirectory,
63
+ environment: {},
64
+ explicitTarget: null,
65
+ });
66
+ const expectedDirectory = join(homeDirectory, DEFAULT_CODEX_DIRECTORY_NAME, CODEX_RULES_DIRECTORY_NAME);
67
+ assert.equal(resolution.codexRulesInstallDirectory, expectedDirectory);
68
+ assert.equal(
69
+ isAllowedInstallDestination(join(expectedDirectory, CODEX_RULES_SHIPPED_FILE_NAME), resolution),
70
+ true,
71
+ );
72
+ assert.equal(
73
+ isAllowedInstallDestination(join(homeDirectory, '.ssh', 'id_rsa'), resolution),
74
+ false,
75
+ );
76
+ });
77
+
78
+ test('CODEX_HOME relocates the Codex rules destination', () => {
79
+ const homeDirectory = join(tmpdir(), 'cdev-codex-home-default');
80
+ const relocatedHome = join(tmpdir(), 'cdev-codex-home-relocated');
81
+ const resolution = resolveInstallRoot({
82
+ homeDirectory,
83
+ environment: { [CODEX_HOME_ENVIRONMENT_VARIABLE]: relocatedHome },
84
+ explicitTarget: null,
85
+ });
86
+ assert.equal(
87
+ resolution.codexRulesInstallDirectory,
88
+ join(relocatedHome, CODEX_RULES_DIRECTORY_NAME),
89
+ );
90
+ });
91
+
92
+ test('a full install copies shipped Codex rules and leaves a local default.rules in place', () => {
93
+ const homeDirectory = mkdtempSync(join(tmpdir(), 'cdev-codex-install-'));
94
+ try {
95
+ const userRulesPath = join(
96
+ homeDirectory,
97
+ DEFAULT_CODEX_DIRECTORY_NAME,
98
+ CODEX_RULES_DIRECTORY_NAME,
99
+ 'default.rules',
100
+ );
101
+ mkdirSync(dirname(userRulesPath), { recursive: true });
102
+ writeFileSync(userRulesPath, 'prefix_rule(pattern=["echo", "hi"], decision="allow")\n');
103
+
104
+ runInstaller(homeDirectory, []);
105
+
106
+ const installedRulesPath = join(
107
+ homeDirectory,
108
+ DEFAULT_CODEX_DIRECTORY_NAME,
109
+ CODEX_RULES_DIRECTORY_NAME,
110
+ CODEX_RULES_SHIPPED_FILE_NAME,
111
+ );
112
+ assert.equal(existsSync(installedRulesPath), true);
113
+ assert.equal(
114
+ readFileSync(installedRulesPath, 'utf8'),
115
+ readFileSync(SHIPPED_RULES_SOURCE_PATH, 'utf8'),
116
+ );
117
+ assert.equal(
118
+ readFileSync(userRulesPath, 'utf8'),
119
+ 'prefix_rule(pattern=["echo", "hi"], decision="allow")\n',
120
+ );
121
+ } finally {
122
+ rmSync(homeDirectory, { recursive: true, force: true });
123
+ }
124
+ });
125
+
126
+ test('a full install Codex rules file contains no personal home path', () => {
127
+ const shippedText = readFileSync(SHIPPED_RULES_SOURCE_PATH, 'utf8');
128
+ assert.equal(shippedText.includes('Users\\jon'), false);
129
+ assert.equal(shippedText.includes('Users/jon'), false);
130
+ assert.equal(shippedText.includes('JonEcho'), false);
131
+ });
132
+
133
+ test('--only journal skips Codex rules; --only core copies them', () => {
134
+ const homeDirectory = mkdtempSync(join(tmpdir(), 'cdev-codex-groups-'));
135
+ try {
136
+ const installedRulesPath = join(
137
+ homeDirectory,
138
+ DEFAULT_CODEX_DIRECTORY_NAME,
139
+ CODEX_RULES_DIRECTORY_NAME,
140
+ CODEX_RULES_SHIPPED_FILE_NAME,
141
+ );
142
+ runInstaller(homeDirectory, ['--only', 'journal']);
143
+ assert.equal(existsSync(installedRulesPath), false);
144
+
145
+ runInstaller(homeDirectory, ['--only', 'core']);
146
+ assert.equal(existsSync(installedRulesPath), true);
147
+ } finally {
148
+ rmSync(homeDirectory, { recursive: true, force: true });
149
+ }
150
+ });
151
+
152
+ test('uninstall removes the shipped Codex rules file and leaves default.rules', () => {
153
+ const homeDirectory = mkdtempSync(join(tmpdir(), 'cdev-codex-uninstall-'));
154
+ try {
155
+ const rulesDirectory = join(
156
+ homeDirectory,
157
+ DEFAULT_CODEX_DIRECTORY_NAME,
158
+ CODEX_RULES_DIRECTORY_NAME,
159
+ );
160
+ const userRulesPath = join(rulesDirectory, 'default.rules');
161
+ mkdirSync(rulesDirectory, { recursive: true });
162
+ writeFileSync(userRulesPath, 'keep-me\n');
163
+
164
+ runInstaller(homeDirectory, []);
165
+ runInstaller(homeDirectory, ['--uninstall']);
166
+
167
+ const installedRulesPath = join(rulesDirectory, CODEX_RULES_SHIPPED_FILE_NAME);
168
+ assert.equal(existsSync(installedRulesPath), false);
169
+ assert.equal(readFileSync(userRulesPath, 'utf8'), 'keep-me\n');
170
+ } finally {
171
+ rmSync(homeDirectory, { recursive: true, force: true });
172
+ }
173
+ });
@@ -0,0 +1,103 @@
1
+ import { test } from 'node:test';
2
+ import { strict as assert } from 'node:assert';
3
+ import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, existsSync, rmSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { dirname, join } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { execFileSync } from 'node:child_process';
8
+ import {
9
+ resolveInstallRoot,
10
+ isAllowedInstallDestination,
11
+ } from './resolve-install-root.mjs';
12
+ import {
13
+ DEFAULT_CURSOR_DIRECTORY_NAME,
14
+ CURSOR_RULES_DIRECTORY_NAME,
15
+ } from './install-constants.mjs';
16
+
17
+ const THIS_DIRECTORY = dirname(fileURLToPath(import.meta.url));
18
+ const INSTALLER_PATH = join(THIS_DIRECTORY, 'install.mjs');
19
+ const PACKAGE_DIRECTORY = dirname(THIS_DIRECTORY);
20
+
21
+ function runInstaller(homeDirectory, extraArguments) {
22
+ return execFileSync('node', [INSTALLER_PATH, ...extraArguments], {
23
+ cwd: PACKAGE_DIRECTORY,
24
+ encoding: 'utf8',
25
+ env: {
26
+ ...process.env,
27
+ HOME: homeDirectory,
28
+ USERPROFILE: homeDirectory,
29
+ GIT_CONFIG_GLOBAL: join(homeDirectory, '.gitconfig'),
30
+ },
31
+ });
32
+ }
33
+
34
+ test('resolveInstallRoot names ~/.cursor/rules and allows generated mdc files under it', () => {
35
+ const homeDirectory = join(tmpdir(), 'cdev-cursor-rules-home');
36
+ const resolution = resolveInstallRoot({
37
+ homeDirectory,
38
+ environment: {},
39
+ explicitTarget: null,
40
+ });
41
+ const expectedDirectory = join(
42
+ homeDirectory,
43
+ DEFAULT_CURSOR_DIRECTORY_NAME,
44
+ CURSOR_RULES_DIRECTORY_NAME,
45
+ );
46
+ assert.equal(resolution.cursorRulesInstallDirectory, expectedDirectory);
47
+ assert.equal(
48
+ isAllowedInstallDestination(join(expectedDirectory, 'plain-language.mdc'), resolution),
49
+ true,
50
+ );
51
+ assert.equal(
52
+ isAllowedInstallDestination(join(homeDirectory, '.ssh', 'id_rsa'), resolution),
53
+ false,
54
+ );
55
+ });
56
+
57
+ test('a full install writes stem-named Cursor rules and leaves a local extra mdc in place', () => {
58
+ const homeDirectory = mkdtempSync(join(tmpdir(), 'cdev-cursor-install-'));
59
+ try {
60
+ const extraRulePath = join(
61
+ homeDirectory,
62
+ DEFAULT_CURSOR_DIRECTORY_NAME,
63
+ CURSOR_RULES_DIRECTORY_NAME,
64
+ 'user-local.mdc',
65
+ );
66
+ mkdirSync(dirname(extraRulePath), { recursive: true });
67
+ writeFileSync(extraRulePath, 'keep-me\n');
68
+
69
+ runInstaller(homeDirectory, []);
70
+
71
+ const generatedPath = join(
72
+ homeDirectory,
73
+ DEFAULT_CURSOR_DIRECTORY_NAME,
74
+ CURSOR_RULES_DIRECTORY_NAME,
75
+ 'plain-language.mdc',
76
+ );
77
+ assert.equal(existsSync(generatedPath), true);
78
+ const generatedText = readFileSync(generatedPath, 'utf8');
79
+ assert.equal(generatedText.includes('alwaysApply: true'), true);
80
+ assert.equal(readFileSync(extraRulePath, 'utf8'), 'keep-me\n');
81
+ } finally {
82
+ rmSync(homeDirectory, { recursive: true, force: true });
83
+ }
84
+ });
85
+
86
+ test('--only journal skips Cursor rule generation; --only core writes them', () => {
87
+ const homeDirectory = mkdtempSync(join(tmpdir(), 'cdev-cursor-groups-'));
88
+ try {
89
+ const generatedPath = join(
90
+ homeDirectory,
91
+ DEFAULT_CURSOR_DIRECTORY_NAME,
92
+ CURSOR_RULES_DIRECTORY_NAME,
93
+ 'plain-language.mdc',
94
+ );
95
+ runInstaller(homeDirectory, ['--only', 'journal']);
96
+ assert.equal(existsSync(generatedPath), false);
97
+
98
+ runInstaller(homeDirectory, ['--only', 'core']);
99
+ assert.equal(existsSync(generatedPath), true);
100
+ } finally {
101
+ rmSync(homeDirectory, { recursive: true, force: true });
102
+ }
103
+ });