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.
- package/AGENTS.md +106 -32
- package/agents/AGENTS.md +0 -5
- package/agents/test_agent_frontmatter.py +7 -7
- package/bin/AGENTS.md +6 -4
- package/bin/install-constants.mjs +51 -0
- package/bin/install.codex-rules.test.mjs +173 -0
- package/bin/install.cursor-rules.test.mjs +103 -0
- package/bin/install.mjs +130 -19
- package/bin/install.profile-root.test.mjs +8 -0
- package/bin/install.prune.test.mjs +2 -1
- package/bin/install.test.mjs +3 -3
- package/bin/install.transaction.test.mjs +1 -0
- package/bin/install.uninstall-transaction.test.mjs +1 -0
- package/bin/resolve-install-root.mjs +43 -10
- package/codex-rules/claude-dev-env.rules +12 -0
- package/commands/AGENTS.md +0 -10
- package/hooks/blocking/test_claude_md_orphan_file_blocker.py +1 -1
- package/hooks/diagnostic/AGENTS.md +32 -0
- package/hooks/diagnostic/hook_log_init.py +2 -2
- package/output-styles/AGENTS.md +1 -1
- package/package.json +2 -1
- package/scripts/sync_to_cursor/AGENTS.md +3 -3
- package/scripts/sync_to_cursor/canonical_docs.py +11 -11
- package/scripts/sync_to_cursor/config/__init__.py +8 -0
- package/scripts/sync_to_cursor/engine.py +26 -1
- package/scripts/sync_to_cursor/rules.py +76 -5
- package/scripts/test_active_capability_references.py +2 -2
- package/scripts/tests/AGENTS.md +2 -0
- package/scripts/tests/test_engine.py +102 -0
- package/scripts/tests/test_rules.py +79 -0
- package/skills/anthropic-plan/AGENTS.md +1 -1
- package/skills/anthropic-plan/SKILL.md +1 -1
- package/skills/anthropic-plan/test_skill_contract.py +8 -6
- package/skills/prototype/SKILL.md +1 -2
- package/skills/prototype/reference/promotion-tasks.md +1 -1
- package/skills/prototype/workflows/promotion.md +1 -1
- package/agents/caveman.md +0 -73
- package/agents/clasp-deployment-orchestrator.md +0 -608
- package/agents/code-advisor.md +0 -23
- package/agents/deep-research.md +0 -152
- package/agents/docs-agent.md +0 -85
- package/commands/commit.md +0 -28
- package/commands/docupdate.md +0 -322
- package/commands/hook-log-extract.md +0 -70
- package/commands/hook-log-init.md +0 -76
- package/commands/implement.md +0 -102
- package/commands/plan.md +0 -14
- package/commands/pr-comments.md +0 -47
- package/commands/review-plan.md +0 -5
- package/commands/right-size.md +0 -15
- package/commands/sum.md +0 -30
- 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
|
-
|
|
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
|
-
|
|
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}
|
|
2334
|
+
* @param {boolean} isManifestRequired
|
|
2226
2335
|
* @returns {ReturnType<typeof buildUninstallPlan>}
|
|
2227
2336
|
*/
|
|
2228
|
-
function resolveUninstallPlan(
|
|
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 ${
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
*
|
|
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({
|
|
2336
|
-
const plan = resolveUninstallPlan(
|
|
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
|
|
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 (
|
|
2583
|
-
console.error(`ERROR: failed to spawn install child: ${
|
|
2693
|
+
if (childProcess.error) {
|
|
2694
|
+
console.error(`ERROR: failed to spawn install child: ${childProcess.error.message}`);
|
|
2584
2695
|
return 1;
|
|
2585
2696
|
}
|
|
2586
|
-
return
|
|
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
|
|
1231
|
+
'the uninstall removes the mypy configuration the install writes outside ~/.claude',
|
|
1231
1232
|
);
|
|
1232
1233
|
assert.equal(
|
|
1233
1234
|
installerOutput.includes(`skipping ${mypyIniPath}`),
|
package/bin/install.test.mjs
CHANGED
|
@@ -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, '
|
|
1675
|
+
const agentDefinitionPath = join(sourceRoot, 'clean-coder.md');
|
|
1676
1676
|
writeFileSync(
|
|
1677
1677
|
agentDefinitionPath,
|
|
1678
|
-
'---\nname:
|
|
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, '
|
|
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 {
|
|
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
|
|
107
|
-
*
|
|
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
|
-
|
|
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
|
|
134
|
-
if (!
|
|
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
|
|
170
|
+
return targetPath;
|
|
138
171
|
}
|
|
139
172
|
if (token.startsWith('--target=')) {
|
|
140
|
-
const
|
|
141
|
-
if (!
|
|
173
|
+
const targetPath = token.slice('--target='.length);
|
|
174
|
+
if (!targetPath) {
|
|
142
175
|
throw new Error('--target requires a path argument');
|
|
143
176
|
}
|
|
144
|
-
return
|
|
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")
|
package/commands/AGENTS.md
CHANGED
|
@@ -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
|
-
"| `/
|
|
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/
|
|
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
|
-
"""
|
|
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)
|
package/output-styles/AGENTS.md
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
|
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
|