claude-dev-env 8.72.2 → 8.73.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/.agents/skills/build-eval/SKILL.md +4 -8
  2. package/.agents/skills/build-eval/reference/context-budget.md +49 -0
  3. package/.agents/skills/orchestrator/reference/oversee-delegated-work.md +1 -0
  4. package/.agents/skills/pr-lifecycle/SKILL.md +3 -10
  5. package/bin/codex-trimmed-sol.mjs +109 -0
  6. package/bin/codex-trimmed-sol.test.mjs +226 -0
  7. package/bin/install.mjs +18 -0
  8. package/bin/install.question-presentation.test.mjs +1 -1
  9. package/docs/codex-accounts.md +26 -0
  10. package/hooks/blocking/context_budget_blocker.py +177 -0
  11. package/hooks/blocking/test_context_budget_blocker.py +189 -0
  12. package/hooks/blocking/test_pre_tool_use_dispatcher.py +3 -1
  13. package/hooks/context_budget/__init__.py +1 -0
  14. package/hooks/context_budget/baseline.py +78 -0
  15. package/hooks/context_budget/findings.py +251 -0
  16. package/hooks/context_budget/hook_measure.py +178 -0
  17. package/hooks/context_budget/model.py +134 -0
  18. package/hooks/context_budget/policy_changes.py +214 -0
  19. package/hooks/context_budget/policy_file.py +222 -0
  20. package/hooks/context_budget/sections.py +213 -0
  21. package/hooks/context_budget/tests/budget_support.py +71 -0
  22. package/hooks/context_budget/tests/test_baseline.py +43 -0
  23. package/hooks/context_budget/tests/test_findings.py +120 -0
  24. package/hooks/context_budget/tests/test_hook_measure.py +57 -0
  25. package/hooks/context_budget/tests/test_policy_changes.py +175 -0
  26. package/hooks/context_budget/tests/test_policy_file.py +62 -0
  27. package/hooks/context_budget/tests/test_sections.py +112 -0
  28. package/hooks/features/write-edit-blocking.md +5 -1
  29. package/hooks/hooks_constants/context_budget_constants.py +154 -0
  30. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +6 -1
  31. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +6 -2
  32. package/hooks/test_context_budget_hooks.py +79 -0
  33. package/hooks/test_files/context_budget/advisor_config/settings.json +3 -0
  34. package/hooks/test_files/context_budget/empty_transcript.jsonl +0 -0
  35. package/hooks/test_files/context_budget/orchestrator_skill_3f2c3ef.txt +170 -0
  36. package/hooks/test_files/context_budget/orchestrator_skill_98f45b1.txt +158 -0
  37. package/package.json +1 -1
  38. package/scripts/_code_review_test_support.py +7 -12
  39. package/scripts/context_budget_check.py +243 -0
  40. package/scripts/dev_env_scripts_constants/context_budget_check_constants.py +26 -0
  41. package/scripts/policy_lint/adapter_context_budget.py +158 -0
  42. package/scripts/policy_lint/adapters.py +73 -0
  43. package/scripts/policy_lint/config/constants.py +17 -1
  44. package/scripts/policy_lint/registry.py +17 -0
  45. package/scripts/tests/test_context_budget_check.py +270 -0
  46. package/scripts/tests/test_policy_lint_rules_context_budget.py +218 -0
  47. package/scripts/tests/test_policy_lint_rules_pairing.py +26 -0
  48. package/system-prompts/codex-sol.md +170 -0
@@ -4,7 +4,8 @@ description: >-
4
4
  Evaluate skills with a bounded direct Codex review suite or `claude plugin eval`.
5
5
  Use labeled inputs and deterministic grading for output correctness; use a plugin
6
6
  wrapper and with/without-plugin comparisons for discovery and contribution.
7
- Use when the user asks to eval or test a skill.
7
+ Use when the user asks to eval or test a skill, asks whether a context file is
8
+ thin enough, or asks to check the context budget.
8
9
  ---
9
10
 
10
11
  # Build eval
@@ -16,6 +17,7 @@ It includes labeled cases, executable witnesses, related-group holdouts, a bound
16
17
  grading. Start with its validation command and two-case smoke. Preserve the distinction
17
18
  between grader validation, stored replay, a fresh recipe run and a complete workflow run.
18
19
  Use the plugin process below when measuring skill discovery or with/without-plugin contribution.
20
+ To check whether a context file is thin enough, follow [the context budget page](reference/context-budget.md).
19
21
 
20
22
  ## Contents
21
23
 
@@ -101,10 +103,4 @@ Otherwise work the steps in order.
101
103
  - `SKILL.md`. Wrapper layout, gotchas, and process.
102
104
  - `reference/graders-and-commands.md`. Prompt frontmatter, grader types, case set, eval commands,
103
105
  and the `claude -p` recipe for flag-gated features.
104
-
105
- ```text
106
- build-eval/
107
- ├── SKILL.md
108
- └── reference/
109
- └── graders-and-commands.md
110
- ```
106
+ - `reference/context-budget.md`. The context budget command and the fix for each finding.
@@ -0,0 +1,49 @@
1
+ # Context budget
2
+
3
+ Use this page for "is this context file thin enough?" and "check the context budget".
4
+
5
+ An entry file is a short map. Detail lives in reference files the agent opens on demand. The repository's `.claude/context-budget.json` names each kind of context file, its line limit, and whether the section rule applies. Its baseline lists the files and hooks that were over budget when the list was built. A listed file's `lines` is its recorded count when it is over its line limit, and `null` when it is listed only for its sections. A file under its limit may grow up to the limit. A file over its limit and a listed hook may only shrink.
6
+
7
+ ## Check one file
8
+
9
+ Run from inside the repository whose policy applies:
10
+
11
+ ```text
12
+ python packages/claude-dev-env/scripts/context_budget_check.py <path> [<path> ...]
13
+ ```
14
+
15
+ Each file's committed `HEAD` version is its prior state, so the grown and ratchet checks work outside a pull request. The command prints `context-budget: OK (<n> checked)` and exits 0 when every file is within budget. Otherwise it prints one finding per line and exits 1.
16
+
17
+ ## Check the hooks
18
+
19
+ ```text
20
+ python packages/claude-dev-env/scripts/context_budget_check.py --hooks
21
+ ```
22
+
23
+ It runs each hook in the policy's `hooks` list and prints the characters each one injects, its limit, and its baseline.
24
+
25
+ ## Fix a finding
26
+
27
+ | Finding | Fix |
28
+ | --- | --- |
29
+ | `section "<heading>" has <n> detail lines and no pointer` | Move the section body to `reference/<kebab-heading>.md` and leave a one-line link to it. |
30
+ | `file has <n> lines (<kind> limit <limit>)` | Move detail sections to reference files and leave one-line links. |
31
+ | `grew from <n> to <m> lines` | Move detail out until the file is back to its recorded count or fewer. |
32
+ | `was already over its limit at <n> lines and may only shrink` | Move detail out until the file has its prior count or fewer. The write hook gives this in a repository with no policy file. |
33
+ | `file is below its over-budget entry` | Set the entry to the numbers the message gives, `null` lines when the file is within its limit, or remove it. |
34
+ | `the over-budget list may only shrink` | Restore the prior policy value and shrink the context instead. |
35
+ | `hook "<name>" injects <n> characters` | Move the hook's text to a skill or reference file and inject a pointer. |
36
+
37
+ ## Rebuild the over-budget list
38
+
39
+ A person never edits the baseline by hand. After a change shrinks listed files, rebuild it:
40
+
41
+ ```text
42
+ python packages/claude-dev-env/scripts/context_budget_check.py --write-baseline
43
+ ```
44
+
45
+ ## Where it runs
46
+
47
+ - The `context-budget` and `context-budget-policy` rules in `cde_lint` run in CI on every pull request.
48
+ - The write hook `hooks/blocking/context_budget_blocker.py` denies a Write, Edit, MultiEdit, or apply_patch that takes a context file past its budget, in any repository the session edits. With no policy file, it holds a file over its limit to its prior line count.
49
+ - `hooks/test_context_budget_hooks.py` holds each listed hook to its budget.
@@ -9,4 +9,5 @@ The `spawn_oversight_hook` adds this duty to every spawn, at every level of the
9
9
  - Before its output reaches the user, check it against the user's words and standards yourself.
10
10
  - Send the agent a correction when it misses, and check the corrected output the same way.
11
11
  - Give each piece of a multi-piece task its own reviewer or helper.
12
+ - Keep each brief, prompt, and context file you hand an agent a short map: when to use it, the steps, and pointers to reference files it opens on demand.
12
13
  - Show the user you are involved: say what you checked and what you corrected.
@@ -22,21 +22,14 @@ User-level rule: applies to **every** git repo that uses GitHub with `gh`. Small
22
22
 
23
23
  ### Pull request submission rules
24
24
 
25
- **Prove the change where it runs before you open the pull request.** The body carries a
26
- "Proof in practice" section with each command you ran and its quoted output. A hook denies
27
- a new pull request without one. Read `~/.claude/docs/rule-guides/proof-before-pull-request.md`
28
- before you plan the proof run. A `feat` pull request also needs an "Eval" section and a
29
- session that invoked `/claude-api build-eval`; read
30
- `~/.claude/docs/rule-guides/features-start-with-an-eval.md` before you build the feature.
25
+ **Prove the change where it runs before opening a PR.** Read `~/.claude/docs/rule-guides/proof-before-pull-request.md` for the body and hook requirements. A `feat` PR also needs an "Eval" section and a session that invoked `/claude-api build-eval`; read `~/.claude/docs/rule-guides/features-start-with-an-eval.md` before you build the feature.
31
26
 
32
- **Open every pull request ready for review.** Pass `--draft` only when the owner asks
33
- for a draft.
27
+ **Open PRs ready for review; use `--draft` when the owner requests it.**
34
28
 
35
29
  **A release bot's PR body is machine input. Leave it alone.** Release automation reads
36
30
  back the body of its own merged pull request to decide it owns that merge. Rewriting the
37
31
  body, or trimming its header or footer, makes the bot treat the merge as somebody else's
38
- work: it cuts no tag, the publish job skips, and it opens one more release pull request on
39
- the next run. The merge stays in the repository. No tag is cut and the package never publishes.
32
+ work: it cuts no tag, skips publishing, and opens another release PR on the next run.
40
33
 
41
34
  Spot one by its head branch, which starts `release-please--branches--`, or by a body that
42
35
  opens with the bot's own marker line. The description rules in this file, the
@@ -0,0 +1,109 @@
1
+ import { lstatSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+
4
+ export const SOL_PROMPT_DIRECTORY_NAME = 'system-prompts';
5
+ export const SOL_PROMPT_FILE_NAME = 'codex-sol.md';
6
+ export const CODEX_CONFIG_FILE_NAME = 'config.toml';
7
+ export const SOL_SETTING_MARKER = '# claude-dev-env trimmed Sol prompt. A claude-dev-env install rewrites the next line.';
8
+ const INSTRUCTIONS_KEY_PATTERN = /^\s*model_instructions_file\s*=/;
9
+ const TABLE_HEADER_PATTERN = /^\s*\[/;
10
+
11
+ /**
12
+ * Return the two config.toml lines that point Codex at the trimmed prompt.
13
+ *
14
+ * `model_instructions_file` replaces the selected model's built-in prompt with
15
+ * the file it names, for every model a run selects. JSON string escaping is
16
+ * valid TOML basic-string escaping, so a Windows path keeps its backslashes.
17
+ *
18
+ * @param {string} instructionsPath Absolute path of the installed prompt file.
19
+ * @returns {string[]}
20
+ */
21
+ export function solSettingLines(instructionsPath) {
22
+ return [SOL_SETTING_MARKER, `model_instructions_file = ${JSON.stringify(instructionsPath)}`];
23
+ }
24
+
25
+ function isPackageMarkerLine(eachLine) {
26
+ return eachLine.replace(/\r$/, '') === SOL_SETTING_MARKER;
27
+ }
28
+
29
+ function withoutPackageSetting(allLines) {
30
+ const allKeptLines = [];
31
+ for (let i = 0; i < allLines.length; i++) {
32
+ if (isPackageMarkerLine(allLines[i])) {
33
+ i++;
34
+ continue;
35
+ }
36
+ allKeptLines.push(allLines[i]);
37
+ }
38
+ return allKeptLines;
39
+ }
40
+
41
+ function hasUserInstructionsSetting(allLines) {
42
+ for (const eachLine of allLines) {
43
+ if (TABLE_HEADER_PATTERN.test(eachLine)) return false;
44
+ if (INSTRUCTIONS_KEY_PATTERN.test(eachLine)) return true;
45
+ }
46
+ return false;
47
+ }
48
+
49
+ function readCodexConfig(configPath) {
50
+ const configEntry = lstatSync(configPath, { throwIfNoEntry: false });
51
+ if (!configEntry) return { isReadable: true, currentText: null };
52
+ if (configEntry.isDirectory()) return { isReadable: false, currentText: null };
53
+ return { isReadable: true, currentText: readFileSync(configPath, 'utf8') };
54
+ }
55
+
56
+ export function codexSolSettingSnapshotPaths(codexHome) {
57
+ const configPath = join(codexHome, CODEX_CONFIG_FILE_NAME);
58
+ const { isReadable, currentText } = readCodexConfig(configPath);
59
+ return isReadable && currentText !== null ? [configPath] : [];
60
+ }
61
+
62
+ /**
63
+ * Point CODEX_HOME/config.toml's `model_instructions_file` at the installed Sol prompt.
64
+ *
65
+ * The package line sits at the top of the file, before any table, so it is a
66
+ * top-level key. A top-level `model_instructions_file` the user wrote keeps
67
+ * the file as it is, and so does a directory at the config path.
68
+ *
69
+ * @param {string} codexHome
70
+ * @param {string} instructionsPath Absolute path of the installed prompt file.
71
+ * @returns {string|null} The config path when the file changed, and null otherwise.
72
+ */
73
+ export function writeCodexSolSetting(codexHome, instructionsPath) {
74
+ const configPath = join(codexHome, CODEX_CONFIG_FILE_NAME);
75
+ const { isReadable, currentText } = readCodexConfig(configPath);
76
+ if (!isReadable) return null;
77
+ const allUserLines = withoutPackageSetting((currentText ?? '').split('\n'));
78
+ if (hasUserInstructionsSetting(allUserLines)) return null;
79
+ const userText = allUserLines.join('\n');
80
+ const configText = [...solSettingLines(instructionsPath), userText === '' ? '' : userText].join('\n');
81
+ if (currentText === configText) return null;
82
+ mkdirSync(codexHome, { recursive: true });
83
+ writeFileSync(configPath, configText, 'utf8');
84
+ return configPath;
85
+ }
86
+
87
+ /**
88
+ * Remove the package's `model_instructions_file` line from CODEX_HOME/config.toml.
89
+ *
90
+ * The rest of the file stays as the user wrote it. A file that held only the
91
+ * package line is removed.
92
+ *
93
+ * @param {string} codexHome
94
+ * @returns {string|null} The config path when it changed, or null when nothing was removed.
95
+ */
96
+ export function removeCodexSolSetting(codexHome) {
97
+ const configPath = join(codexHome, CODEX_CONFIG_FILE_NAME);
98
+ const { isReadable, currentText } = readCodexConfig(configPath);
99
+ if (!isReadable || currentText === null) return null;
100
+ const allLines = currentText.split('\n');
101
+ if (!allLines.some(isPackageMarkerLine)) return null;
102
+ const userText = withoutPackageSetting(allLines).join('\n');
103
+ if (userText.trim() === '') {
104
+ unlinkSync(configPath);
105
+ } else {
106
+ writeFileSync(configPath, userText, 'utf8');
107
+ }
108
+ return configPath;
109
+ }
@@ -0,0 +1,226 @@
1
+ import { test } from 'node:test';
2
+ import { strict as assert } from 'node:assert';
3
+ import { spawnSync } from 'node:child_process';
4
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
5
+ import { tmpdir } from 'node:os';
6
+ import { join } from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+ import {
9
+ CODEX_CONFIG_FILE_NAME,
10
+ codexSolSettingSnapshotPaths,
11
+ removeCodexSolSetting,
12
+ solSettingLines,
13
+ writeCodexSolSetting,
14
+ } from './codex-trimmed-sol.mjs';
15
+
16
+ const packageDirectory = fileURLToPath(new URL('..', import.meta.url));
17
+ const installerPath = join(packageDirectory, 'bin', 'install.mjs');
18
+ const shippedPromptPath = join(packageDirectory, 'system-prompts', 'codex-sol.md');
19
+
20
+ function makeScratchHome(context) {
21
+ const homeDirectory = mkdtempSync(join(tmpdir(), 'cde-sol-setting-'));
22
+ context.after(() => rmSync(homeDirectory, { recursive: true, force: true }));
23
+ return homeDirectory;
24
+ }
25
+
26
+ function installInScratchHome(homeDirectory, argumentsList = [], environment = {}) {
27
+ return spawnSync(process.execPath, [installerPath, ...argumentsList], {
28
+ cwd: packageDirectory,
29
+ encoding: 'utf8',
30
+ env: {
31
+ ...process.env,
32
+ CLAUDE_CONFIG_DIR: undefined,
33
+ LLM_SETTINGS_PROFILES_ROOT: undefined,
34
+ CDE_INSTALL_PSTACK: '0',
35
+ CDE_INSTALL_USAGE_WRAPUP: '0',
36
+ CDE_INSTALL_SUBAGENT_MODELS: '0',
37
+ CLAUDE_DEV_ENV_INSTALL_FAULT: undefined,
38
+ HOME: homeDirectory,
39
+ USERPROFILE: homeDirectory,
40
+ CODEX_HOME: join(homeDirectory, '.codex'),
41
+ GIT_CONFIG_GLOBAL: join(homeDirectory, '.gitconfig'),
42
+ TEMP: homeDirectory,
43
+ TMP: homeDirectory,
44
+ ...environment,
45
+ },
46
+ });
47
+ }
48
+
49
+ function settingText(instructionsPath, userText = '') {
50
+ return [...solSettingLines(instructionsPath), userText].join('\n');
51
+ }
52
+
53
+ test('setting lines point model_instructions_file at the prompt and name no model', () => {
54
+ const allLines = solSettingLines('/home/user/.claude/system-prompts/codex-sol.md');
55
+ assert.ok(allLines.includes('model_instructions_file = "/home/user/.claude/system-prompts/codex-sol.md"'));
56
+ assert.equal(allLines.some((eachLine) => /^\s*model\s*=/.test(eachLine)), false);
57
+ });
58
+
59
+ test('setting lines escape a Windows path as a TOML basic string', () => {
60
+ assert.ok(solSettingLines('C:\\Users\\user\\.claude\\system-prompts\\codex-sol.md').includes(
61
+ 'model_instructions_file = "C:\\\\Users\\\\user\\\\.claude\\\\system-prompts\\\\codex-sol.md"',
62
+ ));
63
+ });
64
+
65
+ test('writing twice changes the config once and keeps user settings below the line', (context) => {
66
+ const codexHome = join(makeScratchHome(context), '.codex');
67
+ const configPath = join(codexHome, CODEX_CONFIG_FILE_NAME);
68
+ const userText = 'model = "gpt-6.1-sol"\n\n[features]\nweb_search = true\n';
69
+ mkdirSync(codexHome, { recursive: true });
70
+ writeFileSync(configPath, userText);
71
+ assert.equal(writeCodexSolSetting(codexHome, '/prompt.md'), configPath);
72
+ assert.equal(writeCodexSolSetting(codexHome, '/prompt.md'), null);
73
+ assert.equal(readFileSync(configPath, 'utf8'), settingText('/prompt.md', userText));
74
+ assert.equal(writeCodexSolSetting(codexHome, '/moved/prompt.md'), configPath);
75
+ assert.equal(readFileSync(configPath, 'utf8'), settingText('/moved/prompt.md', userText));
76
+ assert.deepEqual(codexSolSettingSnapshotPaths(codexHome), [configPath]);
77
+ assert.equal(removeCodexSolSetting(codexHome), configPath);
78
+ assert.equal(readFileSync(configPath, 'utf8'), userText);
79
+ });
80
+
81
+ test('a config saved with CRLF line endings still has its package line rewritten and removed', (context) => {
82
+ const codexHome = join(makeScratchHome(context), '.codex');
83
+ const configPath = join(codexHome, CODEX_CONFIG_FILE_NAME);
84
+ const userText = 'model = "gpt-6.1-sol"\r\n';
85
+ mkdirSync(codexHome, { recursive: true });
86
+ writeFileSync(configPath, settingText('/prompt.md', userText).replace(/\n/g, '\r\n').replace(/\r\r/g, '\r'));
87
+ assert.equal(writeCodexSolSetting(codexHome, '/moved/prompt.md'), configPath);
88
+ assert.equal(readFileSync(configPath, 'utf8'), settingText('/moved/prompt.md', userText));
89
+ writeFileSync(configPath, readFileSync(configPath, 'utf8').replace(/\r?\n/g, '\r\n'));
90
+ assert.equal(removeCodexSolSetting(codexHome), configPath);
91
+ assert.equal(readFileSync(configPath, 'utf8'), userText);
92
+ });
93
+
94
+ test('a user-written top-level model_instructions_file is neither rewritten nor removed', (context) => {
95
+ const codexHome = join(makeScratchHome(context), '.codex');
96
+ const configPath = join(codexHome, CODEX_CONFIG_FILE_NAME);
97
+ const userText = 'model_instructions_file = "/mine.md"\n';
98
+ mkdirSync(codexHome, { recursive: true });
99
+ writeFileSync(configPath, userText);
100
+ assert.equal(writeCodexSolSetting(codexHome, '/prompt.md'), null);
101
+ assert.equal(removeCodexSolSetting(codexHome), null);
102
+ assert.equal(readFileSync(configPath, 'utf8'), userText);
103
+ });
104
+
105
+ test('a model_instructions_file inside a table does not block the top-level setting', (context) => {
106
+ const codexHome = join(makeScratchHome(context), '.codex');
107
+ const configPath = join(codexHome, CODEX_CONFIG_FILE_NAME);
108
+ const userText = '[profiles.other]\nmodel_instructions_file = "/other.md"\n';
109
+ mkdirSync(codexHome, { recursive: true });
110
+ writeFileSync(configPath, userText);
111
+ assert.equal(writeCodexSolSetting(codexHome, '/prompt.md'), configPath);
112
+ assert.equal(readFileSync(configPath, 'utf8'), settingText('/prompt.md', userText));
113
+ });
114
+
115
+ test('a directory at the config path is left alone', (context) => {
116
+ const codexHome = join(makeScratchHome(context), '.codex');
117
+ mkdirSync(join(codexHome, CODEX_CONFIG_FILE_NAME), { recursive: true });
118
+ assert.equal(writeCodexSolSetting(codexHome, '/prompt.md'), null);
119
+ assert.equal(removeCodexSolSetting(codexHome), null);
120
+ assert.deepEqual(codexSolSettingSnapshotPaths(codexHome), []);
121
+ });
122
+
123
+ test('a missing config needs no prior snapshot', (context) => {
124
+ assert.deepEqual(codexSolSettingSnapshotPaths(join(makeScratchHome(context), '.codex')), []);
125
+ });
126
+
127
+ test('full install points config.toml model_instructions_file at the installed trimmed prompt', (context) => {
128
+ const homeDirectory = makeScratchHome(context);
129
+ const configPath = join(homeDirectory, '.codex', CODEX_CONFIG_FILE_NAME);
130
+ const installedPromptPath = join(homeDirectory, '.claude', 'system-prompts', 'codex-sol.md');
131
+
132
+ const firstInstallation = installInScratchHome(homeDirectory);
133
+ assert.equal(firstInstallation.status, 0, firstInstallation.stdout + firstInstallation.stderr);
134
+ assert.equal(readFileSync(configPath, 'utf8'), settingText(installedPromptPath));
135
+ assert.equal(readFileSync(installedPromptPath, 'utf8'), readFileSync(shippedPromptPath, 'utf8'));
136
+
137
+ const secondInstallation = installInScratchHome(homeDirectory);
138
+ assert.equal(secondInstallation.status, 0, secondInstallation.stdout + secondInstallation.stderr);
139
+ assert.equal(secondInstallation.stdout.includes('Codex trimmed Sol prompt setting'), false);
140
+ assert.equal(readFileSync(configPath, 'utf8'), settingText(installedPromptPath));
141
+ });
142
+
143
+ test('uninstall removes the package line and keeps the user config', (context) => {
144
+ const homeDirectory = makeScratchHome(context);
145
+ const codexHome = join(homeDirectory, '.codex');
146
+ const configPath = join(codexHome, CODEX_CONFIG_FILE_NAME);
147
+ const userText = 'approval_policy = "never"\n';
148
+ mkdirSync(codexHome, { recursive: true });
149
+ writeFileSync(configPath, userText);
150
+ const installation = installInScratchHome(homeDirectory);
151
+ assert.equal(installation.status, 0, installation.stdout + installation.stderr);
152
+
153
+ const uninstallation = installInScratchHome(homeDirectory, ['--uninstall']);
154
+ assert.equal(uninstallation.status, 0, uninstallation.stdout + uninstallation.stderr);
155
+ assert.equal(readFileSync(configPath, 'utf8'), userText);
156
+ });
157
+
158
+ test('uninstall removes a config that held only the package line', (context) => {
159
+ const homeDirectory = makeScratchHome(context);
160
+ const configPath = join(homeDirectory, '.codex', CODEX_CONFIG_FILE_NAME);
161
+ const installation = installInScratchHome(homeDirectory);
162
+ assert.equal(installation.status, 0, installation.stdout + installation.stderr);
163
+ const uninstallation = installInScratchHome(homeDirectory, ['--uninstall']);
164
+ assert.equal(uninstallation.status, 0, uninstallation.stdout + uninstallation.stderr);
165
+ assert.equal(existsSync(configPath), false);
166
+ });
167
+
168
+ test('failed uninstall restores the config with the package line', (context) => {
169
+ const homeDirectory = makeScratchHome(context);
170
+ const configPath = join(homeDirectory, '.codex', CODEX_CONFIG_FILE_NAME);
171
+ const installation = installInScratchHome(homeDirectory);
172
+ assert.equal(installation.status, 0, installation.stdout + installation.stderr);
173
+ const priorConfig = readFileSync(configPath, 'utf8');
174
+
175
+ const uninstallation = installInScratchHome(homeDirectory, ['--uninstall'], {
176
+ CLAUDE_DEV_ENV_INSTALL_FAULT: 'after_file_staging',
177
+ });
178
+ assert.notEqual(uninstallation.status, 0);
179
+ assert.match(uninstallation.stderr, /prior installation restored/);
180
+ assert.equal(readFileSync(configPath, 'utf8'), priorConfig);
181
+ });
182
+
183
+ test('failed install restores a user config shared by two homes', (context) => {
184
+ const firstHome = makeScratchHome(context);
185
+ const secondHome = makeScratchHome(context);
186
+ const codexHome = join(firstHome, '.codex');
187
+ const configPath = join(codexHome, CODEX_CONFIG_FILE_NAME);
188
+ const userText = 'approval_policy = "never"\n';
189
+ mkdirSync(codexHome, { recursive: true });
190
+ writeFileSync(configPath, userText);
191
+
192
+ const failedInstallation = installInScratchHome(secondHome, [], {
193
+ CODEX_HOME: codexHome,
194
+ CLAUDE_DEV_ENV_INSTALL_FAULT: 'after_file_staging',
195
+ });
196
+ assert.notEqual(failedInstallation.status, 0);
197
+ assert.match(failedInstallation.stderr, /prior installation restored/);
198
+ assert.ok(failedInstallation.stdout.includes('Codex trimmed Sol prompt setting'));
199
+ assert.equal(readFileSync(configPath, 'utf8'), userText);
200
+ });
201
+
202
+ test('failed install removes a newly created config', (context) => {
203
+ const homeDirectory = makeScratchHome(context);
204
+ const installation = installInScratchHome(homeDirectory, [], {
205
+ CLAUDE_DEV_ENV_INSTALL_FAULT: 'after_file_staging',
206
+ });
207
+ assert.notEqual(installation.status, 0);
208
+ assert.match(installation.stderr, /prior installation restored/);
209
+ assert.ok(!existsSync(join(homeDirectory, '.codex', CODEX_CONFIG_FILE_NAME)));
210
+ });
211
+
212
+ test('failed install leaves a directory at the config path unchanged', (context) => {
213
+ const firstHome = makeScratchHome(context);
214
+ const secondHome = makeScratchHome(context);
215
+ const codexHome = join(firstHome, '.codex');
216
+ const sentinelPath = join(codexHome, CODEX_CONFIG_FILE_NAME, 'keep.txt');
217
+ mkdirSync(join(codexHome, CODEX_CONFIG_FILE_NAME), { recursive: true });
218
+ writeFileSync(sentinelPath, 'keep\n');
219
+ const installation = installInScratchHome(secondHome, [], {
220
+ CODEX_HOME: codexHome,
221
+ CLAUDE_DEV_ENV_INSTALL_FAULT: 'after_file_staging',
222
+ });
223
+ assert.notEqual(installation.status, 0);
224
+ assert.match(installation.stderr, /prior installation restored/);
225
+ assert.equal(readFileSync(sentinelPath, 'utf8'), 'keep\n');
226
+ });
package/bin/install.mjs CHANGED
@@ -60,6 +60,13 @@ import {
60
60
  shouldInstallMarketplacePlugin,
61
61
  } from './install-pstack-plugin.mjs';
62
62
  import { seedCodexPstackModels } from './seed-codex-pstack-models.mjs';
63
+ import {
64
+ codexSolSettingSnapshotPaths,
65
+ removeCodexSolSetting,
66
+ SOL_PROMPT_DIRECTORY_NAME,
67
+ SOL_PROMPT_FILE_NAME,
68
+ writeCodexSolSetting,
69
+ } from './codex-trimmed-sol.mjs';
63
70
  import {
64
71
  removeCodexPackageGuidance,
65
72
  removeCodexQuestionGuidance,
@@ -2631,6 +2638,7 @@ function executeInstallPlan(plan) {
2631
2638
  priorManifestFiles: [
2632
2639
  ...(plan.priorManifest.files || []),
2633
2640
  join(INSTALL_ROOT_RESOLUTION.codexHomeDirectory, 'AGENTS.md'),
2641
+ ...codexSolSettingSnapshotPaths(INSTALL_ROOT_RESOLUTION.codexHomeDirectory),
2634
2642
  ],
2635
2643
  journalParentDirectory: join(CLAUDE_HOME, TRANSACTION_JOURNAL_DIRECTORY_NAME),
2636
2644
  });
@@ -2912,6 +2920,14 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
2912
2920
  readFileSync(join(PACKAGE_ROOT, 'rules', 'question-presentation.md'), 'utf8'),
2913
2921
  );
2914
2922
  if (questionGuidancePath) allCodexGuidancePaths.push(questionGuidancePath);
2923
+ const solSettingPath = writeCodexSolSetting(
2924
+ INSTALL_ROOT_RESOLUTION.codexHomeDirectory,
2925
+ join(CLAUDE_HOME, SOL_PROMPT_DIRECTORY_NAME, SOL_PROMPT_FILE_NAME),
2926
+ );
2927
+ if (solSettingPath) {
2928
+ allCodexGuidancePaths.push(solSettingPath);
2929
+ console.log(` \u2713 ${solSettingPath} (Codex trimmed Sol prompt setting)`);
2930
+ }
2915
2931
  }
2916
2932
  syncWrittenPaths([...allInstalledFiles, ...allCodexGuidancePaths, ...publishedPointerPaths]);
2917
2933
  throwIfFault(FAULT_PHASES.AFTER_FILE_STAGING);
@@ -3272,6 +3288,7 @@ function executeUninstallPlan(plan, helpers = {}) {
3272
3288
  }
3273
3289
  removeCodexPackageGuidance(INSTALL_ROOT_RESOLUTION.codexHomeDirectory);
3274
3290
  removeCodexQuestionGuidance(INSTALL_ROOT_RESOLUTION.codexHomeDirectory);
3291
+ removeCodexSolSetting(INSTALL_ROOT_RESOLUTION.codexHomeDirectory);
3275
3292
  throwIfFault(FAULT_PHASES.AFTER_FILE_STAGING);
3276
3293
 
3277
3294
  if (existsSync(plan.settingsPath)) {
@@ -3365,6 +3382,7 @@ function uninstall() {
3365
3382
  priorManifestFiles: [
3366
3383
  ...plan.removableFiles,
3367
3384
  join(INSTALL_ROOT_RESOLUTION.codexHomeDirectory, 'AGENTS.md'),
3385
+ ...codexSolSettingSnapshotPaths(INSTALL_ROOT_RESOLUTION.codexHomeDirectory),
3368
3386
  ],
3369
3387
  journalParentDirectory: join(CLAUDE_HOME, TRANSACTION_JOURNAL_DIRECTORY_NAME),
3370
3388
  });
@@ -51,7 +51,7 @@ test('full install with pstack disabled delivers one question policy to Claude a
51
51
  assert.equal(readFileSync(join(homeDirectory, '.claude', 'rules', 'question-presentation.md'), 'utf8'), policyText);
52
52
  assert.match(readFileSync(codexGuidancePath, 'utf8'), /<!-- claude-dev-env question presentation: start -->/);
53
53
  assert.ok(readFileSync(codexGuidancePath, 'utf8').includes(policyText));
54
- assert.equal(readFileSync(codexHooksPath, 'utf8'), hooksConfiguration);
54
+ assert.ok(readFileSync(codexHooksPath, 'utf8').endsWith(hooksConfiguration));
55
55
  const manifestPath = join(homeDirectory, '.claude', '.claude-dev-env-manifest.json');
56
56
  assert.equal(JSON.parse(readFileSync(manifestPath, 'utf8')).files.includes(codexGuidancePath), false);
57
57
  });
@@ -127,3 +127,29 @@ Once installed, that is `~/.claude/scripts/account_broker.py`.
127
127
  |---|---|
128
128
  | `_shared/pr-loop/scripts/check_convergence.py` | Requires a Codex clean stamp on HEAD only on `normal`. `luna`, `wait`, or a failed broker skips the Codex gate |
129
129
  | `_shared/advisor/scripts/codex_astra_advisor.py` | Binds Astra only on `normal`, and runs Codex with `CODEX_HOME` set to `decision.home` |
130
+
131
+ ## Trimmed Sol prompt
132
+
133
+ The installer writes one top-level line into `~/.codex/config.toml`, under a
134
+ `# claude-dev-env trimmed Sol prompt` marker. The line points
135
+ `model_instructions_file` at `~/.claude/system-prompts/codex-sol.md`. The file
136
+ is Codex's default `gpt-6.1-sol` prompt with the lines that conflict with the
137
+ package rules rewritten. The line names no model, so every Codex run uses the
138
+ file, whichever model it selects. `sync` links `config.toml` into every account
139
+ home, so every account gets the line.
140
+
141
+ A `model_instructions_file` the user wrote at the top level keeps the config as
142
+ it is. Uninstall removes only the package line.
143
+
144
+ To see which prompt Codex uses, read the setting:
145
+
146
+ ```
147
+ Select-String -Path "$HOME\.codex\config.toml" -Pattern model_instructions_file
148
+ ```
149
+
150
+ To run Codex on the trimmed prompt through the broker, run it as usual. `run`
151
+ reads all of standard input before it starts Codex, so close it:
152
+
153
+ ```
154
+ python ~/.claude/scripts/account_broker.py run --product codex --report <report.json> -- codex exec "<request>" < /dev/null
155
+ ```