claude-dev-env 8.12.0 → 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.
- package/.agents/skills/pull-request/scripts/test_pull_request.py +1 -1
- package/.agents/skills-archived/pr-converge/scripts/test_check_convergence.py +4 -4
- package/_shared/advisor/reference/astra-rung.md +7 -7
- package/_shared/advisor/reference/third-party-bind.md +2 -2
- package/_shared/advisor/scripts/codex_astra_advisor.py +33 -26
- package/_shared/advisor/scripts/codex_astra_preflight.py +57 -55
- package/_shared/advisor/scripts/config/advisor_scripts_constants/astra_advisor_constants.py +8 -8
- package/_shared/advisor/scripts/tests/test_codex_astra_advisor.py +69 -23
- package/_shared/advisor/scripts/tests/test_codex_astra_preflight.py +4 -4
- package/_shared/pr-loop/scripts/check_convergence.py +54 -49
- package/_shared/pr-loop/scripts/codex_review_scripts_constants/run_constants.py +4 -0
- package/_shared/pr-loop/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +10 -1
- package/_shared/pr-loop/scripts/run_codex_review.py +4 -6
- package/_shared/pr-loop/scripts/test_check_convergence.py +6 -6
- package/_shared/pr-loop/scripts/test_check_convergence_codex.py +73 -31
- package/_shared/pr-loop/scripts/test_check_convergence_contract.py +2 -2
- package/_shared/pr-loop/scripts/tests/test__claude_permissions_common.py +2 -2
- package/bin/git_hooks_installer.test.mjs +1 -1
- package/bin/install-constants.mjs +1 -6
- package/bin/install.cursor-rules.test.mjs +24 -51
- package/bin/install.mjs +16 -70
- package/docs/codex-accounts.md +79 -0
- package/hooks/blocking/test_duplicate_rmtree_helper_blocker.py +5 -5
- package/hooks/blocking/test_pii_scanner.py +1 -1
- package/hooks/blocking/test_pytest_testpaths_orphan_blocker.py +2 -2
- package/hooks/git-hooks/conftest.py +16 -0
- package/hooks/git-hooks/git_hooks_constants/verification_notice_constants.py +1 -1
- package/hooks/git-hooks/test_post_commit.py +1 -1
- package/hooks/git-hooks/test_pre_commit.py +1 -1
- package/hooks/git-hooks/test_pre_push.py +2 -2
- package/hooks/git-hooks/test_verification_start.py +1 -1
- package/hooks/git-hooks/verification_notice.py +2 -2
- package/hooks/git-hooks/verification_notice_context.py +21 -2
- package/hooks/git-hooks/verification_start.py +2 -3
- package/package.json +1 -1
- package/rules/durable-post-artifacts.md +4 -3
- package/scripts/claude_account_profile.py +21 -9
- package/scripts/codex_account_choice.py +396 -0
- package/scripts/codex_account_meters.py +372 -0
- package/scripts/dev_env_scripts_constants/codex_account_constants.py +141 -0
- package/scripts/dev_env_scripts_constants/private_term_constants.py +17 -2
- package/scripts/private_term_scan.py +6 -2
- package/scripts/repository_checks/config/constants.py +1 -0
- package/scripts/repository_checks/tracked_private_terms.py +7 -0
- package/scripts/test_claude_account_profile.py +12 -0
- package/scripts/test_codex_account_choice.py +276 -0
- package/scripts/test_codex_account_meters.py +205 -0
- package/scripts/tests/test_automatic_advisory.py +3 -3
- package/scripts/tests/test_automatic_advisory_polling.py +1 -1
- package/scripts/tests/test_automatic_advisory_recovery.py +1 -1
- package/scripts/tests/test_closed_pr_label.py +1 -1
- package/scripts/tests/test_private_term_scan.py +28 -0
- package/scripts/tests/test_review_closure.py +2 -2
- package/scripts/tests/test_review_closure_model.py +2 -2
- package/scripts/tests/test_rules.py +2 -83
- package/scripts/tests/test_tracked_private_terms.py +15 -0
- package/scripts/tests/test_verification_notice.py +29 -13
- package/.agents/skills/cert-classification-rule/SKILL.md +0 -224
- package/.agents/skills/cert-classification-rule/scripts/resort_cert_folders.py +0 -158
- package/_shared/pr-loop/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +0 -82
- package/_shared/pr-loop/scripts/codex_usage_probe.py +0 -573
- package/_shared/pr-loop/scripts/test_codex_usage_probe.py +0 -683
- package/scripts/sync_to_cursor/AGENTS.md +0 -1
- package/scripts/sync_to_cursor/CLAUDE.md +0 -1
- package/scripts/sync_to_cursor/__init__.py +0 -13
- package/scripts/sync_to_cursor/canonical_docs.py +0 -66
- package/scripts/sync_to_cursor/config/__init__.py +0 -8
- package/scripts/sync_to_cursor/engine.py +0 -219
- package/scripts/sync_to_cursor/hashing.py +0 -7
- package/scripts/sync_to_cursor/paths.py +0 -18
- package/scripts/sync_to_cursor/rules.py +0 -494
- package/scripts/sync_to_cursor.py +0 -22
- package/scripts/tests/test_engine.py +0 -101
- 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('
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
);
|
|
78
|
-
mkdirSync(
|
|
79
|
-
writeFileSync(
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2677
|
+
const shouldSeedSubagentModelPolicy = !selectedGroups
|
|
2718
2678
|
|| activeGroups.some((eachGroup) => (eachGroup.includeDirectories || []).includes('rules'));
|
|
2719
|
-
if (
|
|
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
|
|
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.
|
|
@@ -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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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 = "
|
|
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 "
|
|
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 / "
|
|
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
|
-
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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 = "
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
package/package.json
CHANGED
|
@@ -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
|
|
71
|
-
|
|
72
|
-
|
|
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.
|