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/bin/install.mjs CHANGED
@@ -22,10 +22,14 @@ import {
22
22
  MANAGED_SKILLS_DIRECTORY_NAME,
23
23
  MANAGED_HOOKS_DIRECTORY_NAME,
24
24
  SETTINGS_FILE_NAME,
25
+ CODEX_RULES_PACKAGE_DIRECTORY_NAME,
26
+ CURSOR_SYNC_SCRIPT_FILE_NAME,
27
+ WINDOWS_PYTHON_LAUNCHER_COMMAND,
25
28
  } from './install-constants.mjs';
26
29
  import {
27
30
  resolveInstallRoot,
28
31
  parseExplicitTargetFromArgv,
32
+ isAllowedInstallDestination,
29
33
  } from './resolve-install-root.mjs';
30
34
  import {
31
35
  parseInstallTargetSelectionFromArgv,
@@ -211,7 +215,7 @@ function discoverDependencyGroups() {
211
215
  readFileSync(join(dependencyRoot, 'package.json'), 'utf8')
212
216
  );
213
217
  const groupName = dependencyPackageJson.claudeDevEnv?.groupName
214
- || dependencyName.replace(/^@[^/]+\//, '');
218
+ || dependencyName.replace(new RegExp('^@[^/]+/'), '');
215
219
  const group = {
216
220
  description: dependencyPackageJson.description || dependencyName,
217
221
  packageRoot: dependencyRoot,
@@ -257,6 +261,7 @@ export const INSTALL_GROUPS = {
257
261
  skills: CORE_SKILLS,
258
262
  includeDirectories: CORE_INCLUDE_DIRECTORIES,
259
263
  includeAllHooks: true,
264
+ includeCodexRules: true,
260
265
  },
261
266
  journal: {
262
267
  description: 'Session logging and memory',
@@ -302,6 +307,75 @@ export function isWindowsStorePythonStub(executablePath) {
302
307
  return /[\\/]windowsapps[\\/]/i.test(executablePath);
303
308
  }
304
309
 
310
+ /**
311
+ * Split a stored Python command into an executable and prefix arguments.
312
+ *
313
+ * Args:
314
+ * pythonCommand: The command the installer detected (`py -3`, `python3`, or a path).
315
+ *
316
+ * Returns:
317
+ * `{ file, prefixArguments }` for `execFileSync`.
318
+ */
319
+ export function pythonFileAndPrefixArguments(pythonCommand) {
320
+ if (pythonCommand === WINDOWS_PYTHON_LAUNCHER_COMMAND) {
321
+ return { file: 'py', prefixArguments: ['-3'] };
322
+ }
323
+ const unquoted = pythonCommand.replace(/^"(.*)"$/, '$1');
324
+ return { file: unquoted, prefixArguments: [] };
325
+ }
326
+
327
+ /**
328
+ * Read generated Cursor paths from the sync manifest under a Cursor home.
329
+ *
330
+ * Args:
331
+ * cursorRoot: Absolute `.cursor` directory.
332
+ *
333
+ * Returns:
334
+ * Absolute paths the installer may record, including the sync manifest.
335
+ */
336
+ export function collectManagedCursorSyncPaths(cursorRoot) {
337
+ const manifestPath = join(cursorRoot, '.sync-manifest.json');
338
+ if (!existsSync(manifestPath)) return [];
339
+ const parsed = JSON.parse(readFileSync(manifestPath, 'utf8'));
340
+ const generatedPaths = [manifestPath];
341
+ const allEntries = { ...(parsed.entries || {}), ...(parsed.docs_entries || {}) };
342
+ for (const eachRelativePath of Object.keys(allEntries)) {
343
+ generatedPaths.push(join(cursorRoot, eachRelativePath));
344
+ }
345
+ return generatedPaths;
346
+ }
347
+
348
+ /**
349
+ * Run the installed Cursor rule generator against Claude and Cursor roots.
350
+ *
351
+ * Args:
352
+ * pythonCommand: Interpreter command from install preflight.
353
+ * scriptPath: Absolute `sync_to_cursor.py` path.
354
+ * claudeRoot: Managed Claude root that holds `rules/` and `docs/`.
355
+ * cursorRoot: Cursor home that receives `rules/*.mdc`.
356
+ *
357
+ * Returns:
358
+ * void
359
+ */
360
+ export function runCursorRuleSync(pythonCommand, scriptPath, claudeRoot, cursorRoot) {
361
+ mkdirSync(cursorRoot, { recursive: true });
362
+ const { file, prefixArguments } = pythonFileAndPrefixArguments(pythonCommand);
363
+ execFileSync(
364
+ file,
365
+ [
366
+ ...prefixArguments,
367
+ scriptPath,
368
+ '--force',
369
+ '--quiet',
370
+ '--claude-root',
371
+ claudeRoot,
372
+ '--cursor-root',
373
+ cursorRoot,
374
+ ],
375
+ { stdio: 'inherit' },
376
+ );
377
+ }
378
+
305
379
  /**
306
380
  * Formats an absolute interpreter path as a settings.json hook command prefix:
307
381
  * forward-slash separators, double-quoted when the path contains a space so the
@@ -629,8 +703,7 @@ function isManagedPath(candidatePath, managedHomeDirectory = CLAUDE_HOME) {
629
703
  * @returns {boolean} True when the installer itself writes the path.
630
704
  */
631
705
  function isRemovableManifestRecord(candidatePath) {
632
- if (isManagedPath(candidatePath)) return true;
633
- return comparisonKeyForPath(candidatePath) === comparisonKeyForPath(MYPY_INI_INSTALL_PATH);
706
+ return isAllowedInstallDestination(candidatePath, INSTALL_ROOT_RESOLUTION);
634
707
  }
635
708
 
636
709
  /**
@@ -650,6 +723,10 @@ function owningManagedRoot(installedFilePath) {
650
723
  const managedRoot = join(CLAUDE_HOME, directoryName);
651
724
  if (isInsideDirectory(resolvedPath, managedRoot)) return managedRoot;
652
725
  }
726
+ const codexRulesDirectory = INSTALL_ROOT_RESOLUTION.codexRulesInstallDirectory;
727
+ if (isInsideDirectory(resolvedPath, codexRulesDirectory)) return codexRulesDirectory;
728
+ const cursorInstallDirectory = INSTALL_ROOT_RESOLUTION.cursorInstallDirectory;
729
+ if (isInsideDirectory(resolvedPath, cursorInstallDirectory)) return cursorInstallDirectory;
653
730
  return null;
654
731
  }
655
732
 
@@ -1856,7 +1933,7 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
1856
1933
  `${PACKAGE_NAME}: --update — removing prior managed files under ${CLAUDE_HOME}, then reinstalling from the package.\n`,
1857
1934
  );
1858
1935
  purgeManagedInstallation({
1859
- requireManifest: false,
1936
+ isManifestRequired: false,
1860
1937
  throwIfFault,
1861
1938
  });
1862
1939
  } else if (isUpdateRefresh) {
@@ -1940,6 +2017,30 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
1940
2017
  }
1941
2018
  }
1942
2019
  }
2020
+ const shouldInstallCodexRules = !selectedGroups
2021
+ || activeGroups.some((eachGroup) => eachGroup.includeCodexRules);
2022
+ if (shouldInstallCodexRules) {
2023
+ const sourceDirectory = join(PACKAGE_ROOT, CODEX_RULES_PACKAGE_DIRECTORY_NAME);
2024
+ if (existsSync(sourceDirectory)) {
2025
+ const destinationDirectory = INSTALL_ROOT_RESOLUTION.codexRulesInstallDirectory;
2026
+ const stats = copyTree(sourceDirectory, destinationDirectory);
2027
+ summary.codexRules = stats;
2028
+ allInstalledFiles.push(...stats.paths);
2029
+ }
2030
+ }
2031
+ const shouldInstallCursorRules = !selectedGroups
2032
+ || activeGroups.some((eachGroup) => (eachGroup.includeDirectories || []).includes('rules'));
2033
+ if (shouldInstallCursorRules) {
2034
+ const scriptPath = join(CLAUDE_HOME, 'scripts', CURSOR_SYNC_SCRIPT_FILE_NAME);
2035
+ if (!existsSync(scriptPath)) {
2036
+ throw new Error(`cursor rule sync script missing: ${scriptPath}`);
2037
+ }
2038
+ const cursorRoot = dirname(INSTALL_ROOT_RESOLUTION.cursorRulesInstallDirectory);
2039
+ runCursorRuleSync(pythonCommand, scriptPath, CLAUDE_HOME, cursorRoot);
2040
+ const generatedCursorPaths = collectManagedCursorSyncPaths(cursorRoot);
2041
+ allInstalledFiles.push(...generatedCursorPaths);
2042
+ summary.cursorRules = { created: generatedCursorPaths.length, updated: 0, paths: generatedCursorPaths };
2043
+ }
1943
2044
  let skillsCreated = 0;
1944
2045
  let skillsUpdated = 0;
1945
2046
  const skillPaths = [];
@@ -2136,6 +2237,14 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
2136
2237
  console.log(` ${directory}: ${created + updated} files (${created} new, ${updated} updated)`);
2137
2238
  }
2138
2239
  }
2240
+ if (summary.codexRules) {
2241
+ const { created, updated } = summary.codexRules;
2242
+ console.log(` ${CODEX_RULES_PACKAGE_DIRECTORY_NAME}: ${created + updated} files (${created} new, ${updated} updated)`);
2243
+ }
2244
+ if (summary.cursorRules) {
2245
+ const { created } = summary.cursorRules;
2246
+ console.log(` cursor-rules: ${created} generated files`);
2247
+ }
2139
2248
  if (summary.skills) {
2140
2249
  const { created, updated, pruned } = summary.skills;
2141
2250
  const staleClause = pruned > 0 ? `, ${pruned} stale moved aside` : '';
@@ -2222,14 +2331,14 @@ function removeRecordedFile(filePath) {
2222
2331
  /**
2223
2332
  * Build the uninstall plan for this managed root.
2224
2333
  *
2225
- * @param {boolean} requireManifest
2334
+ * @param {boolean} isManifestRequired
2226
2335
  * @returns {ReturnType<typeof buildUninstallPlan>}
2227
2336
  */
2228
- function resolveUninstallPlan(requireManifest) {
2337
+ function resolveUninstallPlan(isManifestRequired) {
2229
2338
  return buildUninstallPlan({
2230
2339
  managedRoot: CLAUDE_HOME,
2231
2340
  manifestFilePath: MANIFEST_FILE,
2232
- requireManifest,
2341
+ requireManifest: isManifestRequired,
2233
2342
  isRemovableRecord: isRemovableManifestRecord,
2234
2343
  });
2235
2344
  }
@@ -2266,20 +2375,20 @@ function executeUninstallPlan(plan, helpers = {}) {
2266
2375
  }
2267
2376
  if (plan.skippedFiles.length > 0) {
2268
2377
  console.warn(
2269
- ` ${plan.skippedFiles.length} manifest record(s) skipped — each names a path outside ${CLAUDE_HOME} and outside ${MYPY_INI_INSTALL_PATH}`,
2378
+ ` ${plan.skippedFiles.length} manifest record(s) skipped — each names a path outside ${CLAUDE_HOME}, outside ${MYPY_INI_INSTALL_PATH}, outside ${INSTALL_ROOT_RESOLUTION.codexRulesInstallDirectory}, and outside ${INSTALL_ROOT_RESOLUTION.cursorInstallDirectory}`,
2270
2379
  );
2271
2380
  }
2272
2381
  throwIfFault(FAULT_PHASES.AFTER_FILE_STAGING);
2273
2382
 
2274
2383
  if (existsSync(plan.settingsPath)) {
2275
2384
  const settings = JSON.parse(readFileSync(plan.settingsPath, 'utf8'));
2276
- let settingsChanged = false;
2385
+ let didSettingsChange = false;
2277
2386
  if (settings.hooks) {
2278
2387
  const managedHookRelativePaths = managedHookScriptRelativePathsFromSourceRoots(
2279
2388
  managedPackageSourceRoots(),
2280
2389
  );
2281
2390
  pruneManagedHooksFromSettings(settings, managedHookRelativePaths);
2282
- settingsChanged = true;
2391
+ didSettingsChange = true;
2283
2392
  console.log(' Hook entries removed from settings.json');
2284
2393
  }
2285
2394
  const managedDenyFromPlan = plan.managedPermissionDenyEntries.length > 0
@@ -2288,13 +2397,13 @@ function executeUninstallPlan(plan, helpers = {}) {
2288
2397
  if (managedDenyFromPlan.length > 0) {
2289
2398
  const pruneOutcome = pruneManagedPermissionsFromSettings(settings, managedDenyFromPlan);
2290
2399
  if (pruneOutcome.removedCount > 0) {
2291
- settingsChanged = true;
2400
+ didSettingsChange = true;
2292
2401
  console.log(
2293
2402
  ` Permission entries removed from settings.json: ${pruneOutcome.removedCount} managed deny(s)`,
2294
2403
  );
2295
2404
  }
2296
2405
  }
2297
- if (settingsChanged) {
2406
+ if (didSettingsChange) {
2298
2407
  writeFileSync(plan.settingsPath, JSON.stringify(settings, null, 4) + '\n');
2299
2408
  }
2300
2409
  }
@@ -2327,13 +2436,13 @@ function executeUninstallPlan(plan, helpers = {}) {
2327
2436
  * without nesting a second journal.
2328
2437
  *
2329
2438
  * @param {{
2330
- * requireManifest: boolean,
2439
+ * isManifestRequired: boolean,
2331
2440
  * throwIfFault?: (phase: string) => void,
2332
2441
  * }} options
2333
2442
  * @returns {number|void} 0 when no manifest exists and none is required.
2334
2443
  */
2335
- function purgeManagedInstallation({ requireManifest, throwIfFault }) {
2336
- const plan = resolveUninstallPlan(requireManifest);
2444
+ function purgeManagedInstallation({ isManifestRequired, throwIfFault }) {
2445
+ const plan = resolveUninstallPlan(isManifestRequired);
2337
2446
  if (plan.isNoOp) {
2338
2447
  return 0;
2339
2448
  }
@@ -2433,6 +2542,8 @@ Examples:
2433
2542
 
2434
2543
  Install location: ~/.claude/ by default; CLAUDE_CONFIG_DIR or --target selects another managed root.
2435
2544
  Named profiles resolve under LLM_SETTINGS_PROFILES_ROOT or ~/.claude-profiles/<directoryName>.
2545
+ Codex exec-policy files copy into ~/.codex/rules, or CODEX_HOME/rules when CODEX_HOME is set.
2546
+ Cursor rule files generate into ~/.cursor/rules as stem-named mdc files, one per Claude rule.
2436
2547
 
2437
2548
  Root precedence: --target > CLAUDE_CONFIG_DIR > ~/.claude
2438
2549
  Profile selection (--profile/--profiles) is mutually exclusive with --target.
@@ -2564,7 +2675,7 @@ function runInstallForAllTargets(allTargets, childArgv) {
2564
2675
  * @returns {number | null}
2565
2676
  */
2566
2677
  function spawnInstallChild(target, childArgv) {
2567
- const result = spawnSync(
2678
+ const childProcess = spawnSync(
2568
2679
  process.execPath,
2569
2680
  [
2570
2681
  fileURLToPath(import.meta.url),
@@ -2579,11 +2690,11 @@ function spawnInstallChild(target, childArgv) {
2579
2690
  env: process.env,
2580
2691
  },
2581
2692
  );
2582
- if (result.error) {
2583
- console.error(`ERROR: failed to spawn install child: ${result.error.message}`);
2693
+ if (childProcess.error) {
2694
+ console.error(`ERROR: failed to spawn install child: ${childProcess.error.message}`);
2584
2695
  return 1;
2585
2696
  }
2586
- return result.status === null ? 1 : result.status;
2697
+ return childProcess.status === null ? 1 : childProcess.status;
2587
2698
  }
2588
2699
 
2589
2700
  if (invokedAsEntryPoint(import.meta.url, process.argv[1])) {
@@ -8,6 +8,7 @@ import { join, resolve } from 'node:path';
8
8
  import { mkdtempSync, rmSync, mkdirSync, readFileSync } from 'node:fs';
9
9
  import { tmpdir } from 'node:os';
10
10
  import { fileURLToPath } from 'node:url';
11
+ import { CODEX_RULES_SHIPPED_FILE_NAME } from './install-constants.mjs';
11
12
  import {
12
13
  resolveInstallRoot,
13
14
  isPathWithinManagedRoot,
@@ -90,6 +91,13 @@ test('declared external mypy.ini is allowed; unrelated external paths are not',
90
91
  isAllowedInstallDestination(join(resolution.managedRoot, 'hooks', 'x.py'), resolution),
91
92
  true,
92
93
  );
94
+ assert.equal(
95
+ isAllowedInstallDestination(
96
+ join(resolution.codexRulesInstallDirectory, CODEX_RULES_SHIPPED_FILE_NAME),
97
+ resolution,
98
+ ),
99
+ true,
100
+ );
93
101
  });
94
102
 
95
103
  test('parseExplicitTargetFromArgv reads --target and --target=', () => {
@@ -238,6 +238,7 @@ function resolveInstallerInvocation(homeDirectory, options) {
238
238
  HOME: homeDirectory,
239
239
  USERPROFILE: homeDirectory,
240
240
  GIT_CONFIG_GLOBAL: join(homeDirectory, '.gitconfig'),
241
+ CODEX_HOME: join(homeDirectory, '.codex'),
241
242
  };
242
243
  if (dependencyResolvable) {
243
244
  childEnvironment.NODE_PATH = ensureDependencyStub(homeDirectory);
@@ -1227,7 +1228,7 @@ test('an uninstall removes the home-directory .mypy.ini the install wrote and sk
1227
1228
  assert.equal(
1228
1229
  existsSync(mypyIniPath),
1229
1230
  false,
1230
- 'the uninstall removes the one file the install writes outside ~/.claude',
1231
+ 'the uninstall removes the mypy configuration the install writes outside ~/.claude',
1231
1232
  );
1232
1233
  assert.equal(
1233
1234
  installerOutput.includes(`skipping ${mypyIniPath}`),
@@ -1672,15 +1672,15 @@ test('copyTree copies AGENTS.md with agent definitions', () => {
1672
1672
  const destinationRoot = mkdtempSync(join(tmpdir(), 'cdev-copy-agents-destination-'));
1673
1673
  try {
1674
1674
  writeFileSync(join(sourceRoot, 'AGENTS.md'), '# Shared guidance\n');
1675
- const agentDefinitionPath = join(sourceRoot, 'docs-agent.md');
1675
+ const agentDefinitionPath = join(sourceRoot, 'clean-coder.md');
1676
1676
  writeFileSync(
1677
1677
  agentDefinitionPath,
1678
- '---\nname: docs-agent\ndescription: fixture agent\n---\n',
1678
+ '---\nname: clean-coder\ndescription: fixture agent\n---\n',
1679
1679
  );
1680
1680
 
1681
1681
  const copyStats = copyTree(sourceRoot, destinationRoot);
1682
1682
  const copiedAgentsPath = join(destinationRoot, 'AGENTS.md');
1683
- const copiedAgentPath = join(destinationRoot, 'docs-agent.md');
1683
+ const copiedAgentPath = join(destinationRoot, 'clean-coder.md');
1684
1684
 
1685
1685
  assert.equal(existsSync(copiedAgentsPath), true, 'the canonical instructions install');
1686
1686
  assert.equal(existsSync(copiedAgentPath), true, 'the real agent definition installs');
@@ -242,6 +242,7 @@ function runInstaller(homeDirectory, extraArguments, options = {}) {
242
242
  HOME: homeDirectory,
243
243
  USERPROFILE: homeDirectory,
244
244
  GIT_CONFIG_GLOBAL: join(homeDirectory, '.gitconfig'),
245
+ CODEX_HOME: join(homeDirectory, '.codex'),
245
246
  };
246
247
  if (options.faultPhase) {
247
248
  childEnvironment[INSTALL_FAULT_ENV] = options.faultPhase;
@@ -74,6 +74,7 @@ function runInstaller(homeDirectory, extraArguments, options = {}) {
74
74
  HOME: homeDirectory,
75
75
  USERPROFILE: homeDirectory,
76
76
  GIT_CONFIG_GLOBAL: join(homeDirectory, '.gitconfig'),
77
+ CODEX_HOME: join(homeDirectory, '.codex'),
77
78
  };
78
79
  if (options.faultPhase) {
79
80
  childEnvironment[INSTALL_FAULT_ENV] = options.faultPhase;
@@ -11,7 +11,14 @@
11
11
 
12
12
  import { homedir } from 'node:os';
13
13
  import { join, normalize, resolve, sep } from 'node:path';
14
- import { MYPY_INI_FILE_NAME } from './install-constants.mjs';
14
+ import {
15
+ MYPY_INI_FILE_NAME,
16
+ CODEX_HOME_ENVIRONMENT_VARIABLE,
17
+ DEFAULT_CODEX_DIRECTORY_NAME,
18
+ CODEX_RULES_DIRECTORY_NAME,
19
+ DEFAULT_CURSOR_DIRECTORY_NAME,
20
+ CURSOR_RULES_DIRECTORY_NAME,
21
+ } from './install-constants.mjs';
15
22
 
16
23
  export const CLAUDE_CONFIG_DIR_ENVIRONMENT_VARIABLE = 'CLAUDE_CONFIG_DIR';
17
24
  export const DEFAULT_CLAUDE_DIRECTORY_NAME = '.claude';
@@ -33,6 +40,10 @@ export const MANIFEST_FILE_NAME = '.claude-dev-env-manifest.json';
33
40
  * manifestFilePath: string,
34
41
  * mypyIniInstallPath: string,
35
42
  * allDeclaredExternalPaths: string[],
43
+ * allDeclaredExternalDirectories: string[],
44
+ * codexRulesInstallDirectory: string,
45
+ * cursorInstallDirectory: string,
46
+ * cursorRulesInstallDirectory: string,
36
47
  * }} InstallRootResolution
37
48
  */
38
49
 
@@ -69,6 +80,18 @@ export function resolveInstallRoot(options = {}) {
69
80
  }
70
81
 
71
82
  const mypyIniInstallPath = resolve(join(homeDirectory, MYPY_INI_FILE_NAME));
83
+ const codexHomeDirectory = normalizeOptionalPath(
84
+ environment[CODEX_HOME_ENVIRONMENT_VARIABLE],
85
+ ) ?? resolve(join(homeDirectory, DEFAULT_CODEX_DIRECTORY_NAME));
86
+ const codexRulesInstallDirectory = resolve(
87
+ join(codexHomeDirectory, CODEX_RULES_DIRECTORY_NAME),
88
+ );
89
+ const cursorInstallDirectory = resolve(
90
+ join(homeDirectory, DEFAULT_CURSOR_DIRECTORY_NAME),
91
+ );
92
+ const cursorRulesInstallDirectory = resolve(
93
+ join(cursorInstallDirectory, CURSOR_RULES_DIRECTORY_NAME),
94
+ );
72
95
  return {
73
96
  managedRoot,
74
97
  source,
@@ -76,6 +99,10 @@ export function resolveInstallRoot(options = {}) {
76
99
  manifestFilePath: join(managedRoot, MANIFEST_FILE_NAME),
77
100
  mypyIniInstallPath,
78
101
  allDeclaredExternalPaths: [mypyIniInstallPath],
102
+ allDeclaredExternalDirectories: [codexRulesInstallDirectory, cursorInstallDirectory],
103
+ codexRulesInstallDirectory,
104
+ cursorInstallDirectory,
105
+ cursorRulesInstallDirectory,
79
106
  };
80
107
  }
81
108
 
@@ -103,8 +130,9 @@ export function isPathWithinManagedRoot(candidatePath, managedRoot) {
103
130
  }
104
131
 
105
132
  /**
106
- * True when a write destination is allowed: inside the managed root or on
107
- * the declared external allowlist (today: ~/.mypy.ini under the home dir).
133
+ * True when a write destination is allowed: inside the managed root, the
134
+ * home-directory `.mypy.ini`, a file under the Codex rules directory, or a file
135
+ * under the Cursor rules directory.
108
136
  *
109
137
  * @param {string} candidatePath
110
138
  * @param {InstallRootResolution} resolution
@@ -115,8 +143,13 @@ export function isAllowedInstallDestination(candidatePath, resolution) {
115
143
  return true;
116
144
  }
117
145
  const normalizedCandidate = normalizePathForComparison(candidatePath);
118
- return resolution.allDeclaredExternalPaths.some(
146
+ if (resolution.allDeclaredExternalPaths.some(
119
147
  (eachExternalPath) => normalizePathForComparison(eachExternalPath) === normalizedCandidate,
148
+ )) {
149
+ return true;
150
+ }
151
+ return (resolution.allDeclaredExternalDirectories ?? []).some(
152
+ (eachExternalDirectory) => isPathWithinManagedRoot(candidatePath, eachExternalDirectory),
120
153
  );
121
154
  }
122
155
 
@@ -130,18 +163,18 @@ export function parseExplicitTargetFromArgv(argv) {
130
163
  for (let index = 0; index < argv.length; index += 1) {
131
164
  const token = argv[index];
132
165
  if (token === '--target') {
133
- const value = argv[index + 1];
134
- if (!value || value.startsWith('--')) {
166
+ const targetPath = argv[index + 1];
167
+ if (!targetPath || targetPath.startsWith('--')) {
135
168
  throw new Error('--target requires a path argument');
136
169
  }
137
- return value;
170
+ return targetPath;
138
171
  }
139
172
  if (token.startsWith('--target=')) {
140
- const value = token.slice('--target='.length);
141
- if (!value) {
173
+ const targetPath = token.slice('--target='.length);
174
+ if (!targetPath) {
142
175
  throw new Error('--target requires a path argument');
143
176
  }
144
- return value;
177
+ return targetPath;
145
178
  }
146
179
  }
147
180
  return null;
@@ -0,0 +1,12 @@
1
+ # Shared Codex exec-policy prefix rules shipped by claude-dev-env.
2
+ # Codex loads every *.rules file under $CODEX_HOME/rules (default ~/.codex/rules).
3
+ # This file is named claude-dev-env.rules so a local default.rules stays in place.
4
+ # Patterns use relative repo paths only. Personal home paths stay out of this file.
5
+
6
+ prefix_rule(pattern=["gemini", "--version"], decision="allow")
7
+ prefix_rule(pattern=["git", "status", "--short", "--branch"], decision="allow")
8
+ prefix_rule(pattern=["git", "status", "--short"], decision="allow")
9
+ prefix_rule(pattern=["git", "diff", "--", "packages/claude-dev-env/_shared/pr-loop/scripts/code_rules_gate_parts", "packages/claude-dev-env/_shared/pr-loop/scripts/tests/test_code_rules_gate.py"], decision="allow")
10
+ prefix_rule(pattern=["Get-Content", "-LiteralPath", "packages/claude-dev-env/_shared/pr-loop/scripts/code_rules_gate_parts/tests/_repo_test_helpers.py"], decision="allow")
11
+ prefix_rule(pattern=["rg", "-n", "-i", "--glob", "!**/__pycache__/**", "verified_commit_gate|verifier_verdict_minter|verdict_directory_write_blocker|verify-skip|code-verifier.*commit|commit.*code-verifier", "packages/claude-dev-env"], decision="allow")
12
+ prefix_rule(pattern=["python", "-m", "pytest", "-q", "packages/claude-dev-env/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py", "packages/claude-dev-env/hooks/blocking/test_bash_pre_tool_use_dispatcher.py", "packages/claude-dev-env/hooks/blocking/test_pre_tool_use_dispatcher.py", "packages/claude-dev-env/hooks/blocking/test_verdict_directory_write_blocker.py"], decision="allow")
@@ -6,17 +6,7 @@ Slash-command definitions installed into `~/.claude/commands/` by `bin/install.m
6
6
 
7
7
  | File | Command | What it does |
8
8
  |---|---|---|
9
- | `commit.md` | `/commit` | Commits and pushes changes to GitHub |
10
- | `docupdate.md` | `/docupdate` | Updates documentation to match current code state |
11
- | `hook-log-extract.md` | `/hook-log-extract` | Extracts and formats hook log entries for a session |
12
- | `hook-log-init.md` | `/hook-log-init` | Initializes the Neon Postgres schema that backs the hook-log extractor (one-time per machine) |
13
- | `implement.md` | `/implement` | Provides full implementation context to a right-sized engineer in XML format |
14
- | `plan.md` | `/plan` | Plans a feature through the `anthropic-plan` skill and workflow |
15
- | `pr-comments.md` | `/pr-comments` | Fetches and formats PR review comments for response |
16
- | `review-plan.md` | `/review-plan` | Reviews the current plan packet against code standards |
17
- | `right-size.md` | `/right-size` | Checks an implementation against the Right-Sized Engineering rules |
18
9
  | `sr-loop.md` | `/sr-loop` | Runs the converging cleanup loop: /simplify passes until clean, then a code-review fix pass |
19
- | `sum.md` | `/sum` | Generates a formatted session summary for quick pickup in a new session |
20
10
 
21
11
  ## Format
22
12
 
@@ -47,7 +47,7 @@ TABLE_WITH_SLASH_COMMAND_AND_SUBDIR = (
47
47
  "# example\n\n"
48
48
  "| Entry | Description |\n"
49
49
  "|---|---|\n"
50
- "| `/commit` | Slash command |\n"
50
+ "| `/sr-loop` | Slash command |\n"
51
51
  "| `scripts/` | A subdirectory |\n"
52
52
  "| Plain prose, no backticks | Not a file |\n"
53
53
  )
@@ -41,3 +41,35 @@ The `blocked_commands` view filters to `outcome = 'blocked'`.
41
41
  - Extractor and Stop wrapper mains are disabled and exit 0 with no work.
42
42
  - Constants for the extractor (table name, offset state file, timeout) live in `hooks_constants/hook_log_extractor_constants.py`.
43
43
  - Tests run with `python -m pytest diagnostic/test_hook_log_*.py`.
44
+
45
+ ## Schema init
46
+
47
+ Run `hook_log_init.py` once per machine, or after rotating the Neon project.
48
+
49
+ Prerequisites: Bitwarden Secrets Manager CLI (`bws`) on PATH; a machine-account
50
+ token in `BWS_ACCESS_TOKEN` for the user environment (`setx` on Windows, shell
51
+ profile on macOS/Linux); Neon connection string stored as
52
+ `NEON_HOOK_LOGS_DATABASE_URL`; Python deps from `requirements-hook-logs.txt`.
53
+
54
+ ```
55
+ bws run -- python packages/claude-dev-env/hooks/diagnostic/hook_log_init.py
56
+ ```
57
+
58
+ `bws run` strips `BWS_ACCESS_TOKEN` from the child environment so the Python
59
+ process never sees it. The script verifies `NEON_HOOK_LOGS_DATABASE_URL`,
60
+ connects with a 5-second timeout, applies `schema.sql` with idempotent DDL,
61
+ runs a sentinel insert/select/delete round-trip, and prints the Neon host,
62
+ table name, and row count.
63
+
64
+ ## Operator CLI flags
65
+
66
+ `hook_log_extractor.py` and `hook_log_stop_wrapper.py` mains exit 0 with no work.
67
+ The extractor body still documents these flags for a re-enable path:
68
+
69
+ - default / `--incremental`: resume from `~/.claude/logs/hooks/.state/offsets.json`
70
+ - `--full-rebuild`: clear offsets, truncate `hook_events`, re-read every JSONL
71
+ - `--summary`: print the top-10 blockers of the last 24 hours
72
+ - `--query <name>`: run `queries/<name>.sql` (`top_blockers_overall`,
73
+ `top_blockers_last_24_hours`, `blocks_last_7_days`, `blocks_by_category`,
74
+ `blocks_by_tool`, `block_details_for_hook`)
75
+
@@ -66,7 +66,7 @@ def verify_environment_variables() -> list[str]:
66
66
  child process invoked via ``bws run -- python hook_log_init.py``
67
67
  would therefore always fail even when the machine is configured
68
68
  correctly. The one-time ``setx BWS_ACCESS_TOKEN`` prerequisite is
69
- documented in ``packages/claude-dev-env/commands/hook-log-init.md``.
69
+ documented in ``packages/claude-dev-env/hooks/diagnostic/AGENTS.md``.
70
70
  """
71
71
  all_missing_variable_names: list[str] = []
72
72
  raw_database_url_value = os.environ.get(NEON_DATABASE_URL_ENVIRONMENT_VARIABLE)
@@ -172,7 +172,7 @@ def _print_missing_environment_variables(all_missing_variable_names: list[str])
172
172
 
173
173
 
174
174
  def main() -> int:
175
- """Entry point for the ``/hook-log-init`` slash command."""
175
+ """CLI entry for one-time Neon schema init."""
176
176
  all_missing_variable_names = verify_environment_variables()
177
177
  if all_missing_variable_names:
178
178
  _print_missing_environment_variables(all_missing_variable_names)
@@ -4,7 +4,7 @@ Output-style instruction files installed into `~/.claude/output-styles/` by `bin
4
4
 
5
5
  ## Files
6
6
 
7
- No output-style instruction files ship in this directory. The active caveman behavior lives in `agents/caveman.md`.
7
+ No output-style instruction files ship in this directory.
8
8
 
9
9
  ## Format
10
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "2.14.0",
3
+ "version": "2.15.0",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -26,6 +26,7 @@
26
26
  "installable-surfaces.manifest.json",
27
27
  "_shared/",
28
28
  "audit-rubrics/",
29
+ "codex-rules/",
29
30
  "AGENTS.md",
30
31
  "CLAUDE.md",
31
32
  "!**/__pycache__/**",
@@ -6,12 +6,12 @@ Python package that syncs Claude rules and docs to Cursor `.mdc` files. Entry po
6
6
 
7
7
  | File | Purpose |
8
8
  |---|---|
9
- | `engine.py` | Main sync logic: loads the manifest, builds rule mappings, hashes sources, writes `.mdc` files, and updates the manifest |
10
- | `rules.py` | Builds `RuleMapping` objects from Claude rule markdown files; applies transforms to fit Cursor's `.mdc` format |
9
+ | `engine.py` | Main sync logic: loads the manifest, builds rule mappings, hashes sources, writes `.mdc` files, and updates the manifest; `--claude-root` and `--cursor-root` select explicit layouts |
10
+ | `rules.py` | Builds `RuleMapping` objects from Claude rule markdown files; applies transforms to fit Cursor's `.mdc` format; maps remaining `rules/*.md` files to `<stem>.mdc` |
11
11
  | `canonical_docs.py` | Checks and syncs canonical documentation files (`CODE_RULES.md`, `TEST_QUALITY.md`) to the Cursor rules directory |
12
12
  | `paths.py` | Resolves the Claude and Cursor layout paths; respects the `LLM_SETTINGS_ROOT` env var for non-home layouts |
13
13
  | `hashing.py` | SHA-256 helpers that detect whether source files changed since the last sync run |
14
- | `config.py` | Package-level constants: `GENERATOR_VERSION`, `CANONICAL_DOC_FILES`, `MAX_RULE_BODY_LINES` |
14
+ | `config/` | Package-level constants: `GENERATOR_VERSION`, `ALL_CANONICAL_DOC_FILES`, `MAX_RULE_BODY_LINES`, skipped inventory filenames, markdown suffix |
15
15
  | `__init__.py` | Empty package marker |
16
16
 
17
17
  ## Layout resolution