claude-dev-env 8.32.0 → 8.32.1

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.
@@ -50,7 +50,8 @@ function withPackageGuidanceBlock(guidanceText, packageGuidanceText) {
50
50
  PACKAGE_GUIDANCE_BLOCK_END,
51
51
  block,
52
52
  );
53
- return replacedText ?? `${guidanceText}\n${block}`;
53
+ if (replacedText !== null) return replacedText;
54
+ return guidanceText ? `${guidanceText}\n${block}` : block;
54
55
  }
55
56
 
56
57
  function comparablePath(filePath) {
@@ -66,28 +67,14 @@ function linksToPackageGuidance(agentsPath, allPackageGuidancePaths) {
66
67
  return allPackageGuidancePaths.some(eachPath => comparablePath(eachPath) === linkTarget);
67
68
  }
68
69
 
69
- /**
70
- * Keep CODEX_HOME/AGENTS.md carrying the skill-load block.
71
- *
72
- * A symbolic link that points at the package guidance (the shared agents-home
73
- * copy, or the retired copy under the Claude home) is replaced by a regular
74
- * file holding the skill-load block and a package-guidance block, so Codex
75
- * keeps both after the package moves its guidance. A file that already holds
76
- * the package-guidance block gets the current guidance text. A link to any
77
- * other file is left alone.
78
- *
79
- * Returns the file path when the file changed, and null otherwise.
80
- */
81
- export function writeCodexAgentsGuidance(codexHome, packageGuidanceText, allPackageGuidancePaths) {
70
+ function writeCodexGuidanceFile(codexHome, allPackageGuidancePaths, transformGuidance) {
82
71
  const agentsPath = join(codexHome, 'AGENTS.md');
83
72
  const agentsEntry = lstatSync(agentsPath, { throwIfNoEntry: false });
73
+ if (agentsEntry && !agentsEntry.isFile() && !agentsEntry.isSymbolicLink()) return null;
84
74
  const isPackageGuidanceLink = agentsEntry?.isSymbolicLink() ?? false;
85
75
  if (isPackageGuidanceLink && !linksToPackageGuidance(agentsPath, allPackageGuidancePaths)) return null;
86
76
  const currentText = agentsEntry && !isPackageGuidanceLink ? readFileSync(agentsPath, 'utf8') : '';
87
- let updatedText = withSkillLoadBlock(currentText);
88
- if (isPackageGuidanceLink || updatedText.includes(PACKAGE_GUIDANCE_BLOCK_START)) {
89
- updatedText = withPackageGuidanceBlock(updatedText, packageGuidanceText);
90
- }
77
+ const updatedText = transformGuidance(currentText);
91
78
  if (!isPackageGuidanceLink && updatedText === currentText) return null;
92
79
  mkdirSync(codexHome, { recursive: true });
93
80
  if (isPackageGuidanceLink) unlinkSync(agentsPath);
@@ -95,6 +82,39 @@ export function writeCodexAgentsGuidance(codexHome, packageGuidanceText, allPack
95
82
  return agentsPath;
96
83
  }
97
84
 
85
+ /**
86
+ * Keep CODEX_HOME/AGENTS.md carrying the skill-load block and the package guidance.
87
+ *
88
+ * The skill-load block goes first, and the package-guidance block holds the
89
+ * current guidance text. A symbolic link that points at the package guidance
90
+ * (the shared agents-home copy, or the retired copy under the Claude home) is
91
+ * replaced by a regular file holding both blocks, so Codex keeps both after
92
+ * the package moves its guidance. A link to any other file is left alone.
93
+ *
94
+ * Returns the file path when the file changed, and null otherwise.
95
+ */
96
+ export function writeCodexAgentsGuidance(codexHome, packageGuidanceText, allPackageGuidancePaths) {
97
+ return writeCodexGuidanceFile(
98
+ codexHome,
99
+ allPackageGuidancePaths,
100
+ currentText => withPackageGuidanceBlock(withSkillLoadBlock(currentText), packageGuidanceText),
101
+ );
102
+ }
103
+
104
+ /**
105
+ * Keep CODEX_HOME/AGENTS.md carrying the package guidance, with no skill-load block.
106
+ *
107
+ * Links follow the same rule as writeCodexAgentsGuidance. Returns the file
108
+ * path when the file changed, and null otherwise.
109
+ */
110
+ export function writeCodexPackageGuidance(codexHome, packageGuidanceText, allPackageGuidancePaths) {
111
+ return writeCodexGuidanceFile(
112
+ codexHome,
113
+ allPackageGuidancePaths,
114
+ currentText => withPackageGuidanceBlock(currentText, packageGuidanceText),
115
+ );
116
+ }
117
+
98
118
  export function writeCodexQuestionGuidance(codexHome, policyText) {
99
119
  const agentsPath = join(codexHome, 'AGENTS.md');
100
120
  const agentsEntry = lstatSync(agentsPath, { throwIfNoEntry: false });
@@ -112,30 +132,63 @@ export function writeCodexQuestionGuidance(codexHome, policyText) {
112
132
  return agentsPath;
113
133
  }
114
134
 
115
- export function hasOnlyQuestionPresentationBlock(guidanceText) {
116
- if (!guidanceText.startsWith(`${QUESTION_PRESENTATION_BLOCK_START}\n`)) return false;
117
- if (!guidanceText.endsWith(`${QUESTION_PRESENTATION_BLOCK_END}\n`)) return false;
118
- return withBlockReplaced(
119
- guidanceText, QUESTION_PRESENTATION_BLOCK_START, QUESTION_PRESENTATION_BLOCK_END, '',
120
- ) === '';
135
+ const ALL_PACKAGE_MANAGED_BLOCK_MARKERS = [
136
+ [SKILL_LOAD_BLOCK_START, SKILL_LOAD_BLOCK_END],
137
+ [PACKAGE_GUIDANCE_BLOCK_START, PACKAGE_GUIDANCE_BLOCK_END],
138
+ [QUESTION_PRESENTATION_BLOCK_START, QUESTION_PRESENTATION_BLOCK_END],
139
+ ];
140
+
141
+ /**
142
+ * Report whether the guidance holds at least one package-managed block and
143
+ * nothing else but blank lines.
144
+ */
145
+ export function hasOnlyPackageManagedBlocks(guidanceText) {
146
+ let remainingText = guidanceText;
147
+ let removedBlockCount = 0;
148
+ for (const [blockStart, blockEnd] of ALL_PACKAGE_MANAGED_BLOCK_MARKERS) {
149
+ const withoutBlock = withBlockReplaced(remainingText, blockStart, blockEnd, '');
150
+ if (withoutBlock === null) continue;
151
+ remainingText = withoutBlock;
152
+ removedBlockCount += 1;
153
+ }
154
+ return removedBlockCount > 0 && remainingText.trim() === '';
155
+ }
156
+
157
+ function removeCodexGuidanceBlock(codexHome, blockStart, blockEnd) {
158
+ const agentsPath = join(codexHome, 'AGENTS.md');
159
+ const agentsEntry = lstatSync(agentsPath, { throwIfNoEntry: false });
160
+ if (!agentsEntry?.isFile()) return null;
161
+ const currentText = readFileSync(agentsPath, 'utf8');
162
+ const updatedText = withBlockReplaced(currentText, blockStart, blockEnd, '');
163
+ if (updatedText === null) return null;
164
+ const isLeftEmpty = updatedText.trim() === '';
165
+ if (isLeftEmpty) unlinkSync(agentsPath);
166
+ if (!isLeftEmpty) writeFileSync(agentsPath, updatedText, 'utf8');
167
+ return agentsPath;
121
168
  }
122
169
 
123
170
  /**
124
171
  * Remove the managed question block while keeping surrounding guidance bytes.
125
172
  *
173
+ * A file left holding only blank lines is deleted.
174
+ *
126
175
  * @param {string} codexHome
127
176
  * @returns {string|null} The changed path, or null when no block was removed.
128
177
  */
129
178
  export function removeCodexQuestionGuidance(codexHome) {
130
- const agentsPath = join(codexHome, 'AGENTS.md');
131
- const agentsEntry = lstatSync(agentsPath, { throwIfNoEntry: false });
132
- if (!agentsEntry?.isFile()) return null;
133
- const currentText = readFileSync(agentsPath, 'utf8');
134
- const updatedText = withBlockReplaced(
135
- currentText, QUESTION_PRESENTATION_BLOCK_START, QUESTION_PRESENTATION_BLOCK_END, '',
179
+ return removeCodexGuidanceBlock(
180
+ codexHome, QUESTION_PRESENTATION_BLOCK_START, QUESTION_PRESENTATION_BLOCK_END,
136
181
  );
137
- if (updatedText === null) return null;
138
- if (updatedText === '') unlinkSync(agentsPath);
139
- if (updatedText !== '') writeFileSync(agentsPath, updatedText, 'utf8');
140
- return agentsPath;
182
+ }
183
+
184
+ /**
185
+ * Remove the managed package-guidance block while keeping surrounding guidance bytes.
186
+ *
187
+ * A file left holding only blank lines is deleted.
188
+ *
189
+ * @param {string} codexHome
190
+ * @returns {string|null} The changed path, or null when no block was removed.
191
+ */
192
+ export function removeCodexPackageGuidance(codexHome) {
193
+ return removeCodexGuidanceBlock(codexHome, PACKAGE_GUIDANCE_BLOCK_START, PACKAGE_GUIDANCE_BLOCK_END);
141
194
  }
@@ -11,9 +11,11 @@ import {
11
11
  SKILL_LOAD_BLOCK_END,
12
12
  SKILL_LOAD_BLOCK_START,
13
13
  SKILL_LOAD_INSTRUCTION,
14
+ removeCodexPackageGuidance,
14
15
  removeCodexQuestionGuidance,
15
16
  withSkillLoadBlock,
16
17
  writeCodexAgentsGuidance,
18
+ writeCodexPackageGuidance,
17
19
  writeCodexQuestionGuidance,
18
20
  } from './codex-skill-load-block.mjs';
19
21
 
@@ -50,16 +52,19 @@ test('the instruction names the skill and binds spawned helpers', () => {
50
52
  assert.match(SKILL_LOAD_INSTRUCTION, /spawn_agent/);
51
53
  });
52
54
 
53
- test('a missing Codex guidance file is created holding only the skill-load block', (context) => {
55
+ test('a missing Codex guidance file is created holding the skill-load block and the package guidance', (context) => {
54
56
  const homes = makeHomes(context);
55
57
 
56
58
  const writtenPath = writeGuidance(homes);
57
59
 
58
60
  assert.equal(writtenPath, homes.agentsPath);
59
- assert.equal(readFileSync(homes.agentsPath, 'utf8'), EXPECTED_BLOCK);
61
+ assert.equal(
62
+ readFileSync(homes.agentsPath, 'utf8'),
63
+ `${EXPECTED_BLOCK}\n${guidanceBlock(PACKAGE_GUIDANCE)}`,
64
+ );
60
65
  });
61
66
 
62
- test('existing guidance keeps every line and gains the skill-load block first', (context) => {
67
+ test('existing guidance keeps every line between the skill-load block and the package guidance', (context) => {
63
68
  const homes = makeHomes(context);
64
69
  mkdirSync(homes.codexHome);
65
70
  writeFileSync(homes.agentsPath, '# pstack model configuration\n\nbug-fix: gpt-6-astra\n');
@@ -68,7 +73,7 @@ test('existing guidance keeps every line and gains the skill-load block first',
68
73
 
69
74
  assert.equal(
70
75
  readFileSync(homes.agentsPath, 'utf8'),
71
- `${EXPECTED_BLOCK}\n# pstack model configuration\n\nbug-fix: gpt-6-astra\n`,
76
+ `${EXPECTED_BLOCK}\n# pstack model configuration\n\nbug-fix: gpt-6-astra\n\n${guidanceBlock(PACKAGE_GUIDANCE)}`,
72
77
  );
73
78
  });
74
79
 
@@ -138,6 +143,78 @@ test('a link to a file the package does not own is left alone', (context) => {
138
143
  assert.equal(readFileSync(ownGuidancePath, 'utf8'), 'My own Codex notes\n');
139
144
  });
140
145
 
146
+ function writePackageGuidance(homes) {
147
+ return writeCodexPackageGuidance(homes.codexHome, PACKAGE_GUIDANCE, homes.allPackageGuidancePaths);
148
+ }
149
+
150
+ test('package guidance creates a missing Codex file holding only the package block', (context) => {
151
+ const homes = makeHomes(context);
152
+
153
+ assert.equal(writePackageGuidance(homes), homes.agentsPath);
154
+ assert.equal(readFileSync(homes.agentsPath, 'utf8'), guidanceBlock(PACKAGE_GUIDANCE));
155
+ });
156
+
157
+ test('package guidance replaces its block in place and a repeat write changes nothing', (context) => {
158
+ const homes = makeHomes(context);
159
+ mkdirSync(homes.codexHome);
160
+ writeFileSync(homes.agentsPath, `Before\n${guidanceBlock('Old guidance\n')}After\n`);
161
+
162
+ assert.equal(writePackageGuidance(homes), homes.agentsPath);
163
+ const expectedGuidance = `Before\n${guidanceBlock(PACKAGE_GUIDANCE)}After\n`;
164
+ assert.equal(readFileSync(homes.agentsPath, 'utf8'), expectedGuidance);
165
+ assert.equal(writePackageGuidance(homes), null);
166
+ assert.equal(readFileSync(homes.agentsPath, 'utf8'), expectedGuidance);
167
+ });
168
+
169
+ test('package guidance appends its block after custom notes', (context) => {
170
+ const homes = makeHomes(context);
171
+ mkdirSync(homes.codexHome);
172
+ writeFileSync(homes.agentsPath, 'Custom agents\n');
173
+
174
+ writePackageGuidance(homes);
175
+
176
+ assert.equal(
177
+ readFileSync(homes.agentsPath, 'utf8'),
178
+ `Custom agents\n\n${guidanceBlock(PACKAGE_GUIDANCE)}`,
179
+ );
180
+ });
181
+
182
+ for (const linkName of ['retiredGuidancePath', 'sharedGuidancePath']) {
183
+ test(`package guidance turns a link at ${linkName} into a file with only the package block`, (context) => {
184
+ const homes = makeHomes(context);
185
+ mkdirSync(homes.codexHome);
186
+ symlinkSync(homes[linkName], homes.agentsPath);
187
+
188
+ assert.equal(writePackageGuidance(homes), homes.agentsPath);
189
+ assert.equal(lstatSync(homes.agentsPath).isSymbolicLink(), false);
190
+ assert.equal(readFileSync(homes.agentsPath, 'utf8'), guidanceBlock(PACKAGE_GUIDANCE));
191
+ });
192
+ }
193
+
194
+ test('package guidance leaves a link to a file the package does not own unchanged', (context) => {
195
+ const homes = makeHomes(context);
196
+ mkdirSync(homes.codexHome);
197
+ const ownGuidancePath = join(homes.root, 'notes', 'codex.md');
198
+ mkdirSync(join(homes.root, 'notes'));
199
+ writeFileSync(ownGuidancePath, 'My own Codex notes\n');
200
+ symlinkSync(ownGuidancePath, homes.agentsPath);
201
+
202
+ assert.equal(writePackageGuidance(homes), null);
203
+ assert.equal(lstatSync(homes.agentsPath).isSymbolicLink(), true);
204
+ assert.equal(readFileSync(ownGuidancePath, 'utf8'), 'My own Codex notes\n');
205
+ });
206
+
207
+ test('package guidance leaves an existing directory and its notes unchanged', (context) => {
208
+ const homes = makeHomes(context);
209
+ mkdirSync(homes.agentsPath, { recursive: true });
210
+ const notesPath = join(homes.agentsPath, 'notes.md');
211
+ writeFileSync(notesPath, 'Directory notes\n');
212
+
213
+ assert.equal(writePackageGuidance(homes), null);
214
+ assert.equal(lstatSync(homes.agentsPath).isDirectory(), true);
215
+ assert.equal(readFileSync(notesPath, 'utf8'), 'Directory notes\n');
216
+ });
217
+
141
218
  test('question guidance creates a missing Codex file and repeat writes preserve bytes', (context) => {
142
219
  const homes = makeHomes(context);
143
220
  const expectedBlock = `${QUESTION_PRESENTATION_BLOCK_START}\n${QUESTION_POLICY}${QUESTION_PRESENTATION_BLOCK_END}\n`;
@@ -245,6 +322,37 @@ test('question removal preserves a guidance directory and its notes', (context)
245
322
  assert.equal(readFileSync(notesPath, 'utf8'), 'Directory notes\n');
246
323
  });
247
324
 
325
+ test('package and question removal delete a file left with only blank lines', (context) => {
326
+ const homes = makeHomes(context);
327
+ writePackageGuidance(homes);
328
+ writeCodexQuestionGuidance(homes.codexHome, QUESTION_POLICY);
329
+
330
+ assert.equal(removeCodexPackageGuidance(homes.codexHome), homes.agentsPath);
331
+ assert.equal(removeCodexQuestionGuidance(homes.codexHome), homes.agentsPath);
332
+ assert.equal(lstatSync(homes.agentsPath, { throwIfNoEntry: false }), undefined);
333
+ });
334
+
335
+ test('package removal keeps the custom text around its block', (context) => {
336
+ const homes = makeHomes(context);
337
+ mkdirSync(homes.codexHome);
338
+ writeFileSync(homes.agentsPath, `Before\n${guidanceBlock(PACKAGE_GUIDANCE)}After\n`);
339
+
340
+ assert.equal(removeCodexPackageGuidance(homes.codexHome), homes.agentsPath);
341
+ assert.equal(readFileSync(homes.agentsPath, 'utf8'), 'Before\nAfter\n');
342
+ });
343
+
344
+ test('package removal leaves a linked file and its target unchanged', (context) => {
345
+ const homes = makeHomes(context);
346
+ mkdirSync(homes.codexHome);
347
+ mkdirSync(dirname(homes.sharedGuidancePath), { recursive: true });
348
+ writeFileSync(homes.sharedGuidancePath, guidanceBlock(PACKAGE_GUIDANCE));
349
+ symlinkSync(homes.sharedGuidancePath, homes.agentsPath);
350
+
351
+ assert.equal(removeCodexPackageGuidance(homes.codexHome), null);
352
+ assert.equal(lstatSync(homes.agentsPath).isSymbolicLink(), true);
353
+ assert.equal(readFileSync(homes.sharedGuidancePath, 'utf8'), guidanceBlock(PACKAGE_GUIDANCE));
354
+ });
355
+
248
356
  test('question removal leaves a missing guidance file absent', (context) => {
249
357
  const homes = makeHomes(context);
250
358
 
package/bin/install.mjs CHANGED
@@ -53,8 +53,10 @@ import {
53
53
  } from './install-pstack-plugin.mjs';
54
54
  import { seedCodexPstackModels } from './seed-codex-pstack-models.mjs';
55
55
  import {
56
+ removeCodexPackageGuidance,
56
57
  removeCodexQuestionGuidance,
57
58
  writeCodexAgentsGuidance,
59
+ writeCodexPackageGuidance,
58
60
  writeCodexQuestionGuidance,
59
61
  } from './codex-skill-load-block.mjs';
60
62
  import {
@@ -2606,7 +2608,7 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
2606
2608
  ];
2607
2609
 
2608
2610
  const allInstalledFiles = [];
2609
- const allQuestionGuidancePaths = [];
2611
+ const allCodexGuidancePaths = [];
2610
2612
  const allUserOwnedPreferencePaths = new Set();
2611
2613
  const summary = {};
2612
2614
  for (const directory of CONTENT_DIRECTORIES) {
@@ -2758,6 +2760,8 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
2758
2760
  console.log(` Pstack: retired release store removed from ${retiredPstack.removedStorePath}`);
2759
2761
  }
2760
2762
  }
2763
+ const packageGuidanceText = selectedGroups ? '' : readFileSync(join(PACKAGE_ROOT, 'AGENTS.md'), 'utf8');
2764
+ const allPackageGuidancePaths = [join(CLAUDE_HOME, 'AGENTS.md'), join(AGENTS_HOME, 'AGENTS.md')];
2761
2765
  if (!selectedGroups && shouldInstallPstackPlugin()) {
2762
2766
  const pstackPlugin = installPstackPluginForHosts();
2763
2767
  summary.pstackPlugin = pstackPlugin;
@@ -2773,8 +2777,8 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
2773
2777
  }
2774
2778
  const skillLoadGuidancePath = writeCodexAgentsGuidance(
2775
2779
  INSTALL_ROOT_RESOLUTION.codexHomeDirectory,
2776
- readFileSync(join(PACKAGE_ROOT, 'AGENTS.md'), 'utf8'),
2777
- [join(CLAUDE_HOME, 'AGENTS.md'), join(AGENTS_HOME, 'AGENTS.md')],
2780
+ packageGuidanceText,
2781
+ allPackageGuidancePaths,
2778
2782
  );
2779
2783
  if (skillLoadGuidancePath) {
2780
2784
  console.log(` \u2713 ${skillLoadGuidancePath} (Codex skill-load line)`);
@@ -2787,13 +2791,22 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
2787
2791
  }
2788
2792
  }
2789
2793
  if (!selectedGroups) {
2794
+ const packageGuidancePath = writeCodexPackageGuidance(
2795
+ INSTALL_ROOT_RESOLUTION.codexHomeDirectory,
2796
+ packageGuidanceText,
2797
+ allPackageGuidancePaths,
2798
+ );
2799
+ if (packageGuidancePath) {
2800
+ allCodexGuidancePaths.push(packageGuidancePath);
2801
+ console.log(` \u2713 ${packageGuidancePath} (Codex package guidance)`);
2802
+ }
2790
2803
  const questionGuidancePath = writeCodexQuestionGuidance(
2791
2804
  INSTALL_ROOT_RESOLUTION.codexHomeDirectory,
2792
2805
  readFileSync(join(PACKAGE_ROOT, 'rules', 'question-presentation.md'), 'utf8'),
2793
2806
  );
2794
- if (questionGuidancePath) allQuestionGuidancePaths.push(questionGuidancePath);
2807
+ if (questionGuidancePath) allCodexGuidancePaths.push(questionGuidancePath);
2795
2808
  }
2796
- syncWrittenPaths([...allInstalledFiles, ...allQuestionGuidancePaths, ...publishedPointerPaths]);
2809
+ syncWrittenPaths([...allInstalledFiles, ...allCodexGuidancePaths, ...publishedPointerPaths]);
2797
2810
  throwIfFault(FAULT_PHASES.AFTER_FILE_STAGING);
2798
2811
  throwIfFault(FAULT_PHASES.BEFORE_DURABLE_PROMOTION);
2799
2812
  const shouldInstallAnyHooks = shouldInstallAllHooks || (allowedHookFiles && allowedHookFiles.size > 0);
@@ -2834,7 +2847,7 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
2834
2847
  summary.hookGroups = totalHookGroups;
2835
2848
  summary.codexHookGroups = totalCodexHookGroups;
2836
2849
  console.log(` Hook groups: ${totalHookGroups} merged into settings.json, ${totalCodexHookGroups} merged into hooks.json`);
2837
- syncWrittenPaths([...allInstalledFiles, ...allQuestionGuidancePaths, ...publishedPointerPaths]);
2850
+ syncWrittenPaths([...allInstalledFiles, ...allCodexGuidancePaths, ...publishedPointerPaths]);
2838
2851
  throwIfFault(FAULT_PHASES.AFTER_SETTINGS_WRITE);
2839
2852
 
2840
2853
  console.warn(
@@ -2856,7 +2869,7 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
2856
2869
  console.warn(` Git hooks: ${gitHookInstallationResult.hooksPathConfigurationResult.reason}`);
2857
2870
  }
2858
2871
  console.log(` Git hook shims: ${gitHookInstallationResult.createdShimPaths.length} files (${KNOWN_GIT_HOOK_NAMES.join(', ')})`);
2859
- syncWrittenPaths([...allInstalledFiles, ...allQuestionGuidancePaths, ...publishedPointerPaths]);
2872
+ syncWrittenPaths([...allInstalledFiles, ...allCodexGuidancePaths, ...publishedPointerPaths]);
2860
2873
  throwIfFault(FAULT_PHASES.AFTER_GIT_CONFIG);
2861
2874
  throwIfFault(FAULT_PHASES.AFTER_LINK_PUBLICATION);
2862
2875
 
@@ -2876,7 +2889,7 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
2876
2889
  console.warn(` ${mypyIniInstallResult.expectedLine}`);
2877
2890
  }
2878
2891
  } else {
2879
- syncWrittenPaths([...allInstalledFiles, ...allQuestionGuidancePaths, ...publishedPointerPaths]);
2892
+ syncWrittenPaths([...allInstalledFiles, ...allCodexGuidancePaths, ...publishedPointerPaths]);
2880
2893
  throwIfFault(FAULT_PHASES.AFTER_SETTINGS_WRITE);
2881
2894
  throwIfFault(FAULT_PHASES.AFTER_GIT_CONFIG);
2882
2895
  throwIfFault(FAULT_PHASES.AFTER_LINK_PUBLICATION);
@@ -2964,7 +2977,7 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
2964
2977
  ? manifestFilesWithFailedPrunes(allManagedInstalledFiles, failedPrunePaths)
2965
2978
  : unionOnComparisonKey(priorManifestFiles || [], allManagedInstalledFiles);
2966
2979
  writeManifest(manifestFiles, manifestSkillNames, summary.managedPermissions.managedPermissions);
2967
- syncWrittenPaths([...allInstalledFiles, ...allQuestionGuidancePaths, MANIFEST_FILE, plan.settingsPath, ...publishedPointerPaths]);
2980
+ syncWrittenPaths([...allInstalledFiles, ...allCodexGuidancePaths, MANIFEST_FILE, plan.settingsPath, ...publishedPointerPaths]);
2968
2981
  throwIfFault(FAULT_PHASES.AFTER_MANIFEST_WRITE);
2969
2982
  console.log(`\nInstalled ${PACKAGE_NAME}:`);
2970
2983
  for (const directory of CONTENT_DIRECTORIES) {
@@ -3143,6 +3156,7 @@ function executeUninstallPlan(plan, helpers = {}) {
3143
3156
  ` ${plan.skippedFiles.length} manifest record(s) skipped — each names a path outside ${CLAUDE_HOME}, outside ${MYPY_INI_INSTALL_PATH}, outside ${INSTALL_ROOT_RESOLUTION.codexRulesInstallDirectory}, outside ${INSTALL_ROOT_RESOLUTION.cursorInstallDirectory}, and outside ${AGENTS_HOME}`,
3144
3157
  );
3145
3158
  }
3159
+ removeCodexPackageGuidance(INSTALL_ROOT_RESOLUTION.codexHomeDirectory);
3146
3160
  removeCodexQuestionGuidance(INSTALL_ROOT_RESOLUTION.codexHomeDirectory);
3147
3161
  throwIfFault(FAULT_PHASES.AFTER_FILE_STAGING);
3148
3162
 
@@ -81,7 +81,7 @@ test('uninstall removes question-only Codex guidance', (context) => {
81
81
  assert.equal(existsSync(codexGuidancePath), false);
82
82
  });
83
83
 
84
- test('uninstall removes the question block and preserves surrounding custom guidance', (context) => {
84
+ test('uninstall removes the managed blocks and preserves surrounding custom guidance', (context) => {
85
85
  const homeDirectory = makeScratchHome(context);
86
86
  const codexGuidancePath = join(homeDirectory, '.codex', 'AGENTS.md');
87
87
  mkdirSync(dirname(codexGuidancePath), { recursive: true });
@@ -94,7 +94,7 @@ test('uninstall removes the question block and preserves surrounding custom guid
94
94
  const removal = installInScratchHome(homeDirectory, ['--uninstall']);
95
95
 
96
96
  assert.equal(removal.status, 0, removal.stdout + removal.stderr);
97
- assert.equal(readFileSync(codexGuidancePath, 'utf8'), `${customGuidance}\nTrailing notes\n`);
97
+ assert.equal(readFileSync(codexGuidancePath, 'utf8'), `${customGuidance}\n\nTrailing notes\n`);
98
98
  });
99
99
 
100
100
  for (const eachPriorGuidance of [null, '# Personal guidance\n']) {
@@ -54,6 +54,8 @@ import {
54
54
  } from './install.mjs';
55
55
  import { EVER_SHIPPED_SKILL_NAMES } from './ever-shipped-skills.mjs';
56
56
  import {
57
+ PACKAGE_GUIDANCE_BLOCK_END,
58
+ PACKAGE_GUIDANCE_BLOCK_START,
57
59
  SKILL_LOAD_BLOCK_START,
58
60
  SKILL_LOAD_INSTRUCTION,
59
61
  QUESTION_PRESENTATION_BLOCK_START,
@@ -3162,6 +3164,109 @@ test('--no-pstack skips the plugin and delivers Codex question guidance', t => {
3162
3164
  );
3163
3165
  });
3164
3166
 
3167
+ const PSTACK_TEST_PACKAGE_GUIDANCE = readFileSync(
3168
+ join(dirname(fileURLToPath(import.meta.url)), '..', 'AGENTS.md'),
3169
+ 'utf8',
3170
+ );
3171
+
3172
+ function assertHoldsPackageGuidance(codexGuidance) {
3173
+ assert.ok(codexGuidance.includes(`${PACKAGE_GUIDANCE_BLOCK_START}\n${PSTACK_TEST_PACKAGE_GUIDANCE}`));
3174
+ assert.ok(codexGuidance.includes(PACKAGE_GUIDANCE_BLOCK_END));
3175
+ }
3176
+
3177
+ test('a fresh full install writes the package guidance into Codex AGENTS.md', t => {
3178
+ const sandbox = pstackPluginSandbox(t);
3179
+
3180
+ runPstackInstaller(sandbox.homeDirectory, [], sandbox.environment);
3181
+
3182
+ const codexGuidance = readFileSync(join(sandbox.homeDirectory, '.codex', 'AGENTS.md'), 'utf8');
3183
+ assertHoldsPackageGuidance(codexGuidance);
3184
+ assert.ok(codexGuidance.startsWith(`${SKILL_LOAD_BLOCK_START}\n${SKILL_LOAD_INSTRUCTION}\n`));
3185
+ assert.ok(codexGuidance.includes(QUESTION_PRESENTATION_BLOCK_START));
3186
+ });
3187
+
3188
+ test('--no-pstack writes package and question guidance without the skill-load block', t => {
3189
+ const sandbox = pstackPluginSandbox(t);
3190
+
3191
+ runPstackInstaller(sandbox.homeDirectory, ['--no-pstack'], sandbox.environment);
3192
+
3193
+ assert.deepEqual(
3194
+ sandbox.recordedCommands().filter(eachCommand => eachCommand.startsWith('codex plugin')),
3195
+ [],
3196
+ );
3197
+ const codexGuidance = readFileSync(join(sandbox.homeDirectory, '.codex', 'AGENTS.md'), 'utf8');
3198
+ assertHoldsPackageGuidance(codexGuidance);
3199
+ assert.ok(codexGuidance.includes(QUESTION_PRESENTATION_BLOCK_START));
3200
+ assert.equal(codexGuidance.includes(SKILL_LOAD_BLOCK_START), false);
3201
+ });
3202
+
3203
+ test('--no-pstack replaces an outdated package block and keeps the text around it', t => {
3204
+ const sandbox = pstackPluginSandbox(t);
3205
+ const codexHome = join(sandbox.homeDirectory, '.codex');
3206
+ mkdirSync(codexHome, { recursive: true });
3207
+ writeFileSync(
3208
+ join(codexHome, 'AGENTS.md'),
3209
+ `My notes\n${PACKAGE_GUIDANCE_BLOCK_START}\nOld guidance\n${PACKAGE_GUIDANCE_BLOCK_END}\nMore notes\n`,
3210
+ );
3211
+
3212
+ runPstackInstaller(sandbox.homeDirectory, ['--no-pstack'], sandbox.environment);
3213
+
3214
+ const codexGuidance = readFileSync(join(codexHome, 'AGENTS.md'), 'utf8');
3215
+ assert.ok(codexGuidance.startsWith(`My notes\n${PACKAGE_GUIDANCE_BLOCK_START}\n${PSTACK_TEST_PACKAGE_GUIDANCE}`));
3216
+ assert.ok(codexGuidance.includes(`${PACKAGE_GUIDANCE_BLOCK_END}\nMore notes\n`));
3217
+ assert.equal(codexGuidance.includes('Old guidance'), false);
3218
+ assert.equal(codexGuidance.includes(SKILL_LOAD_BLOCK_START), false);
3219
+ });
3220
+
3221
+ for (const linkTargetDirectory of ['.claude', '.agents']) {
3222
+ test(`--no-pstack turns a Codex link to the ${linkTargetDirectory} package guidance into a file`, t => {
3223
+ const sandbox = pstackPluginSandbox(t);
3224
+ const codexHome = join(sandbox.homeDirectory, '.codex');
3225
+ const codexAgentsPath = join(codexHome, 'AGENTS.md');
3226
+ mkdirSync(codexHome, { recursive: true });
3227
+ symlinkSync(join(sandbox.homeDirectory, linkTargetDirectory, 'AGENTS.md'), codexAgentsPath);
3228
+
3229
+ runPstackInstaller(sandbox.homeDirectory, ['--no-pstack'], sandbox.environment);
3230
+
3231
+ assert.equal(lstatSync(codexAgentsPath).isSymbolicLink(), false);
3232
+ const codexGuidance = readFileSync(codexAgentsPath, 'utf8');
3233
+ assertHoldsPackageGuidance(codexGuidance);
3234
+ assert.ok(codexGuidance.includes(QUESTION_PRESENTATION_BLOCK_START));
3235
+ assert.equal(codexGuidance.includes(SKILL_LOAD_BLOCK_START), false);
3236
+ });
3237
+ }
3238
+
3239
+ test('--no-pstack leaves a Codex link to a file the package does not own unchanged', t => {
3240
+ const sandbox = pstackPluginSandbox(t);
3241
+ const codexHome = join(sandbox.homeDirectory, '.codex');
3242
+ const codexAgentsPath = join(codexHome, 'AGENTS.md');
3243
+ const ownGuidancePath = join(sandbox.homeDirectory, 'notes', 'codex.md');
3244
+ mkdirSync(codexHome, { recursive: true });
3245
+ mkdirSync(dirname(ownGuidancePath), { recursive: true });
3246
+ writeFileSync(ownGuidancePath, 'My own Codex notes\n');
3247
+ symlinkSync(ownGuidancePath, codexAgentsPath);
3248
+
3249
+ runPstackInstaller(sandbox.homeDirectory, ['--no-pstack'], sandbox.environment);
3250
+
3251
+ assert.equal(lstatSync(codexAgentsPath).isSymbolicLink(), true);
3252
+ assert.equal(readFileSync(ownGuidancePath, 'utf8'), 'My own Codex notes\n');
3253
+ });
3254
+
3255
+ test('a pstack install after a --no-pstack install seeds the Codex model sheet', t => {
3256
+ const sandbox = pstackPluginSandbox(t);
3257
+ const codexHome = join(sandbox.homeDirectory, '.codex');
3258
+ runPstackInstaller(sandbox.homeDirectory, ['--no-pstack'], sandbox.environment);
3259
+
3260
+ runPstackInstaller(sandbox.homeDirectory, [], sandbox.environment);
3261
+
3262
+ assert.match(readFileSync(join(codexHome, 'pstack-models.md'), 'utf8'), /^session hook: on$/m);
3263
+ const codexGuidance = readFileSync(join(codexHome, 'AGENTS.md'), 'utf8');
3264
+ assert.match(codexGuidance, /^feature, refactoring: gpt-6-sol$/m);
3265
+ assert.ok(codexGuidance.startsWith(`${SKILL_LOAD_BLOCK_START}\n${SKILL_LOAD_INSTRUCTION}\n`));
3266
+ assertHoldsPackageGuidance(codexGuidance);
3267
+ assert.ok(codexGuidance.includes(QUESTION_PRESENTATION_BLOCK_START));
3268
+ });
3269
+
3165
3270
  function continuityCommandCount(configurationPath) {
3166
3271
  if (!existsSync(configurationPath)) return 0;
3167
3272
  const configuration = JSON.parse(readFileSync(configurationPath, 'utf8'));
@@ -1,7 +1,7 @@
1
1
  import { copyFileSync, lstatSync, mkdirSync, readFileSync, unlinkSync, writeFileSync, constants as filesystemConstants } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
- import { hasOnlyQuestionPresentationBlock } from './codex-skill-load-block.mjs';
4
+ import { hasOnlyPackageManagedBlocks } from './codex-skill-load-block.mjs';
5
5
 
6
6
  const CODEX_PRESET_PATH = fileURLToPath(new URL('../pstack-codex-models.md', import.meta.url));
7
7
 
@@ -16,7 +16,7 @@ export function seedCodexPstackModels(codexHome, writeAgentGuidance = writeFileS
16
16
  const agentsEntry = lstatSync(agentsPath, { throwIfNoEntry: false });
17
17
  if (agentsEntry && !agentsEntry.isFile()) return null;
18
18
  const existingGuidance = agentsEntry ? readFileSync(agentsPath, 'utf8') : '';
19
- if (agentsEntry && !hasOnlyQuestionPresentationBlock(existingGuidance)) return null;
19
+ if (agentsEntry && !hasOnlyPackageManagedBlocks(existingGuidance)) return null;
20
20
 
21
21
  const preset = readFileSync(CODEX_PRESET_PATH, 'utf8');
22
22
  const agentGuidance = preset.replace(/^session hook: on\n?$/m, '');
@@ -4,7 +4,14 @@ import { existsSync, lstatSync, mkdtempSync, mkdirSync, readFileSync, rmSync, sy
4
4
  import { tmpdir } from 'node:os';
5
5
  import { join } from 'node:path';
6
6
  import { seedCodexPstackModels } from './seed-codex-pstack-models.mjs';
7
- import { QUESTION_PRESENTATION_BLOCK_END, QUESTION_PRESENTATION_BLOCK_START } from './codex-skill-load-block.mjs';
7
+ import {
8
+ PACKAGE_GUIDANCE_BLOCK_END,
9
+ PACKAGE_GUIDANCE_BLOCK_START,
10
+ QUESTION_PRESENTATION_BLOCK_END,
11
+ QUESTION_PRESENTATION_BLOCK_START,
12
+ SKILL_LOAD_BLOCK_END,
13
+ SKILL_LOAD_BLOCK_START,
14
+ } from './codex-skill-load-block.mjs';
8
15
 
9
16
  const QUESTION_GUIDANCE = `${QUESTION_PRESENTATION_BLOCK_START}\nAsk one question.\n${QUESTION_PRESENTATION_BLOCK_END}\n`;
10
17
 
@@ -114,3 +121,47 @@ test('a linked question-only Codex file does not trigger model seeding', (contex
114
121
  assert.equal(lstatSync(join(codexHome, 'AGENTS.md')).isSymbolicLink(), true);
115
122
  assert.equal(readFileSync(linkedGuidancePath, 'utf8'), QUESTION_GUIDANCE);
116
123
  });
124
+
125
+ const PACKAGE_GUIDANCE = `${PACKAGE_GUIDANCE_BLOCK_START}\nStatus\n${PACKAGE_GUIDANCE_BLOCK_END}\n`;
126
+ const SKILL_LOAD_GUIDANCE = `${SKILL_LOAD_BLOCK_START}\nLoad the skill.\n${SKILL_LOAD_BLOCK_END}\n`;
127
+
128
+ function codexHomeHolding(context, existingGuidance) {
129
+ const root = mkdtempSync(join(tmpdir(), 'cde-pstack-models-'));
130
+ context.after(() => rmSync(root, { recursive: true, force: true }));
131
+ const codexHome = join(root, '.codex');
132
+ mkdirSync(codexHome);
133
+ writeFileSync(join(codexHome, 'AGENTS.md'), existingGuidance);
134
+ return codexHome;
135
+ }
136
+
137
+ for (const [label, existingGuidance] of [
138
+ ['package block', PACKAGE_GUIDANCE],
139
+ ['package then question blocks', `${PACKAGE_GUIDANCE}\n${QUESTION_GUIDANCE}`],
140
+ ['question then package blocks', `${QUESTION_GUIDANCE}\n${PACKAGE_GUIDANCE}`],
141
+ ['all three managed blocks', `${SKILL_LOAD_GUIDANCE}\n${PACKAGE_GUIDANCE}\n${QUESTION_GUIDANCE}`],
142
+ ]) {
143
+ test(`a Codex file holding only the ${label} receives model defaults and keeps its blocks`, (context) => {
144
+ const codexHome = codexHomeHolding(context, existingGuidance);
145
+
146
+ const paths = seedCodexPstackModels(codexHome);
147
+
148
+ assert.deepEqual(paths, [join(codexHome, 'pstack-models.md'), join(codexHome, 'AGENTS.md')]);
149
+ const agents = readFileSync(join(codexHome, 'AGENTS.md'), 'utf8');
150
+ assert.match(agents, /^feature, refactoring: gpt-6-sol$/m);
151
+ assert.ok(agents.endsWith(existingGuidance));
152
+ });
153
+ }
154
+
155
+ for (const [label, existingGuidance] of [
156
+ ['an empty file', ''],
157
+ ['a package block with personal notes', `${PACKAGE_GUIDANCE}My notes\n`],
158
+ ['personal notes before managed blocks', `My notes\n${PACKAGE_GUIDANCE}\n${QUESTION_GUIDANCE}`],
159
+ ]) {
160
+ test(`${label} does not trigger model seeding`, (context) => {
161
+ const codexHome = codexHomeHolding(context, existingGuidance);
162
+
163
+ assert.equal(seedCodexPstackModels(codexHome), null);
164
+ assert.equal(existsSync(join(codexHome, 'pstack-models.md')), false);
165
+ assert.equal(readFileSync(join(codexHome, 'AGENTS.md'), 'utf8'), existingGuidance);
166
+ });
167
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "8.32.0",
3
+ "version": "8.32.1",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {