claude-dev-env 8.11.1 → 8.13.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 (105) hide show
  1. package/.agents/skills/e-code-review/SKILL.md +24 -71
  2. package/.agents/skills/e-code-review/reference/fix.md +7 -78
  3. package/.agents/skills/e-code-review/reference/high.md +129 -0
  4. package/.agents/skills/e-code-review/reference/low.md +20 -51
  5. package/.agents/skills/e-code-review/reference/max.md +169 -0
  6. package/.agents/skills/e-code-review/reference/medium.md +7 -191
  7. package/.agents/skills/e-code-review/reference/xhigh.md +47 -91
  8. package/.agents/skills/pull-request/scripts/test_pull_request.py +1 -1
  9. package/.agents/skills-archived/pr-converge/scripts/test_check_convergence.py +4 -4
  10. package/_shared/advisor/reference/astra-rung.md +7 -7
  11. package/_shared/advisor/reference/third-party-bind.md +2 -2
  12. package/_shared/advisor/scripts/codex_astra_advisor.py +33 -26
  13. package/_shared/advisor/scripts/codex_astra_preflight.py +57 -55
  14. package/_shared/advisor/scripts/config/advisor_scripts_constants/astra_advisor_constants.py +8 -8
  15. package/_shared/advisor/scripts/tests/test_codex_astra_advisor.py +69 -23
  16. package/_shared/advisor/scripts/tests/test_codex_astra_preflight.py +4 -4
  17. package/_shared/pr-loop/scripts/check_convergence.py +54 -49
  18. package/_shared/pr-loop/scripts/codex_review_scripts_constants/run_constants.py +4 -0
  19. package/_shared/pr-loop/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +10 -1
  20. package/_shared/pr-loop/scripts/run_codex_review.py +4 -6
  21. package/_shared/pr-loop/scripts/test_check_convergence.py +6 -6
  22. package/_shared/pr-loop/scripts/test_check_convergence_codex.py +73 -31
  23. package/_shared/pr-loop/scripts/test_check_convergence_contract.py +2 -2
  24. package/_shared/pr-loop/scripts/tests/test__claude_permissions_common.py +2 -2
  25. package/bin/git_hooks_installer.test.mjs +1 -1
  26. package/bin/install-constants.mjs +1 -6
  27. package/bin/install.agents-home.test.mjs +9 -9
  28. package/bin/install.cursor-rules.test.mjs +24 -51
  29. package/bin/install.mjs +16 -70
  30. package/commands/sr-loop.md +3 -1
  31. package/docs/codex-accounts.md +79 -0
  32. package/hooks/blocking/test_duplicate_rmtree_helper_blocker.py +5 -5
  33. package/hooks/blocking/test_pii_scanner.py +1 -1
  34. package/hooks/blocking/test_pytest_testpaths_orphan_blocker.py +2 -2
  35. package/hooks/git-hooks/conftest.py +16 -0
  36. package/hooks/git-hooks/git_hooks_constants/verification_notice_constants.py +1 -1
  37. package/hooks/git-hooks/test_post_commit.py +1 -1
  38. package/hooks/git-hooks/test_pre_commit.py +1 -1
  39. package/hooks/git-hooks/test_pre_push.py +2 -2
  40. package/hooks/git-hooks/test_verification_start.py +1 -1
  41. package/hooks/git-hooks/verification_notice.py +2 -2
  42. package/hooks/git-hooks/verification_notice_context.py +21 -2
  43. package/hooks/git-hooks/verification_start.py +2 -3
  44. package/hooks/hooks_constants/code_rules_path_utils_constants.py +0 -1
  45. package/package.json +1 -1
  46. package/rules/durable-post-artifacts.md +4 -3
  47. package/scripts/claude_account_profile.py +21 -9
  48. package/scripts/codex_account_choice.py +396 -0
  49. package/scripts/codex_account_meters.py +372 -0
  50. package/scripts/dev_env_scripts_constants/codex_account_constants.py +141 -0
  51. package/scripts/dev_env_scripts_constants/private_term_constants.py +17 -2
  52. package/scripts/private_term_scan.py +6 -2
  53. package/scripts/repository_checks/config/constants.py +1 -0
  54. package/scripts/repository_checks/tracked_private_terms.py +7 -0
  55. package/scripts/test_claude_account_profile.py +12 -0
  56. package/scripts/test_codex_account_choice.py +276 -0
  57. package/scripts/test_codex_account_meters.py +205 -0
  58. package/scripts/tests/test_automatic_advisory.py +3 -3
  59. package/scripts/tests/test_automatic_advisory_polling.py +1 -1
  60. package/scripts/tests/test_automatic_advisory_recovery.py +1 -1
  61. package/scripts/tests/test_closed_pr_label.py +1 -1
  62. package/scripts/tests/test_private_term_scan.py +28 -0
  63. package/scripts/tests/test_review_closure.py +2 -2
  64. package/scripts/tests/test_review_closure_model.py +2 -2
  65. package/scripts/tests/test_rules.py +2 -83
  66. package/scripts/tests/test_tracked_private_terms.py +15 -0
  67. package/scripts/tests/test_verification_notice.py +29 -13
  68. package/.agents/skills/cert-classification-rule/SKILL.md +0 -224
  69. package/.agents/skills/cert-classification-rule/scripts/resort_cert_folders.py +0 -158
  70. package/.agents/skills/e-code-review/preflight-proposal.contract.test.mjs +0 -20
  71. package/.agents/skills/e-code-review/reference/effort-evaluation.md +0 -35
  72. package/.agents/skills/e-code-review/reference/loop.md +0 -261
  73. package/.agents/skills/e-code-review/reference/mode-contract.test.mjs +0 -67
  74. package/.agents/skills/e-code-review/reference/preflight-proposal.md +0 -75
  75. package/.agents/skills/e-code-review/reference/runner-selection.md +0 -40
  76. package/.agents/skills/e-code-review/scripts/config/e_code_review_effort_constants/__init__.py +0 -41
  77. package/.agents/skills/e-code-review/scripts/config/e_code_review_effort_constants/effort_constants.py +0 -40
  78. package/.agents/skills/e-code-review/scripts/e_code_review_scripts_constants/__init__.py +0 -1
  79. package/.agents/skills/e-code-review/scripts/e_code_review_scripts_constants/finding_pipeline_constants.py +0 -49
  80. package/.agents/skills/e-code-review/scripts/e_code_review_scripts_constants/grok_code_review_constants.py +0 -55
  81. package/.agents/skills/e-code-review/scripts/effort_defaults_evidence.json +0 -186
  82. package/.agents/skills/e-code-review/scripts/effort_evaluation.py +0 -362
  83. package/.agents/skills/e-code-review/scripts/finding_pipeline.py +0 -140
  84. package/.agents/skills/e-code-review/scripts/fixtures/demanding.json +0 -26
  85. package/.agents/skills/e-code-review/scripts/fixtures/easy.json +0 -14
  86. package/.agents/skills/e-code-review/scripts/fixtures/medium.json +0 -20
  87. package/.agents/skills/e-code-review/scripts/grok_code_review.py +0 -230
  88. package/.agents/skills/e-code-review/scripts/test_effort_evaluation.py +0 -180
  89. package/.agents/skills/e-code-review/scripts/test_finding_pipeline.py +0 -197
  90. package/.agents/skills/e-code-review/scripts/test_grok_code_review.py +0 -289
  91. package/_shared/pr-loop/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +0 -82
  92. package/_shared/pr-loop/scripts/codex_usage_probe.py +0 -573
  93. package/_shared/pr-loop/scripts/test_codex_usage_probe.py +0 -683
  94. package/scripts/sync_to_cursor/AGENTS.md +0 -1
  95. package/scripts/sync_to_cursor/CLAUDE.md +0 -1
  96. package/scripts/sync_to_cursor/__init__.py +0 -13
  97. package/scripts/sync_to_cursor/canonical_docs.py +0 -66
  98. package/scripts/sync_to_cursor/config/__init__.py +0 -8
  99. package/scripts/sync_to_cursor/engine.py +0 -219
  100. package/scripts/sync_to_cursor/hashing.py +0 -7
  101. package/scripts/sync_to_cursor/paths.py +0 -18
  102. package/scripts/sync_to_cursor/rules.py +0 -494
  103. package/scripts/sync_to_cursor.py +0 -22
  104. package/scripts/tests/test_engine.py +0 -101
  105. package/scripts/tests/test_sync_to_cursor.py +0 -315
@@ -15,7 +15,6 @@ import { fileURLToPath } from 'node:url';
15
15
  import { execFileSync } from 'node:child_process';
16
16
  import {
17
17
  resolveInstallRoot,
18
- isAllowedInstallDestination,
19
18
  } from './resolve-install-root.mjs';
20
19
  import {
21
20
  DEFAULT_CURSOR_DIRECTORY_NAME,
@@ -43,78 +42,54 @@ function runInstaller(homeDirectory, extraArguments, environmentOverrides = {})
43
42
  });
44
43
  }
45
44
 
46
- test('resolveInstallRoot names ~/.cursor/rules and allows generated mdc files under it', () => {
47
- const homeDirectory = join(tmpdir(), 'cdev-cursor-rules-home');
48
- const resolution = resolveInstallRoot({
49
- homeDirectory,
50
- environment: {},
51
- explicitTarget: null,
52
- });
53
- const expectedDirectory = join(
54
- homeDirectory,
55
- DEFAULT_CURSOR_DIRECTORY_NAME,
56
- CURSOR_RULES_DIRECTORY_NAME,
57
- );
58
- assert.equal(resolution.cursorRulesInstallDirectory, expectedDirectory);
59
- assert.equal(
60
- isAllowedInstallDestination(join(expectedDirectory, 'asd-ste100-language.mdc'), resolution),
61
- true,
62
- );
63
- assert.equal(
64
- isAllowedInstallDestination(join(homeDirectory, '.ssh', 'id_rsa'), resolution),
65
- false,
66
- );
67
- });
68
-
69
- test('a full install writes stem-named Cursor rules and leaves a local extra mdc in place', () => {
45
+ test('a reinstall moves the Cursor rules an older install generated and keeps a local mdc', () => {
70
46
  const homeDirectory = mkdtempSync(join(tmpdir(), 'cdev-cursor-install-'));
71
47
  try {
72
- const extraRulePath = join(
73
- homeDirectory,
74
- DEFAULT_CURSOR_DIRECTORY_NAME,
75
- CURSOR_RULES_DIRECTORY_NAME,
76
- 'user-local.mdc',
77
- );
78
- mkdirSync(dirname(extraRulePath), { recursive: true });
79
- writeFileSync(extraRulePath, 'keep-me\n');
48
+ runInstaller(homeDirectory, []);
49
+ const cursorDirectory = join(homeDirectory, DEFAULT_CURSOR_DIRECTORY_NAME);
50
+ const rulesDirectory = join(cursorDirectory, CURSOR_RULES_DIRECTORY_NAME);
51
+ const generatedRulePath = join(rulesDirectory, 'asd-ste100-language.mdc');
52
+ const syncManifestPath = join(cursorDirectory, '.sync-manifest.json');
53
+ const localRulePath = join(rulesDirectory, 'user-local.mdc');
54
+ mkdirSync(rulesDirectory, { recursive: true });
55
+ writeFileSync(generatedRulePath, 'generated\n');
56
+ writeFileSync(syncManifestPath, '{}\n');
57
+ writeFileSync(localRulePath, 'keep-me\n');
58
+ const manifestPath = join(homeDirectory, '.claude', '.claude-dev-env-manifest.json');
59
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
60
+ manifest.files.push(generatedRulePath, syncManifestPath);
61
+ writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
80
62
 
81
63
  runInstaller(homeDirectory, []);
82
64
 
83
- const generatedPath = join(
84
- homeDirectory,
85
- DEFAULT_CURSOR_DIRECTORY_NAME,
86
- CURSOR_RULES_DIRECTORY_NAME,
87
- 'asd-ste100-language.mdc',
88
- );
89
- assert.equal(existsSync(generatedPath), true);
90
- const generatedText = readFileSync(generatedPath, 'utf8');
91
- assert.equal(generatedText.includes('alwaysApply: true'), true);
92
- assert.equal(readFileSync(extraRulePath, 'utf8'), 'keep-me\n');
65
+ assert.equal(existsSync(generatedRulePath), false);
66
+ assert.equal(existsSync(syncManifestPath), false);
67
+ assert.equal(readFileSync(localRulePath, 'utf8'), 'keep-me\n');
68
+ const reinstalledManifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
69
+ assert.equal(reinstalledManifest.files.includes(generatedRulePath), false);
93
70
  } finally {
94
71
  rmSync(homeDirectory, { recursive: true, force: true });
95
72
  }
96
73
  });
97
74
 
98
- test('--only journal is rejected before Cursor rules change; --only core writes them', () => {
75
+ test('--only journal is rejected before the policy is seeded; --only core seeds it and writes no Cursor rules', () => {
99
76
  const homeDirectory = mkdtempSync(join(tmpdir(), 'cdev-cursor-groups-'));
100
77
  try {
101
- const generatedPath = join(
78
+ const cursorRulesDirectory = join(
102
79
  homeDirectory,
103
80
  DEFAULT_CURSOR_DIRECTORY_NAME,
104
81
  CURSOR_RULES_DIRECTORY_NAME,
105
- 'asd-ste100-language.mdc',
106
82
  );
107
83
  const policyPath = join(homeDirectory, '.agents', 'rules', 'subagent-model-policy.json');
108
84
  assert.throws(
109
85
  () => runInstaller(homeDirectory, ['--only', 'journal']),
110
86
  error => error.status === 1 && /Unknown group\(s\): journal/.test(error.stderr),
111
87
  );
112
- assert.equal(existsSync(generatedPath), false);
113
88
  assert.equal(existsSync(policyPath), false);
114
89
 
115
90
  runInstaller(homeDirectory, ['--only', 'core']);
116
- assert.equal(existsSync(generatedPath), true);
117
91
  assert.equal(existsSync(policyPath), true);
92
+ assert.equal(existsSync(cursorRulesDirectory), false);
118
93
  } finally {
119
94
  rmSync(homeDirectory, { recursive: true, force: true });
120
95
  }
@@ -427,7 +402,7 @@ test('uninstalling one profile keeps routing for another installed profile', ()
427
402
  }
428
403
  });
429
404
 
430
- test('a blocked shared rule destination rolls back generated Cursor files', () => {
405
+ test('a blocked shared rule destination fails the install and keeps the existing file', () => {
431
406
  const homeDirectory = mkdtempSync(join(tmpdir(), 'cdev-pstack-rollback-'));
432
407
  try {
433
408
  const sharedRulesPath = join(homeDirectory, '.agents', 'rules');
@@ -435,8 +410,6 @@ test('a blocked shared rule destination rolls back generated Cursor files', () =
435
410
  writeFileSync(sharedRulesPath, 'keep-existing-file');
436
411
  assert.throws(() => runInstaller(homeDirectory, ['--only', 'core']));
437
412
  assert.equal(readFileSync(sharedRulesPath, 'utf8'), 'keep-existing-file');
438
- assert.equal(existsSync(join(homeDirectory, '.cursor', 'rules', 'pstack-models.mdc')), false);
439
- assert.equal(existsSync(join(homeDirectory, '.cursor', '.sync-manifest.json')), false);
440
413
  } finally {
441
414
  rmSync(homeDirectory, { recursive: true, force: true });
442
415
  }
package/bin/install.mjs CHANGED
@@ -33,7 +33,6 @@ import {
33
33
  CODEX_HOME_ENVIRONMENT_VARIABLE,
34
34
  DEFAULT_CODEX_DIRECTORY_NAME,
35
35
  CODEX_RULES_PACKAGE_DIRECTORY_NAME,
36
- CURSOR_SYNC_SCRIPT_FILE_NAME,
37
36
  CURSOR_RULES_DIRECTORY_NAME,
38
37
  SUBAGENT_MODEL_POLICY_FILE_NAME,
39
38
  WINDOWS_PYTHON_LAUNCHER_COMMAND,
@@ -378,58 +377,6 @@ export function pythonFileAndPrefixArguments(pythonCommand) {
378
377
  return { file: unquoted, prefixArguments: [] };
379
378
  }
380
379
 
381
- /**
382
- * Read generated Cursor paths from the sync manifest under a Cursor home.
383
- *
384
- * Args:
385
- * cursorRoot: Absolute `.cursor` directory.
386
- *
387
- * Returns:
388
- * Absolute paths the installer may record, including the sync manifest.
389
- */
390
- export function collectManagedCursorSyncPaths(cursorRoot) {
391
- const manifestPath = join(cursorRoot, '.sync-manifest.json');
392
- if (!existsSync(manifestPath)) return [];
393
- const parsed = JSON.parse(readFileSync(manifestPath, 'utf8'));
394
- const generatedPaths = [manifestPath];
395
- const allEntries = { ...(parsed.entries || {}), ...(parsed.docs_entries || {}) };
396
- for (const eachRelativePath of Object.keys(allEntries)) {
397
- generatedPaths.push(join(cursorRoot, eachRelativePath));
398
- }
399
- return generatedPaths;
400
- }
401
-
402
- /**
403
- * Run the installed Cursor rule generator against Claude and Cursor roots.
404
- *
405
- * Args:
406
- * pythonCommand: Interpreter command from install preflight.
407
- * scriptPath: Absolute `sync_to_cursor.py` path.
408
- * claudeRoot: Managed Claude root that holds `rules/` and `docs/`.
409
- * cursorRoot: Cursor home that receives `rules/*.mdc`.
410
- *
411
- * Returns:
412
- * void
413
- */
414
- export function runCursorRuleSync(pythonCommand, scriptPath, claudeRoot, cursorRoot) {
415
- mkdirSync(cursorRoot, { recursive: true });
416
- const { file, prefixArguments } = pythonFileAndPrefixArguments(pythonCommand);
417
- execFileSync(
418
- file,
419
- [
420
- ...prefixArguments,
421
- scriptPath,
422
- '--force',
423
- '--quiet',
424
- '--claude-root',
425
- claudeRoot,
426
- '--cursor-root',
427
- cursorRoot,
428
- ],
429
- { stdio: 'inherit' },
430
- );
431
- }
432
-
433
380
  /**
434
381
  * Install the pstack plugin from its marketplace into the hosts this run serves.
435
382
  *
@@ -2371,6 +2318,19 @@ function pruneStaleFilesAcrossManagedRoots(priorInstalledFiles, currentInstalled
2371
2318
  skillsPrunedCount += rootOutcome.prunedCount;
2372
2319
  }
2373
2320
  }
2321
+ const cursorInstallDirectory = INSTALL_ROOT_RESOLUTION.cursorInstallDirectory;
2322
+ const retiredCursorSyncManifestPath = join(cursorInstallDirectory, '.sync-manifest.json');
2323
+ const isRetiredCursorRuleFile = priorFile => resolve(priorFile) === retiredCursorSyncManifestPath
2324
+ || isInsideDirectory(resolve(priorFile), INSTALL_ROOT_RESOLUTION.cursorRulesInstallDirectory);
2325
+ const retiredCursorRulesOutcome = pruneStaleInstalledFiles(
2326
+ priorInstalledFiles?.filter(isRetiredCursorRuleFile) ?? null,
2327
+ currentInstalledFiles,
2328
+ cursorInstallDirectory,
2329
+ join(backupRoot, 'cursor'),
2330
+ { managedHomeDirectory: cursorInstallDirectory },
2331
+ );
2332
+ prunedCount += retiredCursorRulesOutcome.prunedCount;
2333
+ failedPaths.push(...retiredCursorRulesOutcome.failedPaths);
2374
2334
  const retiredClaudeGuidancePath = join(CLAUDE_HOME, 'AGENTS.md');
2375
2335
  const retiredGuidanceOutcome = pruneStaleInstalledFiles(
2376
2336
  priorInstalledFiles?.filter(priorFile => resolve(priorFile) === retiredClaudeGuidancePath) ?? null,
@@ -2714,19 +2674,9 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
2714
2674
  allInstalledFiles.push(...stats.paths);
2715
2675
  }
2716
2676
  }
2717
- const shouldInstallCursorRules = !selectedGroups
2677
+ const shouldSeedSubagentModelPolicy = !selectedGroups
2718
2678
  || activeGroups.some((eachGroup) => (eachGroup.includeDirectories || []).includes('rules'));
2719
- if (shouldInstallCursorRules) {
2720
- const scriptPath = join(CLAUDE_HOME, 'scripts', CURSOR_SYNC_SCRIPT_FILE_NAME);
2721
- if (!existsSync(scriptPath)) {
2722
- throw new Error(`cursor rule sync script missing: ${scriptPath}`);
2723
- }
2724
- const cursorRoot = dirname(INSTALL_ROOT_RESOLUTION.cursorRulesInstallDirectory);
2725
- runCursorRuleSync(pythonCommand, scriptPath, CLAUDE_HOME, cursorRoot);
2726
- const generatedCursorPaths = collectManagedCursorSyncPaths(cursorRoot);
2727
- allInstalledFiles.push(...generatedCursorPaths);
2728
- summary.cursorRules = { created: generatedCursorPaths.length, updated: 0, paths: generatedCursorPaths };
2729
- syncWrittenPaths(allInstalledFiles);
2679
+ if (shouldSeedSubagentModelPolicy) {
2730
2680
  const seededPolicyPath = seedSubagentModelPolicy();
2731
2681
  if (seededPolicyPath) {
2732
2682
  allInstalledFiles.push(seededPolicyPath);
@@ -2988,10 +2938,6 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
2988
2938
  const { created, updated } = summary.codexRules;
2989
2939
  console.log(` ${CODEX_RULES_PACKAGE_DIRECTORY_NAME}: ${created + updated} files (${created} new, ${updated} updated)`);
2990
2940
  }
2991
- if (summary.cursorRules) {
2992
- const { created } = summary.cursorRules;
2993
- console.log(` cursor-rules: ${created} generated files`);
2994
- }
2995
2941
  if (summary[MANAGED_AGENTS_DIRECTORY_NAME]) {
2996
2942
  const { created, updated } = summary[MANAGED_AGENTS_DIRECTORY_NAME];
2997
2943
  console.log(` ${MANAGED_AGENTS_DIRECTORY_NAME}: ${created + updated} files (${created} new, ${updated} updated)`);
@@ -3336,7 +3282,7 @@ Install location: ~/.claude/ by default; CLAUDE_CONFIG_DIR or --target selects a
3336
3282
  Skills and agents live under ~/.agents (sibling of ~/.claude). ~/.claude/skills and ~/.claude/agents are directory pointers to that home.
3337
3283
  Named profiles resolve under LLM_SETTINGS_PROFILES_ROOT or ~/.claude-profiles/<directoryName>.
3338
3284
  Codex exec-policy files copy into ~/.codex/rules, or CODEX_HOME/rules when CODEX_HOME is set.
3339
- Cursor rule files generate into ~/.cursor/rules as stem-named mdc files, one per Claude rule.
3285
+ A reinstall moves the Cursor rule files older installs generated under ~/.cursor/rules into the run backup.
3340
3286
 
3341
3287
  The installer reads only flags. A bare path argument carries no meaning, so
3342
3288
  npx ${PACKAGE_NAME} . lands in the same root as a run with no argument.
@@ -54,7 +54,9 @@ stack's design, intent, and changes.
54
54
  ## Phase B — loop /code-review low --fix
55
55
 
56
56
  1. Invoke the `code-review` skill with arguments `low --fix` on the same
57
- target. Let it report findings and apply its fixes.
57
+ target. Let it report findings and apply its fixes. When the host has no
58
+ `code-review` skill, invoke the `e-code-review` skill with `low --fix`
59
+ instead. It runs the same review and fix steps.
58
60
  2. After any pass that changed files: test, commit, push as in Phase A.
59
61
  3. Repeat until a pass reports zero findings, or until a pass reports only
60
62
  nits — then fix, test, commit, push, and stop with no confirming pass.
@@ -0,0 +1,79 @@
1
+ # Codex accounts
2
+
3
+ The codex_account_choice picker spreads agent work across up to four Codex
4
+ accounts on one machine. Each account signs in under
5
+ its own Codex home, and every home shares the same Codex setup: config, rules,
6
+ skills, plugins and prompts. A job asks the picker which account to use.
7
+
8
+ ## Pieces
9
+
10
+ | File | What it does |
11
+ |---|---|
12
+ | `scripts/codex_account_choice.py` | `choose` names the account and tier a job runs on, `check` tells a running job whether its account is still above a floor, `sync` links the shared setup into every account's home |
13
+ | `scripts/codex_account_meters.py` | Reads one account's rate-limit windows through `codex app-server` with `CODEX_HOME` set to that account's home |
14
+ | `scripts/dev_env_scripts_constants/codex_account_constants.py` | The account names in try order, the shared entry names, the 10% bar, the 1% Luna stop, and the 20% 5-hour floor for Luna |
15
+
16
+ ## Names and order
17
+
18
+ The accounts are `codex-1`, `codex-2`, `codex-3` and `codex-4`. Their homes are
19
+ `~/.codex-profiles/codex-1` and so on, or under `CODEX_PROFILES_ROOT` when set.
20
+ Jobs try them in that order. The names say nothing about a plan, so a plan change
21
+ keeps every name. To change the order, sign the accounts into different folders.
22
+
23
+ ## Sign in once
24
+
25
+ ```
26
+ python packages/claude-dev-env/scripts/codex_account_choice.py sync
27
+ ```
28
+
29
+ Then, for each account, sign in with that account's home:
30
+
31
+ ```
32
+ $env:CODEX_HOME = "$HOME\.codex-profiles\codex-1"; codex login
33
+ ```
34
+
35
+ The sign-in lives in that folder's `auth.json`. Only the entries in
36
+ `ALL_SHARED_CODEX_HOME_NAMES` link to `~/.codex`. Sign-in, sessions, history,
37
+ logs and state files stay per account.
38
+
39
+ ## Which account a job uses
40
+
41
+ Room is the smaller of an account's two windows: the 5-hour window and the week.
42
+
43
+ | Condition | Answer |
44
+ |---|---|
45
+ | First account in order with more than 10% left | `normal` on that account |
46
+ | No account over 10%, one or more over 1% | `luna` on the account with the most room, `stop_below_percent` 1 |
47
+ | An account that reports a 5-hour window, with under 20% of that window left | never takes `luna` |
48
+ | No account over 1% | `wait`, naming the account whose blocking windows reset first |
49
+ | Account not signed in, or its meter unread | skipped, with the reason in `accounts` |
50
+
51
+ ```
52
+ python packages/claude-dev-env/scripts/codex_account_choice.py choose
53
+ {"tier": "normal", "account": "codex-1", "codex_home": "...\\codex-1", "percent_left": 62.0,
54
+ "stop_below_percent": null, "reason": "codex-1 has 62% left", "accounts": [...]}
55
+ ```
56
+
57
+ A job runs Codex with `CODEX_HOME` set to `codex_home`. On `luna`, the job runs
58
+ Luna and polls `check` between steps:
59
+
60
+ ```
61
+ python packages/claude-dev-env/scripts/codex_account_choice.py check codex-2 --floor 1
62
+ ```
63
+
64
+ `check` exits 0 while the account has more than the floor left, and 3 once it has
65
+ not or its meter is unread. The job stops on 3.
66
+
67
+ ## Shared callers
68
+
69
+ Two scripts under `_shared` run `choose` by path. Each finds the picker at
70
+ `scripts/codex_account_choice.py` in the directory that holds `_shared`. Once
71
+ installed, that is `~/.claude/scripts/codex_account_choice.py`.
72
+
73
+ | Caller | What it does with the answer |
74
+ |---|---|
75
+ | `_shared/pr-loop/scripts/check_convergence.py` | Requires a Codex clean stamp on HEAD only on `normal`. `luna`, `wait`, or a failed picker skips the Codex gate |
76
+ | `_shared/advisor/scripts/codex_astra_advisor.py` | Binds Astra only on `normal`, and runs Codex with `CODEX_HOME` set to `codex_home` |
77
+
78
+ Pass `--codex-path` when `codex` is off PATH. Without it the picker also tries
79
+ the desktop install path under the user home.
@@ -172,7 +172,7 @@ def test_path_exempts_test_file_suffix() -> None:
172
172
 
173
173
  def test_path_does_not_exempt_production_module() -> None:
174
174
  assert not path_is_exempt(
175
- "shared_utils/samsung_utils/cert_failure_processor/failure_categorizer.py"
175
+ "shared_utils/vendor_utils/cert_failure_processor/failure_categorizer.py"
176
176
  )
177
177
 
178
178
 
@@ -241,7 +241,7 @@ def test_main_blocks_trio_copy_introduced_by_second_multi_edit() -> None:
241
241
  {
242
242
  "tool_name": "MultiEdit",
243
243
  "tool_input": {
244
- "file_path": "shared_utils/samsung_utils/cert_failure_processor/parser.py",
244
+ "file_path": "shared_utils/vendor_utils/cert_failure_processor/parser.py",
245
245
  "edits": [
246
246
  {"old_string": "a = 1", "new_string": "a = 11"},
247
247
  {"old_string": "b = 2", "new_string": COPIED_TRIO},
@@ -261,7 +261,7 @@ def test_main_allows_clean_multi_edit() -> None:
261
261
  {
262
262
  "tool_name": "MultiEdit",
263
263
  "tool_input": {
264
- "file_path": "shared_utils/samsung_utils/cert_failure_processor/parser.py",
264
+ "file_path": "shared_utils/vendor_utils/cert_failure_processor/parser.py",
265
265
  "edits": [
266
266
  {"old_string": "a = 1", "new_string": "a = 11"},
267
267
  {"old_string": "b = 2", "new_string": "b = 22"},
@@ -301,7 +301,7 @@ def test_main_blocks_local_trio_copy_in_production_module() -> None:
301
301
  "tool_name": "Write",
302
302
  "tool_input": {
303
303
  "file_path": (
304
- "shared_utils/samsung_utils/cert_failure_processor/failure_categorizer.py"
304
+ "shared_utils/vendor_utils/cert_failure_processor/failure_categorizer.py"
305
305
  ),
306
306
  "content": COPIED_TRIO,
307
307
  },
@@ -319,7 +319,7 @@ def test_main_allows_import_of_shared_helper() -> None:
319
319
  {
320
320
  "tool_name": "Write",
321
321
  "tool_input": {
322
- "file_path": "shared_utils/samsung_utils/cleanup.py",
322
+ "file_path": "shared_utils/vendor_utils/cleanup.py",
323
323
  "content": (
324
324
  "from shared_utils.web_automation.utils.windows_filesystem import "
325
325
  "force_rmtree\n\nforce_rmtree(path)\n"
@@ -24,7 +24,7 @@ SYNTHETIC_AWS_KEY = "AKIA" + ("B" * 16)
24
24
  SYNTHETIC_PEM_HEADER = "-----BEGIN RSA PRIVATE KEY-----"
25
25
  SYNTHETIC_REAL_EMAIL = "person.fixture@company-example.io"
26
26
  SYNTHETIC_SAFE_EMAIL = "user@example.com"
27
- SYNTHETIC_GITHUB_NOREPLY_EMAIL = "24366590+octocat@users.noreply.github.com"
27
+ SYNTHETIC_GITHUB_NOREPLY_EMAIL = "81234567+octocat@users.noreply.github.com"
28
28
  SYNTHETIC_HOME_PATH = r"C:\Users\fixture_real_user\notes.txt"
29
29
  SYNTHETIC_PLACEHOLDER_HOME = r"C:\Users\<you>\notes.txt"
30
30
  SYNTHETIC_PRIVATE_IP = "192.168.42.17"
@@ -20,7 +20,7 @@ HOOK_SCRIPT_PATH = HOOK_DIRECTORY / "pytest_testpaths_orphan_blocker.py"
20
20
 
21
21
  PYPROJECT_WITH_EXPLICIT_TESTPATHS = (
22
22
  "[tool.pytest.ini_options]\n"
23
- 'testpaths = [\n "tests",\n "samsung_utils/tests",\n]\n'
23
+ 'testpaths = [\n "tests",\n "vendor_utils/tests",\n]\n'
24
24
  'python_files = ["test_*.py"]\n'
25
25
  )
26
26
 
@@ -91,7 +91,7 @@ def test_find_flags_test_directory_absent_from_explicit_testpaths(tmp_path: Path
91
91
  def test_find_passes_test_directory_listed_in_testpaths(tmp_path: Path) -> None:
92
92
  package_root = tmp_path / "shared_utils"
93
93
  _write_package(package_root, PYPROJECT_WITH_EXPLICIT_TESTPATHS)
94
- registered_test_file = package_root / "samsung_utils" / "tests" / "test_normalizer.py"
94
+ registered_test_file = package_root / "vendor_utils" / "tests" / "test_normalizer.py"
95
95
  assert find_unregistered_test_directory(str(registered_test_file)) is None
96
96
 
97
97
 
@@ -28,3 +28,19 @@ for each_module_name in list(sys.modules):
28
28
  ):
29
29
  del sys.modules[each_module_name]
30
30
  importlib.invalidate_caches()
31
+
32
+
33
+ import pytest
34
+
35
+ from git_hooks_constants.verification_notice_constants import (
36
+ TARGET_REPOSITORY_ENVIRONMENT_VARIABLE,
37
+ )
38
+
39
+ EXAMPLE_TARGET_REPOSITORY = "example-owner/example-repo"
40
+
41
+
42
+ @pytest.fixture(autouse=True)
43
+ def configured_target_repository(monkeypatch: pytest.MonkeyPatch) -> str:
44
+ """Point the verification notice at the example repository for each test."""
45
+ monkeypatch.setenv(TARGET_REPOSITORY_ENVIRONMENT_VARIABLE, EXAMPLE_TARGET_REPOSITORY)
46
+ return EXAMPLE_TARGET_REPOSITORY
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  NOTICE_EVENT_COMMIT: str = "commit"
4
4
  NOTICE_EVENT_PUSH: str = "push"
5
5
  ALL_NOTICE_EVENTS: frozenset[str] = frozenset({NOTICE_EVENT_COMMIT, NOTICE_EVENT_PUSH})
6
- TARGET_REPOSITORY_REMOTE: str = "jonecho/python-automation"
6
+ TARGET_REPOSITORY_ENVIRONMENT_VARIABLE: str = "CLAUDE_VERIFICATION_TARGET_REPOSITORY"
7
7
  GIT_CONFIG_SUBCOMMAND: str = "config"
8
8
  GIT_REV_PARSE_SUBCOMMAND: str = "rev-parse"
9
9
  ALL_GIT_REMOTE_URL_QUERY: tuple[str, ...] = (GIT_CONFIG_SUBCOMMAND, "--get", "remote.origin.url")
@@ -27,7 +27,7 @@ def test_native_git_commit_notice_reports_the_new_head(tmp_path: Path) -> None:
27
27
  repository_path = create_native_repository(
28
28
  tmp_path,
29
29
  "repo",
30
- "https://github.com/JonEcho/python-automation.git",
30
+ "https://github.com/example-owner/example-repo.git",
31
31
  )
32
32
  hook_directory = tmp_path / "hooks"
33
33
  install_native_hook(repository_path, hook_directory, "post-commit", "post_commit")
@@ -44,7 +44,7 @@ def test_native_git_commit_allows_missing_setup_and_prints_advisory(tmp_path: Pa
44
44
  repository_path.mkdir()
45
45
  configure_native_repository(
46
46
  repository_path,
47
- "https://github.com/JonEcho/python-automation.git",
47
+ "https://github.com/example-owner/example-repo.git",
48
48
  )
49
49
  hook_directory = tmp_path / "hooks"
50
50
  install_native_hook(repository_path, hook_directory, "pre-commit", "pre_commit")
@@ -44,7 +44,7 @@ def test_native_git_push_to_local_bare_remote_allows_missing_setup(tmp_path: Pat
44
44
  repository_path = create_native_repository(
45
45
  tmp_path,
46
46
  "repo",
47
- "https://github.com/JonEcho/python-automation.git",
47
+ "https://github.com/example-owner/example-repo.git",
48
48
  initial_branch="main",
49
49
  )
50
50
  run_git(repository_path, "add", "README.md")
@@ -54,7 +54,7 @@ def test_native_git_push_to_local_bare_remote_allows_missing_setup(tmp_path: Pat
54
54
  configure_remote_url_rewrite(
55
55
  repository_path,
56
56
  bare_repository_path,
57
- "https://github.com/JonEcho/python-automation.git",
57
+ "https://github.com/example-owner/example-repo.git",
58
58
  )
59
59
  hook_directory = tmp_path / "hooks"
60
60
  install_native_hook(repository_path, hook_directory, "pre-push", "pre_push")
@@ -23,7 +23,7 @@ def build_context(
23
23
  repository_root: Path,
24
24
  *,
25
25
  event: str = "commit",
26
- repository_remote: str = "jonecho/python-automation",
26
+ repository_remote: str = "example-owner/example-repo",
27
27
  ) -> VerificationNoticeContext:
28
28
  git_directory = repository_root / ".git"
29
29
  return VerificationNoticeContext(
@@ -35,7 +35,6 @@ from git_hooks_constants.verification_notice_constants import (
35
35
  PYTHON_COMMAND,
36
36
  REPORT_FILE_NAME,
37
37
  SCOPED_VERIFICATION_SCRIPT_PATH,
38
- TARGET_REPOSITORY_REMOTE,
39
38
  UNKNOWN_SHA,
40
39
  )
41
40
 
@@ -49,6 +48,7 @@ from verification_notice_context import (
49
48
  VerificationNoticeContext,
50
49
  _load_notice_context,
51
50
  _run_git_query,
51
+ is_target_repository,
52
52
  )
53
53
  from verification_start import load_runner_configuration, start_automatic_advisory
54
54
  from verification_notice_state import (
@@ -71,7 +71,7 @@ def build_verification_notice(context: VerificationNoticeContext) -> str:
71
71
  Returns:
72
72
  The formatted advisory text, or an empty string for another repository.
73
73
  """
74
- if context.repository_remote != TARGET_REPOSITORY_REMOTE:
74
+ if not is_target_repository(context.repository_remote):
75
75
  return ""
76
76
  current_head = context.current_head or UNKNOWN_SHA
77
77
  state, verified_head = _evaluate_context(context, _run_git_query)
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import json
6
+ import os
6
7
  import subprocess
7
8
  from collections.abc import Mapping
8
9
  from dataclasses import dataclass
@@ -32,7 +33,7 @@ from git_hooks_constants.verification_notice_constants import (
32
33
  REPOSITORY_PART_INDEX,
33
34
  SELECTED_MANIFEST_PATH_FIELD,
34
35
  SELECTION_FIELD,
35
- TARGET_REPOSITORY_REMOTE,
36
+ TARGET_REPOSITORY_ENVIRONMENT_VARIABLE,
36
37
  )
37
38
  from local_verification.manifest import ManifestRunFatal, load_manifest
38
39
 
@@ -52,6 +53,24 @@ class VerificationNoticeContext:
52
53
  all_report_fields: Mapping[str, object] | None
53
54
 
54
55
 
56
+ def is_target_repository(repository_remote: str | None) -> bool:
57
+ """Report whether a normalized remote names the configured target repository.
58
+
59
+ The target is read from the environment at call time and has no default,
60
+ so an unset or blank setting matches no repository.
61
+
62
+ Args:
63
+ repository_remote: The normalized owner/name remote, or None.
64
+
65
+ Returns:
66
+ True when the remote equals the configured target, case-insensitively.
67
+ """
68
+ configured_target = os.environ.get(TARGET_REPOSITORY_ENVIRONMENT_VARIABLE, "").strip()
69
+ if not configured_target or repository_remote is None:
70
+ return False
71
+ return repository_remote.casefold() == configured_target.casefold()
72
+
73
+
55
74
  def normalize_repository_remote(remote_url: str) -> str | None:
56
75
  """Return an owner/repository slug from common Git remote URL forms.
57
76
 
@@ -85,7 +104,7 @@ def _load_notice_context(
85
104
  normalized_remote = normalize_repository_remote(
86
105
  _run_git_query(repository_root, ALL_GIT_REMOTE_URL_QUERY, False) or ""
87
106
  )
88
- if normalized_remote != TARGET_REPOSITORY_REMOTE or current_head is None:
107
+ if not is_target_repository(normalized_remote) or current_head is None:
89
108
  return None
90
109
  if not git_directory_text:
91
110
  return None
@@ -20,9 +20,8 @@ from git_hooks_constants.verification_notice_constants import (
20
20
  RUNNER_FILE_NAME,
21
21
  RUNNER_PYTHON_FIELD,
22
22
  RUNNER_SETTINGS_FIELD,
23
- TARGET_REPOSITORY_REMOTE,
24
23
  )
25
- from verification_notice_context import VerificationNoticeContext
24
+ from verification_notice_context import VerificationNoticeContext, is_target_repository
26
25
 
27
26
  _subprocess_window_hooks_directory = str(Path(__file__).resolve().parents[2] / "hooks")
28
27
  if _subprocess_window_hooks_directory not in sys.path:
@@ -33,7 +32,7 @@ from hooks_constants.subprocess_window import hidden_window_creation_flags
33
32
 
34
33
  def start_automatic_advisory(context: VerificationNoticeContext) -> None:
35
34
  """Start the detached advisory poller when native metadata is valid."""
36
- if context.repository_remote != TARGET_REPOSITORY_REMOTE:
35
+ if not is_target_repository(context.repository_remote):
37
36
  return
38
37
  if context.event not in ALL_NOTICE_EVENTS:
39
38
  return
@@ -23,7 +23,6 @@ ALL_CONFIG_DIRECTORY_NAMES = frozenset(
23
23
  "usage_pause_constants",
24
24
  "prototype_scripts_constants",
25
25
  "codex_review_scripts_constants",
26
- "e_code_review_scripts_constants",
27
26
  "dev_env_scripts_constants",
28
27
  "fresh_branch_scripts_constants",
29
28
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "8.11.1",
3
+ "version": "8.13.0",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -67,9 +67,10 @@ server write. Pass the matching action and body file. Use `pr-create`,
67
67
 
68
68
  Pass `--repository <owner>/<name>` for the repository the post targets. A
69
69
  post may name a private organization only inside a repository that
70
- organization owns. The linter holds the names as digests, so it reports the
71
- line that names one without printing the name. Describe the organization in
72
- general terms and drop the link.
70
+ organization owns. The same digests cover the owner's private repository, the
71
+ owner's personal handle, and a private client. The linter holds the names as
72
+ digests, so it reports the line that names one without printing the name.
73
+ Describe the name in general terms and drop the link.
73
74
 
74
75
  The linter reads the body file and reports a volatile local path without
75
76
  printing the body. Fix the body and rerun the linter before posting.