claude-dev-env 8.9.0 → 8.11.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/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_regression.py +85 -3
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_regression.py +34 -0
- package/_shared/pr-loop/scripts/fix_pr_test_proof.py +132 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/fix_pr_test_proof_constants.py +43 -0
- package/_shared/pr-loop/scripts/test_fix_pr_test_proof.py +126 -0
- package/bin/codex-skill-load-block.mjs +45 -0
- package/bin/codex-skill-load-block.test.mjs +72 -0
- package/bin/install-constants.mjs +5 -0
- package/bin/install.agents-home.test.mjs +76 -1
- package/bin/install.cursor-rules.test.mjs +6 -0
- package/bin/install.mjs +55 -33
- package/bin/install.test.mjs +37 -0
- package/bin/install.transaction.test.mjs +29 -7
- package/bin/seed-codex-pstack-models.mjs +27 -0
- package/bin/seed-codex-pstack-models.test.mjs +72 -0
- package/docs/CODE_RULES.md +5 -1
- package/hooks/blocking/code_rules_enforcer.py +1 -3
- package/hooks/blocking/code_rules_magic_values.py +1 -3
- package/hooks/blocking/code_rules_paired_test.py +5 -6
- package/hooks/blocking/tdd_enforcer_parts/__init__.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_ephemeral.py +1 -46
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +2 -40
- package/hooks/followup_ledger.py +8 -1
- package/hooks/git-hooks/test_installed_policy_lint.py +4 -2
- package/hooks/hooks.json +27 -0
- package/hooks/hooks_constants/followup_ledger_constants.py +4 -0
- package/hooks/hooks_constants/messages.py +0 -1
- package/hooks/hooks_constants/nested_project_hooks_constants.py +37 -0
- package/hooks/hooks_constants/skill_loaded_reminder_constants.py +16 -4
- package/hooks/hooks_constants/test_messages.py +1 -7
- package/hooks/lifecycle/nested_project_hooks.py +395 -0
- package/hooks/lifecycle/test_nested_project_hooks.py +251 -0
- package/hooks/session/skill_loaded_reminder.py +28 -13
- package/hooks/session/test_skill_loaded_reminder.py +49 -2
- package/hooks/test_followup_ledger.py +18 -0
- package/package.json +2 -1
- package/pstack-codex-models.md +37 -0
- package/rules/bdd.md +1 -1
- package/rules/build-needs-a-user.md +53 -0
- package/rules/code-standards.md +1 -1
- package/rules/flag-non-breaking-findings.md +7 -0
- package/rules/paired-test-coverage.md +4 -2
- package/rules/testing.md +5 -1
- package/scripts/cde_lint.py +47 -1
- package/scripts/policy_lint/adapter_callers.py +124 -0
- package/scripts/policy_lint/adapter_pairing.py +4 -2
- package/scripts/policy_lint/adapter_support.py +4 -3
- package/scripts/policy_lint/adapters.py +15 -0
- package/scripts/policy_lint/check_catalog.py +24 -0
- package/scripts/policy_lint/config/approved_test_pairs.py +3 -2
- package/scripts/policy_lint/config/check_catalog_constants.py +13 -0
- package/scripts/policy_lint/config/constants.py +18 -0
- package/scripts/policy_lint/engine.py +9 -3
- package/scripts/policy_lint/model.py +6 -2
- package/scripts/policy_lint/registry.py +6 -0
- package/scripts/policy_lint/selection.py +2 -0
- package/scripts/tests/test_cde_lint.py +50 -0
- package/scripts/tests/test_policy_lint_rules_archive.py +16 -0
- package/scripts/tests/test_policy_lint_rules_callers.py +126 -0
- package/scripts/tests/test_policy_lint_rules_check_catalog.py +25 -1
- package/scripts/tests/test_policy_lint_rules_pairing.py +31 -2
- package/scripts/tests/test_policy_lint_selection.py +35 -0
- package/hooks/blocking/tdd_enforcer.py +0 -195
- package/hooks/blocking/tdd_enforcer_parts/candidate_paths.py +0 -142
- package/hooks/blocking/tdd_enforcer_parts/config/__init__.py +0 -1
- package/hooks/blocking/tdd_enforcer_parts/config/tdd_enforcer_constants.py +0 -49
- package/hooks/blocking/tdd_enforcer_parts/content_hash_store.py +0 -298
- package/hooks/blocking/tdd_enforcer_parts/decisions.py +0 -96
- package/hooks/blocking/tdd_enforcer_parts/freshness.py +0 -95
- package/hooks/blocking/tdd_enforcer_parts/git_tracking.py +0 -132
- package/hooks/blocking/tdd_enforcer_parts/path_classification.py +0 -119
- package/hooks/blocking/tdd_enforcer_parts/tests/test_candidate_paths.py +0 -31
- package/hooks/blocking/tdd_enforcer_parts/tests/test_content_hash_store.py +0 -256
- package/hooks/blocking/tdd_enforcer_parts/tests/test_decisions.py +0 -46
- package/hooks/blocking/tdd_enforcer_parts/tests/test_freshness.py +0 -28
- package/hooks/blocking/tdd_enforcer_parts/tests/test_git_tracking.py +0 -111
- package/hooks/blocking/tdd_enforcer_parts/tests/test_path_classification.py +0 -36
- package/hooks/blocking/test_tdd_enforcer.py +0 -1506
- package/hooks/blocking/test_tdd_enforcer_restore.py +0 -108
- package/hooks/blocking/test_tdd_enforcer_scratchpad.py +0 -105
- package/scripts/codex_compat_watcher.py +0 -502
- package/scripts/tests/test_codex_compat_watcher.py +0 -599
package/bin/install.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, copyFileSync, unlinkSync, rmSync, rmdirSync, renameSync, realpathSync, lstatSync, constants as filesystemConstants } from 'node:fs';
|
|
4
|
-
import { join, dirname, resolve, relative, basename, isAbsolute, extname } from 'node:path';
|
|
4
|
+
import { join, dirname, resolve, relative, basename, isAbsolute, extname, sep } from 'node:path';
|
|
5
5
|
import { homedir } from 'node:os';
|
|
6
6
|
import { execSync, execFileSync, spawnSync } from 'node:child_process';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
@@ -38,12 +38,14 @@ import {
|
|
|
38
38
|
SUBAGENT_MODEL_POLICY_FILE_NAME,
|
|
39
39
|
WINDOWS_PYTHON_LAUNCHER_COMMAND,
|
|
40
40
|
PYTHON_PROBE_TIMEOUT_MILLISECONDS,
|
|
41
|
-
|
|
41
|
+
ALL_CODEX_NATIVE_PRE_TOOL_USE_MATCHERS,
|
|
42
42
|
} from './install-constants.mjs';
|
|
43
43
|
import {
|
|
44
44
|
installPstackPlugin,
|
|
45
45
|
shouldInstallPstackPlugin,
|
|
46
46
|
} from './install-pstack-plugin.mjs';
|
|
47
|
+
import { seedCodexPstackModels } from './seed-codex-pstack-models.mjs';
|
|
48
|
+
import { writeCodexSkillLoadBlock } from './codex-skill-load-block.mjs';
|
|
47
49
|
import {
|
|
48
50
|
continuityHostConfigurationPaths,
|
|
49
51
|
removeContinuityHooks,
|
|
@@ -1173,11 +1175,9 @@ export function copyTree(sourceBase, destBase, options = {}) {
|
|
|
1173
1175
|
* @param {string} backupName The managed file name used in the backup path.
|
|
1174
1176
|
* @returns {string|null} The backup path when existing content differs.
|
|
1175
1177
|
*/
|
|
1176
|
-
function backupHubBeforeOverwrite(destPath,
|
|
1178
|
+
function backupHubBeforeOverwrite(destPath, incomingText, backupName) {
|
|
1177
1179
|
if (!existsSync(destPath)) return null;
|
|
1178
|
-
|
|
1179
|
-
const incomingBytes = readFileSync(incomingPath);
|
|
1180
|
-
if (existingBytes.equals(incomingBytes)) return null;
|
|
1180
|
+
if (readFileSync(destPath, 'utf8') === incomingText) return null;
|
|
1181
1181
|
const backupsDir = join(CLAUDE_HOME, 'backups');
|
|
1182
1182
|
mkdirSync(backupsDir, { recursive: true });
|
|
1183
1183
|
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
@@ -2102,7 +2102,7 @@ function selectCodexNativeHooksConfig(hooksConfig) {
|
|
|
2102
2102
|
...hooksConfig,
|
|
2103
2103
|
hooks: {
|
|
2104
2104
|
PreToolUse: Array.isArray(preToolUseGroups)
|
|
2105
|
-
? preToolUseGroups.filter(group => group?.matcher
|
|
2105
|
+
? preToolUseGroups.filter(group => ALL_CODEX_NATIVE_PRE_TOOL_USE_MATCHERS.has(group?.matcher))
|
|
2106
2106
|
: [],
|
|
2107
2107
|
},
|
|
2108
2108
|
};
|
|
@@ -2371,6 +2371,15 @@ function pruneStaleFilesAcrossManagedRoots(priorInstalledFiles, currentInstalled
|
|
|
2371
2371
|
skillsPrunedCount += rootOutcome.prunedCount;
|
|
2372
2372
|
}
|
|
2373
2373
|
}
|
|
2374
|
+
const retiredClaudeGuidancePath = join(CLAUDE_HOME, 'AGENTS.md');
|
|
2375
|
+
const retiredGuidanceOutcome = pruneStaleInstalledFiles(
|
|
2376
|
+
priorInstalledFiles?.filter(priorFile => resolve(priorFile) === retiredClaudeGuidancePath) ?? null,
|
|
2377
|
+
currentInstalledFiles,
|
|
2378
|
+
CLAUDE_HOME,
|
|
2379
|
+
backupRoot,
|
|
2380
|
+
);
|
|
2381
|
+
prunedCount += retiredGuidanceOutcome.prunedCount;
|
|
2382
|
+
failedPaths.push(...retiredGuidanceOutcome.failedPaths);
|
|
2374
2383
|
return { prunedCount, skillsPrunedCount, failedPaths };
|
|
2375
2384
|
}
|
|
2376
2385
|
|
|
@@ -2769,6 +2778,23 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
|
|
|
2769
2778
|
if (!selectedGroups && shouldInstallPstackPlugin()) {
|
|
2770
2779
|
const pstackPlugin = installPstackPluginForHosts();
|
|
2771
2780
|
summary.pstackPlugin = pstackPlugin;
|
|
2781
|
+
if (pstackPlugin.hosts.some(host => host.host === 'codex' && host.status === 'installed')) {
|
|
2782
|
+
summary.codexPstackModels = seedCodexPstackModels(
|
|
2783
|
+
INSTALL_ROOT_RESOLUTION.codexHomeDirectory,
|
|
2784
|
+
);
|
|
2785
|
+
if (summary.codexPstackModels) {
|
|
2786
|
+
allInstalledFiles.push(...summary.codexPstackModels);
|
|
2787
|
+
for (const eachPath of summary.codexPstackModels) {
|
|
2788
|
+
allUserOwnedPreferencePaths.add(eachPath);
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
const skillLoadGuidancePath = writeCodexSkillLoadBlock(
|
|
2792
|
+
INSTALL_ROOT_RESOLUTION.codexHomeDirectory,
|
|
2793
|
+
);
|
|
2794
|
+
if (skillLoadGuidancePath) {
|
|
2795
|
+
console.log(` \u2713 ${skillLoadGuidancePath} (Codex skill-load line)`);
|
|
2796
|
+
}
|
|
2797
|
+
}
|
|
2772
2798
|
for (const eachHost of pstackPlugin.hosts) {
|
|
2773
2799
|
console.log(eachHost.warning
|
|
2774
2800
|
? ` Pstack (${eachHost.host}): ${eachHost.status} \u2014 ${eachHost.warning}`
|
|
@@ -2877,31 +2903,27 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
|
|
|
2877
2903
|
);
|
|
2878
2904
|
}
|
|
2879
2905
|
|
|
2880
|
-
const claudeHubSource = join(PACKAGE_ROOT, '.claude', 'CLAUDE.md');
|
|
2881
|
-
if (existsSync(claudeHubSource)) {
|
|
2882
|
-
const claudeHubDest = join(CLAUDE_HOME, 'CLAUDE.md');
|
|
2883
|
-
const backupPath = backupHubBeforeOverwrite(claudeHubDest, claudeHubSource, 'CLAUDE.md');
|
|
2884
|
-
if (backupPath) {
|
|
2885
|
-
console.log(
|
|
2886
|
-
` \u21bb ${relative(CLAUDE_HOME, backupPath)} (previous CLAUDE.md hub preserved)`
|
|
2887
|
-
);
|
|
2888
|
-
}
|
|
2889
|
-
copyFileSync(claudeHubSource, claudeHubDest);
|
|
2890
|
-
allInstalledFiles.push(claudeHubDest);
|
|
2891
|
-
console.log(` \u2713 ${relative(CLAUDE_HOME, claudeHubDest)} (hub)`);
|
|
2892
|
-
}
|
|
2893
2906
|
const agentsHubSource = join(PACKAGE_ROOT, 'AGENTS.md');
|
|
2894
2907
|
if (existsSync(agentsHubSource)) {
|
|
2895
|
-
const agentsHubDest = join(
|
|
2896
|
-
const
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2908
|
+
const agentsHubDest = join(AGENTS_HOME, 'AGENTS.md');
|
|
2909
|
+
const claudeHubDest = join(CLAUDE_HOME, 'CLAUDE.md');
|
|
2910
|
+
const claudeHubText = `@${relative(CLAUDE_HOME, agentsHubDest).split(sep).join('/')}\n`;
|
|
2911
|
+
const allHubWrites = [
|
|
2912
|
+
{ destPath: agentsHubDest, text: readFileSync(agentsHubSource, 'utf8'), label: 'AGENTS.md', role: 'shared guidance' },
|
|
2913
|
+
{ destPath: claudeHubDest, text: claudeHubText, label: 'CLAUDE.md', role: 'hub' },
|
|
2914
|
+
];
|
|
2915
|
+
mkdirSync(AGENTS_HOME, { recursive: true });
|
|
2916
|
+
for (const eachHubWrite of allHubWrites) {
|
|
2917
|
+
const backupPath = backupHubBeforeOverwrite(eachHubWrite.destPath, eachHubWrite.text, eachHubWrite.label);
|
|
2918
|
+
if (backupPath) {
|
|
2919
|
+
console.log(
|
|
2920
|
+
` \u21bb ${relative(CLAUDE_HOME, backupPath)} (previous ${eachHubWrite.label} preserved)`
|
|
2921
|
+
);
|
|
2922
|
+
}
|
|
2923
|
+
writeFileSync(eachHubWrite.destPath, eachHubWrite.text);
|
|
2924
|
+
allInstalledFiles.push(eachHubWrite.destPath);
|
|
2925
|
+
console.log(` \u2713 ${eachHubWrite.destPath} (${eachHubWrite.role})`);
|
|
2901
2926
|
}
|
|
2902
|
-
copyFileSync(agentsHubSource, agentsHubDest);
|
|
2903
|
-
allInstalledFiles.push(agentsHubDest);
|
|
2904
|
-
console.log(` \u2713 ${relative(CLAUDE_HOME, agentsHubDest)} (canonical guidance)`);
|
|
2905
2927
|
}
|
|
2906
2928
|
const isFullInstall = !selectedGroups;
|
|
2907
2929
|
const didPruneRun = isFullInstall && UNRESOLVED_DEPENDENCY_NAMES.length === 0;
|
|
@@ -3322,10 +3344,10 @@ Profile selection (--profile/--profiles) is mutually exclusive with --target.
|
|
|
3322
3344
|
Target selection finishes before any mutation. Each target writes its own manifest
|
|
3323
3345
|
with package, version, targetIdentity, managedRoot, files, and skills.
|
|
3324
3346
|
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3347
|
+
The package guidance lands once at ~/.agents/AGENTS.md, and ~/.claude/CLAUDE.md
|
|
3348
|
+
holds one import line that loads it. If either file already exists with other
|
|
3349
|
+
contents, the installer writes the previous contents to
|
|
3350
|
+
~/.claude/backups/<name>.<timestamp>.bak first.
|
|
3329
3351
|
`);
|
|
3330
3352
|
}
|
|
3331
3353
|
|
package/bin/install.test.mjs
CHANGED
|
@@ -52,6 +52,11 @@ import {
|
|
|
52
52
|
retainNewestRunBackupOnly,
|
|
53
53
|
} from './install.mjs';
|
|
54
54
|
import { EVER_SHIPPED_SKILL_NAMES } from './ever-shipped-skills.mjs';
|
|
55
|
+
import {
|
|
56
|
+
SKILL_LOAD_BLOCK_START,
|
|
57
|
+
SKILL_LOAD_INSTRUCTION,
|
|
58
|
+
withSkillLoadBlock,
|
|
59
|
+
} from './codex-skill-load-block.mjs';
|
|
55
60
|
|
|
56
61
|
|
|
57
62
|
test('installer reports the authoritative Git hook names', () => {
|
|
@@ -3064,6 +3069,34 @@ test('a full install adds the pstack marketplace and plugin on both hosts', t =>
|
|
|
3064
3069
|
]);
|
|
3065
3070
|
assert.match(installerOutput, /Pstack \(claude\): installed/);
|
|
3066
3071
|
assert.match(installerOutput, /Pstack \(codex\): installed/);
|
|
3072
|
+
const codexHome = join(sandbox.homeDirectory, '.codex');
|
|
3073
|
+
const sheet = readFileSync(join(codexHome, 'pstack-models.md'), 'utf8');
|
|
3074
|
+
const agents = readFileSync(join(codexHome, 'AGENTS.md'), 'utf8');
|
|
3075
|
+
assert.match(sheet, /^session hook: on$/m);
|
|
3076
|
+
assert.match(agents, /^feature, refactoring: gpt-6-sol$/m);
|
|
3077
|
+
assert.doesNotMatch(agents, /^session hook:/m);
|
|
3078
|
+
assert.ok(agents.startsWith(`${SKILL_LOAD_BLOCK_START}\n${SKILL_LOAD_INSTRUCTION}\n`));
|
|
3079
|
+
assert.equal(existsSync(join(sandbox.homeDirectory, '.claude', 'pstack-models.md')), false);
|
|
3080
|
+
const manifestPath = join(sandbox.homeDirectory, '.claude', '.claude-dev-env-manifest.json');
|
|
3081
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
3082
|
+
assert.equal(manifest.files.includes(join(codexHome, 'pstack-models.md')), false);
|
|
3083
|
+
assert.equal(manifest.files.includes(join(codexHome, 'AGENTS.md')), false);
|
|
3084
|
+
});
|
|
3085
|
+
|
|
3086
|
+
test('a repeat install preserves Codex pstack configuration', t => {
|
|
3087
|
+
const sandbox = pstackPluginSandbox(t);
|
|
3088
|
+
const codexHome = join(sandbox.homeDirectory, '.codex');
|
|
3089
|
+
runPstackInstaller(sandbox.homeDirectory, [], sandbox.environment);
|
|
3090
|
+
writeFileSync(join(codexHome, 'AGENTS.md'), 'Custom Codex guidance\n');
|
|
3091
|
+
writeFileSync(join(codexHome, 'pstack-models.md'), 'session hook: off\n');
|
|
3092
|
+
|
|
3093
|
+
runPstackInstaller(sandbox.homeDirectory, [], sandbox.environment);
|
|
3094
|
+
|
|
3095
|
+
assert.equal(
|
|
3096
|
+
readFileSync(join(codexHome, 'AGENTS.md'), 'utf8'),
|
|
3097
|
+
withSkillLoadBlock('Custom Codex guidance\n'),
|
|
3098
|
+
);
|
|
3099
|
+
assert.equal(readFileSync(join(codexHome, 'pstack-models.md'), 'utf8'), 'session hook: off\n');
|
|
3067
3100
|
});
|
|
3068
3101
|
|
|
3069
3102
|
test('an absent host command is reported and leaves the other host installed', t => {
|
|
@@ -3085,6 +3118,8 @@ test('an absent host command is reported and leaves the other host installed', t
|
|
|
3085
3118
|
sandbox.recordedCommands().filter(eachCommand => eachCommand.startsWith('codex')),
|
|
3086
3119
|
[],
|
|
3087
3120
|
);
|
|
3121
|
+
assert.equal(existsSync(join(sandbox.homeDirectory, '.codex', 'pstack-models.md')), false);
|
|
3122
|
+
assert.equal(existsSync(join(sandbox.homeDirectory, '.codex', 'AGENTS.md')), false);
|
|
3088
3123
|
});
|
|
3089
3124
|
|
|
3090
3125
|
test('--no-pstack leaves both hosts untouched', t => {
|
|
@@ -3096,6 +3131,8 @@ test('--no-pstack leaves both hosts untouched', t => {
|
|
|
3096
3131
|
|
|
3097
3132
|
assert.deepEqual(sandbox.recordedCommands(), []);
|
|
3098
3133
|
assert.doesNotMatch(installerOutput, /Pstack \(/);
|
|
3134
|
+
assert.equal(existsSync(join(sandbox.homeDirectory, '.codex', 'pstack-models.md')), false);
|
|
3135
|
+
assert.equal(existsSync(join(sandbox.homeDirectory, '.codex', 'AGENTS.md')), false);
|
|
3099
3136
|
});
|
|
3100
3137
|
|
|
3101
3138
|
function continuityCommandCount(configurationPath) {
|
|
@@ -270,13 +270,6 @@ test('runWithInstallTransaction commits and discards journal on success', () =>
|
|
|
270
270
|
rmSync(box.root, { recursive: true, force: true });
|
|
271
271
|
});
|
|
272
272
|
|
|
273
|
-
/**
|
|
274
|
-
* Spawn the real installer against a sandbox home with optional fault phase.
|
|
275
|
-
*
|
|
276
|
-
* @param {string} homeDirectory
|
|
277
|
-
* @param {string[]} extraArguments
|
|
278
|
-
* @param {{ faultPhase?: string|null }} [options]
|
|
279
|
-
*/
|
|
280
273
|
function runInstaller(homeDirectory, extraArguments, options = {}) {
|
|
281
274
|
const childEnvironment = {
|
|
282
275
|
...process.env,
|
|
@@ -285,6 +278,7 @@ function runInstaller(homeDirectory, extraArguments, options = {}) {
|
|
|
285
278
|
USERPROFILE: homeDirectory,
|
|
286
279
|
GIT_CONFIG_GLOBAL: join(homeDirectory, '.gitconfig'),
|
|
287
280
|
CODEX_HOME: join(homeDirectory, '.codex'),
|
|
281
|
+
...options.environment,
|
|
288
282
|
};
|
|
289
283
|
if (options.faultPhase) {
|
|
290
284
|
childEnvironment[INSTALL_FAULT_ENV] = options.faultPhase;
|
|
@@ -298,6 +292,34 @@ function runInstaller(homeDirectory, extraArguments, options = {}) {
|
|
|
298
292
|
});
|
|
299
293
|
}
|
|
300
294
|
|
|
295
|
+
test('a later install fault removes newly seeded Codex pstack files', () => {
|
|
296
|
+
const homeDirectory = mkdtempSync(join(tmpdir(), 'cdev-txn-codex-pstack-'));
|
|
297
|
+
try {
|
|
298
|
+
const codexCommandPath = join(
|
|
299
|
+
homeDirectory,
|
|
300
|
+
process.platform === 'win32' ? 'codex.cmd' : 'codex',
|
|
301
|
+
);
|
|
302
|
+
writeFileSync(
|
|
303
|
+
codexCommandPath,
|
|
304
|
+
process.platform === 'win32' ? '@echo off\r\nexit /b 0\r\n' : '#!/bin/sh\nexit 0\n',
|
|
305
|
+
{ mode: 0o755 },
|
|
306
|
+
);
|
|
307
|
+
const failedRun = runInstaller(homeDirectory, [], {
|
|
308
|
+
faultPhase: FAULT_PHASES.AFTER_MANIFEST_WRITE,
|
|
309
|
+
environment: {
|
|
310
|
+
CDE_INSTALL_PSTACK: '1',
|
|
311
|
+
CDE_CODEX_EXECUTABLE: codexCommandPath,
|
|
312
|
+
},
|
|
313
|
+
});
|
|
314
|
+
assert.notEqual(failedRun.status, 0, failedRun.stdout + failedRun.stderr);
|
|
315
|
+
assert.match(`${failedRun.stdout}${failedRun.stderr}`, /after_manifest_write/);
|
|
316
|
+
assert.equal(existsSync(join(homeDirectory, '.codex', 'pstack-models.md')), false);
|
|
317
|
+
assert.equal(existsSync(join(homeDirectory, '.codex', 'AGENTS.md')), false);
|
|
318
|
+
} finally {
|
|
319
|
+
rmSync(homeDirectory, { recursive: true, force: true });
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
|
|
301
323
|
function seedPriorInstall(homeDirectory) {
|
|
302
324
|
const claudeDirectory = join(homeDirectory, '.claude');
|
|
303
325
|
const rulesFile = join(claudeDirectory, 'rules', 'prior.md');
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { copyFileSync, lstatSync, mkdirSync, readFileSync, unlinkSync, writeFileSync, constants as filesystemConstants } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
const CODEX_PRESET_PATH = fileURLToPath(new URL('../pstack-codex-models.md', import.meta.url));
|
|
6
|
+
|
|
7
|
+
function pathExists(path) {
|
|
8
|
+
return lstatSync(path, { throwIfNoEntry: false }) !== undefined;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function seedCodexPstackModels(codexHome, writeAgentGuidance = writeFileSync) {
|
|
12
|
+
const sheetPath = join(codexHome, 'pstack-models.md');
|
|
13
|
+
const agentsPath = join(codexHome, 'AGENTS.md');
|
|
14
|
+
if (pathExists(sheetPath) || pathExists(agentsPath)) return null;
|
|
15
|
+
|
|
16
|
+
const preset = readFileSync(CODEX_PRESET_PATH, 'utf8');
|
|
17
|
+
const agentGuidance = preset.replace(/^session hook: on\n?$/m, '');
|
|
18
|
+
mkdirSync(codexHome, { recursive: true });
|
|
19
|
+
copyFileSync(CODEX_PRESET_PATH, sheetPath, filesystemConstants.COPYFILE_EXCL);
|
|
20
|
+
try {
|
|
21
|
+
writeAgentGuidance(agentsPath, agentGuidance, { encoding: 'utf8', flag: 'wx' });
|
|
22
|
+
} catch (writeError) {
|
|
23
|
+
unlinkSync(sheetPath);
|
|
24
|
+
throw writeError;
|
|
25
|
+
}
|
|
26
|
+
return [sheetPath, agentsPath];
|
|
27
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import { strict as assert } from 'node:assert';
|
|
3
|
+
import { existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { seedCodexPstackModels } from './seed-codex-pstack-models.mjs';
|
|
7
|
+
|
|
8
|
+
test('fresh Codex home gets the selected roles and hook setting', (context) => {
|
|
9
|
+
const root = mkdtempSync(join(tmpdir(), 'cde-pstack-models-'));
|
|
10
|
+
context.after(() => rmSync(root, { recursive: true, force: true }));
|
|
11
|
+
const codexHome = join(root, '.codex');
|
|
12
|
+
const claudeHome = join(root, '.claude');
|
|
13
|
+
mkdirSync(claudeHome);
|
|
14
|
+
writeFileSync(join(claudeHome, 'CLAUDE.md'), 'Claude guidance\n');
|
|
15
|
+
|
|
16
|
+
const paths = seedCodexPstackModels(codexHome);
|
|
17
|
+
const sheet = readFileSync(join(codexHome, 'pstack-models.md'), 'utf8');
|
|
18
|
+
const agents = readFileSync(join(codexHome, 'AGENTS.md'), 'utf8');
|
|
19
|
+
assert.deepEqual(paths, [join(codexHome, 'pstack-models.md'), join(codexHome, 'AGENTS.md')]);
|
|
20
|
+
assert.match(sheet, /^feature, refactoring: gpt-6-sol$/m);
|
|
21
|
+
assert.match(sheet, /^bug-fix: gpt-6-astra$/m);
|
|
22
|
+
assert.match(sheet, /^how explorer: gpt-6-luna$/m);
|
|
23
|
+
assert.match(sheet, /^arena runners: gpt-6-astra, gpt-6-sol, gpt-6-luna$/m);
|
|
24
|
+
assert.match(sheet, /^session hook: on$/m);
|
|
25
|
+
assert.match(agents, /^feature, refactoring: gpt-6-sol$/m);
|
|
26
|
+
assert.doesNotMatch(agents, /^session hook:/m);
|
|
27
|
+
assert.equal(readFileSync(join(claudeHome, 'CLAUDE.md'), 'utf8'), 'Claude guidance\n');
|
|
28
|
+
assert.equal(existsSync(join(claudeHome, 'pstack-models.md')), false);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('existing Codex configuration stays untouched on repeat installs', (context) => {
|
|
32
|
+
const root = mkdtempSync(join(tmpdir(), 'cde-pstack-models-'));
|
|
33
|
+
context.after(() => rmSync(root, { recursive: true, force: true }));
|
|
34
|
+
const codexHome = join(root, '.codex');
|
|
35
|
+
seedCodexPstackModels(codexHome);
|
|
36
|
+
writeFileSync(join(codexHome, 'AGENTS.md'), 'Custom agents\n');
|
|
37
|
+
|
|
38
|
+
assert.equal(seedCodexPstackModels(codexHome), null);
|
|
39
|
+
assert.equal(readFileSync(join(codexHome, 'AGENTS.md'), 'utf8'), 'Custom agents\n');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('a failed AGENTS write removes only the sheet created by that attempt', (context) => {
|
|
43
|
+
const root = mkdtempSync(join(tmpdir(), 'cde-pstack-models-'));
|
|
44
|
+
context.after(() => rmSync(root, { recursive: true, force: true }));
|
|
45
|
+
const codexHome = join(root, '.codex');
|
|
46
|
+
const claudeHome = join(root, '.claude');
|
|
47
|
+
mkdirSync(claudeHome);
|
|
48
|
+
writeFileSync(join(claudeHome, 'CLAUDE.md'), 'Existing Claude guidance\n');
|
|
49
|
+
|
|
50
|
+
const competingWrite = (agentsPath) => {
|
|
51
|
+
writeFileSync(agentsPath, 'Existing Codex guidance\n');
|
|
52
|
+
throw new Error('AGENTS.md already exists');
|
|
53
|
+
};
|
|
54
|
+
assert.throws(() => seedCodexPstackModels(codexHome, competingWrite), /already exists/);
|
|
55
|
+
assert.equal(existsSync(join(codexHome, 'pstack-models.md')), false);
|
|
56
|
+
assert.equal(readFileSync(join(codexHome, 'AGENTS.md'), 'utf8'), 'Existing Codex guidance\n');
|
|
57
|
+
assert.equal(readFileSync(join(claudeHome, 'CLAUDE.md'), 'utf8'), 'Existing Claude guidance\n');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
for (const existingFile of ['AGENTS.md', 'pstack-models.md']) {
|
|
61
|
+
test(`partial Codex setup with ${existingFile} is preserved`, (context) => {
|
|
62
|
+
const root = mkdtempSync(join(tmpdir(), 'cde-pstack-models-'));
|
|
63
|
+
context.after(() => rmSync(root, { recursive: true, force: true }));
|
|
64
|
+
const codexHome = join(root, '.codex');
|
|
65
|
+
mkdirSync(codexHome);
|
|
66
|
+
writeFileSync(join(codexHome, existingFile), 'Custom configuration\n');
|
|
67
|
+
|
|
68
|
+
assert.equal(seedCodexPstackModels(codexHome), null);
|
|
69
|
+
assert.equal(readFileSync(join(codexHome, existingFile), 'utf8'), 'Custom configuration\n');
|
|
70
|
+
assert.equal(existsSync(join(codexHome, existingFile === 'AGENTS.md' ? 'pstack-models.md' : 'AGENTS.md')), false);
|
|
71
|
+
});
|
|
72
|
+
}
|
package/docs/CODE_RULES.md
CHANGED
|
@@ -75,7 +75,11 @@ Parameters follow YAGNI: add an optional parameter when a caller varies the valu
|
|
|
75
75
|
|
|
76
76
|
## 8. TDD PROCESS
|
|
77
77
|
|
|
78
|
-
1. **RED** — failing test
|
|
78
|
+
1. **RED** — a failing test. 2. **GREEN** — minimum code to pass. 3. **REFACTOR** — only if valuable.
|
|
79
|
+
|
|
80
|
+
This loop is the default for a bug fix and for new behavior, and the TDD skill (`pstack:tdd`) carries the procedure. No hook or lint checks the order. A prototype may run ahead of its tests and adds them before the pull request goes ready. A bug fix ships with a test that reproduces the bug.
|
|
81
|
+
|
|
82
|
+
**Proof of check.** Each pull request body states how the change was checked: a test, a run, a screenshot, or a measurement, named so a reviewer can repeat it. A reviewer flags a pull request whose body names no proof.
|
|
79
83
|
|
|
80
84
|
## 9. SELF-CONTAINED COMPONENTS
|
|
81
85
|
|
|
@@ -11,9 +11,7 @@ Advisory only (non-blocking):
|
|
|
11
11
|
|
|
12
12
|
Companion tests live alongside this file as
|
|
13
13
|
``test_code_rules_enforcer_<suffix>.py``; the ``<suffix>`` split keeps each
|
|
14
|
-
concern focused.
|
|
15
|
-
``test_code_rules_enforcer_*.py`` sibling as a test candidate for the
|
|
16
|
-
``code_rules_*`` module family, so the suffix files satisfy its gate.
|
|
14
|
+
concern focused.
|
|
17
15
|
"""
|
|
18
16
|
import json
|
|
19
17
|
import sys
|
|
@@ -36,9 +36,7 @@ def _mask_string_literals_preserving_length(source_line: str) -> str:
|
|
|
36
36
|
|
|
37
37
|
Matching tests live in
|
|
38
38
|
``test_code_rules_enforcer_magic_string_masking.py``, one of the
|
|
39
|
-
``test_code_rules_enforcer_<suffix>.py`` family files
|
|
40
|
-
``tdd_enforcer.py`` hook accepts as test candidates for the
|
|
41
|
-
``code_rules_*`` module family.
|
|
39
|
+
``test_code_rules_enforcer_<suffix>.py`` family files.
|
|
42
40
|
"""
|
|
43
41
|
|
|
44
42
|
string_literal_pattern = re.compile(
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
"""Public-function paired-test coverage check for ``code_rules_enforcer``.
|
|
2
2
|
|
|
3
|
-
The
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
orders, so a forgotten function gets a behavioral test before the
|
|
3
|
+
The staged policy lint's test-pairing rule judges coverage at file
|
|
4
|
+
granularity: a module whose dedicated test file exercises some public functions
|
|
5
|
+
while leaving one untested still satisfies it. This check closes that gap from
|
|
6
|
+
both write orders, so a forgotten function gets a behavioral test before the
|
|
8
7
|
partially-covered module lands.
|
|
9
8
|
|
|
10
9
|
``check_public_function_missing_paired_test`` fires on the production-module
|
|
@@ -25,7 +24,7 @@ Both checks stay conservative to keep false positives near zero:
|
|
|
25
24
|
|
|
26
25
|
- The production-side check runs only on a production module whose stem-matched
|
|
27
26
|
test file already exists; a module with no dedicated test file is out of scope
|
|
28
|
-
and left to the
|
|
27
|
+
and left to the staged lint's test-pairing rule. The test-side check runs only on a
|
|
29
28
|
stem-matched test file whose paired production module exists on disk.
|
|
30
29
|
- The production-side check fires only when the suite already exercises the
|
|
31
30
|
module — covering at least one of its public functions, or referencing one of
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Content analysis the policy lint's adapter pairing rule loads to exempt constants-only and import-only edits."""
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import json
|
|
6
5
|
import os
|
|
7
6
|
import subprocess
|
|
8
7
|
import sys
|
|
@@ -21,13 +20,11 @@ if _HOOKS_DIRECTORY not in sys.path:
|
|
|
21
20
|
from code_rules_enforcer import main as enforcer_main # noqa: E402
|
|
22
21
|
from code_rules_shared import is_ephemeral_script_path # noqa: E402
|
|
23
22
|
from code_rules_enforcer_test_support import ( # noqa: E402
|
|
24
|
-
build_write_payload,
|
|
25
23
|
run_enforcer_cli,
|
|
26
24
|
run_write_entrypoint,
|
|
27
25
|
)
|
|
28
26
|
|
|
29
27
|
_ENFORCER_SCRIPT = Path(__file__).resolve().parent / "code_rules_enforcer.py"
|
|
30
|
-
_TDD_SCRIPT = Path(__file__).resolve().parent / "tdd_enforcer.py"
|
|
31
28
|
|
|
32
29
|
_VIOLATING_PRODUCTION_SOURCE = "def process_data(payload: str) -> None:\n print(payload)\n"
|
|
33
30
|
|
|
@@ -56,44 +53,6 @@ def _run_main_with_write_payload(
|
|
|
56
53
|
return run_write_entrypoint(enforcer_main, file_path, content)
|
|
57
54
|
|
|
58
55
|
|
|
59
|
-
def _run_tdd_with_write_payload(
|
|
60
|
-
file_path: str,
|
|
61
|
-
content: str,
|
|
62
|
-
) -> subprocess.CompletedProcess[str]:
|
|
63
|
-
"""Drive the TDD enforcer through subprocess with a Write payload.
|
|
64
|
-
|
|
65
|
-
Args:
|
|
66
|
-
file_path: The destination path the Write targets.
|
|
67
|
-
content: The production-looking content to write.
|
|
68
|
-
|
|
69
|
-
Returns:
|
|
70
|
-
The completed process carrying stdout, stderr, and the exit code.
|
|
71
|
-
"""
|
|
72
|
-
write_payload = build_write_payload(file_path, content)
|
|
73
|
-
return subprocess.run(
|
|
74
|
-
[sys.executable, str(_TDD_SCRIPT)],
|
|
75
|
-
input=write_payload,
|
|
76
|
-
capture_output=True,
|
|
77
|
-
text=True,
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
def _decision_from(completed: subprocess.CompletedProcess[str]) -> str | None:
|
|
82
|
-
"""Extract the permissionDecision from a hook's JSON stdout.
|
|
83
|
-
|
|
84
|
-
Args:
|
|
85
|
-
completed: The completed subprocess carrying the hook's stdout.
|
|
86
|
-
|
|
87
|
-
Returns:
|
|
88
|
-
The permissionDecision string, or None when stdout is empty.
|
|
89
|
-
"""
|
|
90
|
-
if not completed.stdout:
|
|
91
|
-
return None
|
|
92
|
-
parsed = json.loads(completed.stdout)
|
|
93
|
-
hook_output = parsed.get("hookSpecificOutput", {})
|
|
94
|
-
return hook_output.get("permissionDecision")
|
|
95
|
-
|
|
96
|
-
|
|
97
56
|
def test_should_return_true_for_claude_job_dir_tmp_path(
|
|
98
57
|
tmp_path: Path,
|
|
99
58
|
monkeypatch: pytest.MonkeyPatch,
|
|
@@ -320,7 +279,7 @@ def test_should_exempt_same_path_set_on_both_gates(
|
|
|
320
279
|
tmp_path: Path,
|
|
321
280
|
monkeypatch: pytest.MonkeyPatch,
|
|
322
281
|
) -> None:
|
|
323
|
-
"""B21: every ephemeral path exits 0 on
|
|
282
|
+
"""B21: every ephemeral path exits 0 on enforcer main."""
|
|
324
283
|
monkeypatch.setenv("CLAUDE_JOB_DIR", str(tmp_path))
|
|
325
284
|
all_ephemeral_paths = [
|
|
326
285
|
str(tmp_path / "tmp" / "scratch.py"),
|
|
@@ -339,7 +298,3 @@ def test_should_exempt_same_path_set_on_both_gates(
|
|
|
339
298
|
assert "deny" not in captured_stdout.lower(), (
|
|
340
299
|
f"enforcer must not deny ephemeral path {each_ephemeral_path!r}"
|
|
341
300
|
)
|
|
342
|
-
completed = _run_tdd_with_write_payload(each_ephemeral_path, _VIOLATING_PRODUCTION_SOURCE)
|
|
343
|
-
assert _decision_from(completed) != "deny", (
|
|
344
|
-
f"TDD enforcer must not deny ephemeral path {each_ephemeral_path!r}"
|
|
345
|
-
)
|
|
@@ -496,8 +496,8 @@ def test_aggregate_exit_code_zero_with_no_output_allows() -> None:
|
|
|
496
496
|
def test_aggregate_explicit_allow_payload_signals_allow_decision() -> None:
|
|
497
497
|
"""An explicit permissionDecision allow from a hosted hook signals an allow decision.
|
|
498
498
|
|
|
499
|
-
|
|
500
|
-
|
|
499
|
+
A hosted hook can write an explicit allow payload, which auto-approves the
|
|
500
|
+
write standalone. The aggregator must surface that as an
|
|
501
501
|
explicit allow decision so the dispatcher re-emits it rather than silently
|
|
502
502
|
falling back to the default permission flow.
|
|
503
503
|
"""
|
|
@@ -680,43 +680,6 @@ def _orphan_claude_md_payload(tmp_path: Path) -> str:
|
|
|
680
680
|
return _write_payload(claude_md_path, orphan_table)
|
|
681
681
|
|
|
682
682
|
|
|
683
|
-
def _parse_hook_allow(completed_process: subprocess.CompletedProcess[str]) -> bool:
|
|
684
|
-
"""Parse one hook's subprocess result for an explicit permissionDecision allow.
|
|
685
|
-
|
|
686
|
-
Args:
|
|
687
|
-
completed_process: The completed subprocess from running a hook.
|
|
688
|
-
|
|
689
|
-
Returns:
|
|
690
|
-
True when the hook emitted an explicit allow decision.
|
|
691
|
-
"""
|
|
692
|
-
stdout_text = completed_process.stdout.strip()
|
|
693
|
-
if not stdout_text:
|
|
694
|
-
return False
|
|
695
|
-
try:
|
|
696
|
-
parsed_output = json.loads(stdout_text)
|
|
697
|
-
except json.JSONDecodeError:
|
|
698
|
-
return False
|
|
699
|
-
hook_specific = parsed_output.get("hookSpecificOutput", {})
|
|
700
|
-
if not isinstance(hook_specific, dict):
|
|
701
|
-
return False
|
|
702
|
-
return hook_specific.get("permissionDecision") == "allow"
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
def test_retired_tdd_hook_cannot_auto_approve_the_write(tmp_path: Path) -> None:
|
|
706
|
-
"""Pairing belongs to staged lint; its edit-time auto-approval is absent."""
|
|
707
|
-
config_target_path = str(tmp_path / "config" / "timing.py")
|
|
708
|
-
constants_only_content = (
|
|
709
|
-
'"""Timing constants."""\n\nMAXIMUM_RETRIES = 3\nRETRY_DELAY_SECONDS = 5\n'
|
|
710
|
-
)
|
|
711
|
-
payload_text = _write_payload(config_target_path, constants_only_content)
|
|
712
|
-
standalone_result = _run_hook_subprocess("blocking/tdd_enforcer.py", payload_text)
|
|
713
|
-
assert _parse_hook_allow(standalone_result)
|
|
714
|
-
dispatcher_result = _run_dispatcher(payload_text)
|
|
715
|
-
dispatcher_is_deny, reason = _parse_hook_decision(dispatcher_result)
|
|
716
|
-
assert not dispatcher_is_deny, reason
|
|
717
|
-
assert not _parse_hook_allow(dispatcher_result)
|
|
718
|
-
|
|
719
|
-
|
|
720
683
|
def test_runpy_hosted_hook_sees_its_own_argv_not_the_dispatchers(tmp_path: Path) -> None:
|
|
721
684
|
"""A runpy-hosted hook resolves its own script path as sys.argv, not the dispatcher's.
|
|
722
685
|
|
|
@@ -851,7 +814,6 @@ ALL_RETAINED_PATHS = {
|
|
|
851
814
|
ALL_RETIRED_PATHS = {
|
|
852
815
|
"validation/hook_format_validator.py",
|
|
853
816
|
"blocking/code_rules_enforcer.py",
|
|
854
|
-
"blocking/tdd_enforcer.py",
|
|
855
817
|
"blocking/windows_rmtree_blocker.py",
|
|
856
818
|
"blocking/state_description_blocker.py",
|
|
857
819
|
"blocking/subprocess_budget_completeness.py",
|
package/hooks/followup_ledger.py
CHANGED
|
@@ -31,6 +31,8 @@ from hooks_constants.followup_ledger_constants import (
|
|
|
31
31
|
GIT_REFERENCE_PREFIX,
|
|
32
32
|
LEDGER_APPEND_MODE,
|
|
33
33
|
LEDGER_ENCODING,
|
|
34
|
+
LEDGER_IGNORE_FILE_NAME,
|
|
35
|
+
LEDGER_IGNORE_TEXT,
|
|
34
36
|
MESSAGE_KEY,
|
|
35
37
|
ORIGIN_COMMIT_KEY,
|
|
36
38
|
RULE_ID_KEY,
|
|
@@ -88,7 +90,9 @@ def record_followup_finding(repository_root: Path, finding: FollowupFinding) ->
|
|
|
88
90
|
"""Append one finding to the repository's ledger, once.
|
|
89
91
|
|
|
90
92
|
A finding already present in the ledger is left alone, so a gate that runs
|
|
91
|
-
on every commit records a standing smell a single time.
|
|
93
|
+
on every commit records a standing smell a single time. The ledger
|
|
94
|
+
directory carries a ``.gitignore`` matching every file in it, so the
|
|
95
|
+
ledger stays out of ``git status`` in any repository. Every filesystem
|
|
92
96
|
error is swallowed, so a ledger failure leaves the caller's gate decision
|
|
93
97
|
unchanged.
|
|
94
98
|
|
|
@@ -116,6 +120,9 @@ def record_followup_finding(repository_root: Path, finding: FollowupFinding) ->
|
|
|
116
120
|
)
|
|
117
121
|
try:
|
|
118
122
|
ledger_path.parent.mkdir(parents=True, exist_ok=True)
|
|
123
|
+
ignore_path = ledger_path.parent / LEDGER_IGNORE_FILE_NAME
|
|
124
|
+
if not ignore_path.exists():
|
|
125
|
+
ignore_path.write_text(LEDGER_IGNORE_TEXT, encoding=LEDGER_ENCODING)
|
|
119
126
|
with ledger_path.open(LEDGER_APPEND_MODE, encoding=LEDGER_ENCODING) as ledger_file:
|
|
120
127
|
ledger_file.write(record_text + "\n")
|
|
121
128
|
except OSError:
|
|
@@ -14,7 +14,6 @@ from test_native_hook_support import run_git
|
|
|
14
14
|
|
|
15
15
|
ALL_RETIRED_HOOK_PATHS = (
|
|
16
16
|
"blocking/code_rules_enforcer.py",
|
|
17
|
-
"blocking/tdd_enforcer.py",
|
|
18
17
|
"blocking/windows_rmtree_blocker.py",
|
|
19
18
|
"blocking/state_description_blocker.py",
|
|
20
19
|
"blocking/subprocess_budget_completeness.py",
|
|
@@ -26,6 +25,7 @@ ALL_RETIRED_HOOK_PATHS = (
|
|
|
26
25
|
"blocking/plain_language_blocker.py",
|
|
27
26
|
"lifecycle/config_change_guard.py",
|
|
28
27
|
)
|
|
28
|
+
ALL_ARCHIVED_HOOK_PATHS = ("blocking/tdd_enforcer.py",)
|
|
29
29
|
PACKAGE_ROOT = Path(__file__).resolve().parents[2]
|
|
30
30
|
ALL_RULE_FIXTURES = (
|
|
31
31
|
("code-rules", "src/worker.py", "from typing import Any\n\ndef worker() -> Any:\n return None\n"),
|
|
@@ -80,7 +80,7 @@ def _legacy_settings_bytes(managed_root: Path, foreign_command: str) -> bytes:
|
|
|
80
80
|
"type": "command",
|
|
81
81
|
"command": f"python {managed_root.as_posix()}/hooks/{each_path}",
|
|
82
82
|
"timeout": 10,
|
|
83
|
-
} for each_path in ALL_RETIRED_HOOK_PATHS),
|
|
83
|
+
} for each_path in (*ALL_RETIRED_HOOK_PATHS, *ALL_ARCHIVED_HOOK_PATHS)),
|
|
84
84
|
],
|
|
85
85
|
}]},
|
|
86
86
|
}
|
|
@@ -135,6 +135,8 @@ def _assert_retired_registrations(managed_root: Path, settings_bytes: bytes, for
|
|
|
135
135
|
source = PACKAGE_ROOT / "hooks" / each_relative_path
|
|
136
136
|
installed = managed_root / "hooks" / each_relative_path
|
|
137
137
|
assert hashlib.sha256(installed.read_bytes()).digest() == hashlib.sha256(source.read_bytes()).digest()
|
|
138
|
+
for each_relative_path in ALL_ARCHIVED_HOOK_PATHS:
|
|
139
|
+
assert not any(each_relative_path in each_command for each_command in all_commands)
|
|
138
140
|
|
|
139
141
|
|
|
140
142
|
@pytest.fixture(scope="module")
|