claude-dev-env 8.19.1 → 8.20.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/bin/install.mjs CHANGED
@@ -25,6 +25,10 @@ import {
25
25
  pruneManagedPermissionsFromSettings,
26
26
  retiredManagedPermissions,
27
27
  } from './merge_managed_permissions.mjs';
28
+ import {
29
+ mergeMissingSettingsDefaults,
30
+ settingsDefaultsFromPackageSettings,
31
+ } from './merge_settings_defaults.mjs';
28
32
  import {
29
33
  SKIPPED_SOURCE_ENTRY_NAMES,
30
34
  SKIPPED_SOURCE_FILE_EXTENSIONS,
@@ -2176,6 +2180,26 @@ function mergeManagedPermissions() {
2176
2180
  return { ...mergeOutcome, removedCount: pruneOutcome.removedCount };
2177
2181
  }
2178
2182
 
2183
+ /**
2184
+ * Add each package setting default that ~/.claude/settings.json lacks.
2185
+ *
2186
+ * @returns {{addedKeys: string[]}}
2187
+ */
2188
+ function mergePackageSettingsDefaults() {
2189
+ const packageSettingsPath = join(PACKAGE_ROOT, SETTINGS_FILE_NAME);
2190
+ if (!existsSync(packageSettingsPath)) return { addedKeys: [] };
2191
+ const settingsDefaults = settingsDefaultsFromPackageSettings(
2192
+ JSON.parse(readFileSync(packageSettingsPath, 'utf8')),
2193
+ );
2194
+ const settingsPath = join(CLAUDE_HOME, SETTINGS_FILE_NAME);
2195
+ const settings = loadClaudeSettingsObject(settingsPath);
2196
+ const mergeOutcome = mergeMissingSettingsDefaults(settings, settingsDefaults);
2197
+ if (mergeOutcome.addedKeys.length > 0) {
2198
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 4) + '\n');
2199
+ }
2200
+ return mergeOutcome;
2201
+ }
2202
+
2179
2203
  /**
2180
2204
  * Merge two path lists into one, keyed on `comparisonKeyForPath`, with the
2181
2205
  * current run's spelling winning a collision.
@@ -2864,6 +2888,11 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
2864
2888
  );
2865
2889
  }
2866
2890
 
2891
+ const settingsDefaultsMerge = mergePackageSettingsDefaults();
2892
+ if (settingsDefaultsMerge.addedKeys.length > 0) {
2893
+ console.log(` Settings: added ${settingsDefaultsMerge.addedKeys.join(', ')}`);
2894
+ }
2895
+
2867
2896
  const agentsHubSource = join(PACKAGE_ROOT, 'AGENTS.md');
2868
2897
  if (existsSync(agentsHubSource)) {
2869
2898
  const agentsHubDest = join(AGENTS_HOME, 'AGENTS.md');
@@ -21,6 +21,10 @@ import {
21
21
  mergeManagedPermissionsIntoSettings,
22
22
  pruneManagedPermissionsFromSettings,
23
23
  } from './merge_managed_permissions.mjs';
24
+ import {
25
+ mergeMissingSettingsDefaults,
26
+ settingsDefaultsFromPackageSettings,
27
+ } from './merge_settings_defaults.mjs';
24
28
 
25
29
  const PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
26
30
  const PACKAGE_SETTINGS_PATH = join(PACKAGE_ROOT, 'settings.json');
@@ -251,3 +255,52 @@ test('sandbox uninstall removes only package-owned permission entries and keeps
251
255
  rmSync(sandboxHome, { recursive: true, force: true });
252
256
  }
253
257
  });
258
+
259
+ test('package settings.json publishes the advisor model default', () => {
260
+ const packageSettings = JSON.parse(readFileSync(PACKAGE_SETTINGS_PATH, 'utf8'));
261
+ assert.deepEqual(settingsDefaultsFromPackageSettings(packageSettings), { advisorModel: 'opus' });
262
+ });
263
+
264
+ test('a missing default is added and a user-set value is kept', () => {
265
+ const emptySettings = {};
266
+ assert.deepEqual(mergeMissingSettingsDefaults(emptySettings, { advisorModel: 'opus' }), {
267
+ addedKeys: ['advisorModel'],
268
+ });
269
+ assert.equal(emptySettings.advisorModel, 'opus');
270
+ const userSettings = { advisorModel: 'sonnet' };
271
+ assert.deepEqual(mergeMissingSettingsDefaults(userSettings, { advisorModel: 'opus' }), {
272
+ addedKeys: [],
273
+ });
274
+ assert.equal(userSettings.advisorModel, 'sonnet');
275
+ });
276
+
277
+ test('a Fable main model gets no advisor default', () => {
278
+ for (const fableModel of ['fable', 'claude-fable-5-1']) {
279
+ const fableSettings = { model: fableModel };
280
+ assert.deepEqual(mergeMissingSettingsDefaults(fableSettings, { advisorModel: 'opus' }), {
281
+ addedKeys: [],
282
+ });
283
+ assert.equal(Object.hasOwn(fableSettings, 'advisorModel'), false);
284
+ }
285
+ const opusSettings = { model: 'opus' };
286
+ mergeMissingSettingsDefaults(opusSettings, { advisorModel: 'opus' });
287
+ assert.equal(opusSettings.advisorModel, 'opus');
288
+ });
289
+
290
+ test('install adds advisorModel when missing and keeps a user choice on rerun', () => {
291
+ const sandboxHome = mkdtempSync(join(tmpdir(), 'cde-settings-defaults-'));
292
+ try {
293
+ const firstRun = runInstallerInSandbox(sandboxHome);
294
+ assert.equal(firstRun.status, 0, firstRun.stderr);
295
+ const settingsPath = join(sandboxHome, '.claude', 'settings.json');
296
+ assert.equal(JSON.parse(readFileSync(settingsPath, 'utf8')).advisorModel, 'opus');
297
+ const userChoice = JSON.parse(readFileSync(settingsPath, 'utf8'));
298
+ userChoice.advisorModel = 'sonnet';
299
+ writeFileSync(settingsPath, JSON.stringify(userChoice, null, 4) + '\n');
300
+ const secondRun = runInstallerInSandbox(sandboxHome);
301
+ assert.equal(secondRun.status, 0, secondRun.stderr);
302
+ assert.equal(JSON.parse(readFileSync(settingsPath, 'utf8')).advisorModel, 'sonnet');
303
+ } finally {
304
+ rmSync(sandboxHome, { recursive: true, force: true });
305
+ }
306
+ });
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Merge package-owned scalar setting defaults into a settings object.
3
+ *
4
+ * Every top-level key in the package settings.json other than the managed
5
+ * lists (`permissions`) is a default. Install adds a default only when the
6
+ * user settings lack that key, so a value the user already set stays as is.
7
+ * A Fable main model accepts only a Fable advisor, so a user whose settings
8
+ * name a Fable model gets no package advisor default.
9
+ */
10
+
11
+ export const MANAGED_LIST_SETTING_KEYS = Object.freeze(['permissions']);
12
+ export const ADVISOR_MODEL_SETTING_KEY = 'advisorModel';
13
+ const FABLE_MODEL_PATTERN = /fable/i;
14
+
15
+ /**
16
+ * Read the setting defaults from a package settings source object.
17
+ *
18
+ * @param {Record<string, unknown> | null | undefined} packageSettings
19
+ * @returns {Record<string, unknown>}
20
+ */
21
+ export function settingsDefaultsFromPackageSettings(packageSettings) {
22
+ if (!packageSettings || typeof packageSettings !== 'object' || Array.isArray(packageSettings)) {
23
+ return {};
24
+ }
25
+ return Object.fromEntries(
26
+ Object.entries(packageSettings).filter(
27
+ ([settingKey]) => !MANAGED_LIST_SETTING_KEYS.includes(settingKey),
28
+ ),
29
+ );
30
+ }
31
+
32
+ /**
33
+ * Add each default whose key the target settings lack.
34
+ *
35
+ * @param {Record<string, unknown>} targetSettings Mutated in place.
36
+ * @param {Record<string, unknown>} settingsDefaults
37
+ * @returns {{addedKeys: string[]}}
38
+ */
39
+ export function mergeMissingSettingsDefaults(targetSettings, settingsDefaults) {
40
+ const addedKeys = [];
41
+ const mainModel = targetSettings.model;
42
+ const usesFableMainModel = typeof mainModel === 'string' && FABLE_MODEL_PATTERN.test(mainModel);
43
+ for (const [settingKey, defaultValue] of Object.entries(settingsDefaults)) {
44
+ if (Object.hasOwn(targetSettings, settingKey)) continue;
45
+ if (settingKey === ADVISOR_MODEL_SETTING_KEY && usesFableMainModel) continue;
46
+ targetSettings[settingKey] = defaultValue;
47
+ addedKeys.push(settingKey);
48
+ }
49
+ return { addedKeys };
50
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "8.19.1",
3
+ "version": "8.20.0",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -24,7 +24,7 @@ One command prints the verdict:
24
24
  python packages/claude-dev-env/scripts/agent_merge_check.py <owner>/<name> <number>
25
25
  ```
26
26
 
27
- It prints `MERGE` and exits 0 when the pull request is ready. It prints `HOLD` with the reason and exits 1 for a draft, for a head behind or conflicting with the base, for a required check that is not passing, for a check still running or red, and for an open review thread. It exits 2 when the state could not be read.
27
+ It prints `MERGE` and exits 0 when the pull request is ready. It prints `HOLD` with the reason and exits 1 for a draft, for a head behind or conflicting with the base, for a required check that is not passing, for a check still running or red, for a head the merge queue ejected for failed checks, and for an open review thread. It exits 2 when the state could not be read.
28
28
 
29
29
  Each hold reason names its own repair, and each repair belongs to the agent:
30
30
 
@@ -35,6 +35,7 @@ Each hold reason names its own repair, and each repair belongs to the agent:
35
35
  | A required check is red | Read the failing check, fix it, push |
36
36
  | A check is red or still running | Fix it, or wait for it, then read the verdict again |
37
37
  | A review thread is open | Answer it, push the fix, resolve the thread |
38
+ | Ejected from the merge queue for failed checks on this head | Read the merge_group run, fix the failure, push, then read the verdict again |
38
39
  | The pull request is a draft | Mark it ready once the checks pass |
39
40
 
40
41
  ## When a gate elsewhere holds the merge command
@@ -26,6 +26,8 @@ It prints `CLOSED` and exits 0 when every finding on the head is answered. It pr
26
26
  | A blocking `Claude Approvals` row | A push, which moves the head the check reports on |
27
27
  | A top-level comment on the pull request | A later top-level comment from the driving account |
28
28
 
29
+ A review bot rewrites its summary comment on each pass. Its edit leaves an answered summary closed, because each new finding it has arrives as a review thread or a new comment. An edit from a person reopens the comment.
30
+
29
31
  A red circle marks a finding a review states as blocking, so resolution in silence leaves it open. The reply says what changed or why the finding stands, and the reviewer reads it beside the diff.
30
32
 
31
33
  The driving account is the one that opened the pull request. Where the agent comments under a second login, `--driver-login <login>` names it, repeatably.