claude-dev-env 7.7.1 → 8.0.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 (103) hide show
  1. package/.agents/agents/clean-coder.md +5 -6
  2. package/.agents/agents/issue-tracker.md +1 -1
  3. package/.agents/agents/pr-description-writer.md +1 -1
  4. package/.agents/agents/skill-writer-agent.md +1 -12
  5. package/.agents/agents/test_agent_frontmatter.py +2 -19
  6. package/.agents/skills/issue-tracker/SKILL.md +1 -1
  7. package/.agents/skills/orchestrator/SKILL.md +4 -5
  8. package/.agents/skills/pull-request/SKILL.md +8 -7
  9. package/.agents/skills/pull-request/reference/publication-tasks.md +1 -1
  10. package/.agents/skills/team-advisor/SKILL.md +2 -3
  11. package/_shared/advisor/advisor-protocol.md +0 -1
  12. package/_shared/advisor/reference/lifecycle.md +0 -1
  13. package/_shared/advisor/reference/warm-up.md +1 -2
  14. package/audit-rubrics/prompts/category-e-dead-code.md +3 -3
  15. package/bin/install-constants.mjs +0 -5
  16. package/bin/install-pstack-plugin.mjs +192 -0
  17. package/bin/install-pstack-plugin.test.mjs +181 -0
  18. package/bin/install.agents-home.test.mjs +0 -4
  19. package/bin/install.codex-rules.test.mjs +6 -0
  20. package/bin/install.cursor-rules.test.mjs +3 -253
  21. package/bin/install.mjs +55 -160
  22. package/bin/install.test.mjs +110 -149
  23. package/bin/prune-retired-pstack-store.mjs +83 -0
  24. package/bin/prune-retired-pstack-store.test.mjs +105 -0
  25. package/bin/prune-session-continuity.mjs +57 -0
  26. package/bin/prune-session-continuity.test.mjs +70 -0
  27. package/bin/resolve-install-root.mjs +2 -0
  28. package/bin/resolve-install-root.test.mjs +69 -0
  29. package/docs/CODE_RULES.md +1 -1
  30. package/docs/agent-spawn-protocol.md +1 -1
  31. package/docs/codex-compatibility.md +4 -16
  32. package/docs/nas-ssh-invocation.md +1 -1
  33. package/docs/references/prose-style-enforcement.md +2 -2
  34. package/hooks/HOOK_SPECS_PROMPT_WORKFLOW.md +1 -3
  35. package/hooks/blocking/code_rules_constants_config.py +1 -108
  36. package/hooks/blocking/code_rules_enforcer.py +9 -5
  37. package/hooks/blocking/code_rules_test_layout.py +6 -7
  38. package/hooks/blocking/test_code_rules_constants_config.py +13 -0
  39. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -1
  40. package/hooks/blocking/test_code_rules_enforcer_dispatch_wiring.py +15 -0
  41. package/hooks/blocking/test_code_rules_enforcer_split_constants_config.py +0 -60
  42. package/hooks/blocking/test_context_control_policy_files.py +0 -11
  43. package/hooks/hooks_constants/blocking_check_limits.py +1 -1
  44. package/hooks/hooks_constants/code_rules_enforcer_constants.py +0 -1
  45. package/hooks/hooks_constants/setup_project_paths_constants.py +1 -2
  46. package/hooks/session/test_untracked_repo_detector.py +1 -1
  47. package/hooks/validators/code_quality_checks.py +15 -2
  48. package/hooks/validators/test_code_quality_checks.py +22 -0
  49. package/package.json +1 -2
  50. package/rules/ci-owns-the-gate.md +58 -0
  51. package/rules/code-standards.md +1 -2
  52. package/rules/docstring-prose-matches-implementation.md +2 -2
  53. package/rules/failure-blast-radius.md +1 -1
  54. package/rules/git-workflow.md +23 -11
  55. package/rules/orphan-css-class.md +1 -1
  56. package/rules/paired-test-coverage.md +1 -1
  57. package/rules/retired-hook-prose.md +1 -1
  58. package/rules/verify-before-asking.md +1 -1
  59. package/rules/windows-filesystem-safe.md +1 -1
  60. package/rules/workflow-substitution-slots.md +1 -1
  61. package/scripts/Sync-RepoMain.ps1 +71 -6
  62. package/scripts/policy_lint/adapter_detectors.py +116 -10
  63. package/scripts/policy_lint/config/approved_test_pairs.py +12 -0
  64. package/scripts/policy_lint/config/constants.py +3 -0
  65. package/scripts/tests/Sync-RepoMain.Tests.ps1 +108 -0
  66. package/scripts/tests/test_policy_lint_rules_validator_baseline.py +126 -0
  67. package/.agents/skills/session-continuity/AGENTS.md +0 -1
  68. package/.agents/skills/session-continuity/CLAUDE.md +0 -1
  69. package/.agents/skills/session-continuity/README.md +0 -223
  70. package/.agents/skills/session-continuity/SKILL.md +0 -125
  71. package/.agents/skills/session-continuity/continuity.mjs +0 -445
  72. package/.agents/skills/session-continuity/continuity.test.mjs +0 -381
  73. package/bin/install-session-continuity.mjs +0 -131
  74. package/bin/install-session-continuity.test.mjs +0 -106
  75. package/bin/install.pstack-plugin.test.mjs +0 -115
  76. package/bin/pstack-codex-discovery.test.mjs +0 -79
  77. package/bin/pstack-model-preferences.claude.json +0 -23
  78. package/bin/pstack-model-preferences.codex.json +0 -24
  79. package/bin/pstack.mjs +0 -392
  80. package/bin/pstack.test.mjs +0 -347
  81. package/bin/refresh-pstack-plugin-skills.test.mjs +0 -211
  82. package/bin/select-pstack-models.test.mjs +0 -776
  83. package/hooks/blocking/test_code_rules_enforcer_file_global_constants.py +0 -231
  84. package/rules/agent-spawn-protocol.md +0 -9
  85. package/rules/ask-user-question-required.md +0 -31
  86. package/rules/claims-as-quotes.md +0 -55
  87. package/rules/confirm-implementation-forks.md +0 -11
  88. package/rules/file-global-constants.md +0 -81
  89. package/rules/hedging-claims.md +0 -7
  90. package/rules/measurement-denominators.md +0 -39
  91. package/rules/prompt-workflow-context-controls.md +0 -19
  92. package/rules/pstack-host-mapping.md +0 -48
  93. package/rules/pstack-models.md +0 -142
  94. package/rules/vault-context.md +0 -5
  95. package/scripts/pstack-adapters/common.md +0 -17
  96. package/scripts/pstack-adapters/create-skill.md +0 -14
  97. package/scripts/pstack-adapters/host-claude.md +0 -7
  98. package/scripts/pstack-adapters/host-codex.md +0 -11
  99. package/scripts/pstack-adapters/host-cursor.md +0 -7
  100. package/scripts/pstack.lock.json +0 -11
  101. package/scripts/refresh_pstack_plugin_skills.mjs +0 -200
  102. package/scripts/select_pstack_models.mjs +0 -469
  103. /package/.agents/{skills → skills-archived}/source-command-logifix/SKILL.md +0 -0
@@ -0,0 +1,181 @@
1
+ import { test } from 'node:test';
2
+ import { strict as assert } from 'node:assert';
3
+ import {
4
+ PSTACK_MARKETPLACE_REPOSITORY,
5
+ PSTACK_PLUGIN_HOSTS,
6
+ PSTACK_PLUGIN_IDENTIFIER,
7
+ hostCommandInvocation,
8
+ installPstackPlugin,
9
+ pstackPluginPlan,
10
+ shouldInstallPstackPlugin,
11
+ } from './install-pstack-plugin.mjs';
12
+
13
+ function recordingRunner(outcomeForCommand = () => ({ status: 0, stderr: '' })) {
14
+ const calls = [];
15
+ return {
16
+ calls,
17
+ runCommand(executable, commandArguments, options) {
18
+ calls.push({ executable, commandArguments, environment: options.environment });
19
+ return outcomeForCommand(executable, commandArguments);
20
+ },
21
+ };
22
+ }
23
+
24
+ const ROOTS = Object.freeze({
25
+ claudeRoot: '/managed/.claude',
26
+ codexHome: '/managed/.codex',
27
+ environment: {},
28
+ });
29
+
30
+ const WINDOWS_ENVIRONMENT = Object.freeze({ ComSpec: 'C:\\Windows\\system32\\cmd.exe' });
31
+
32
+ test('the Claude plan carries the two documented plugin commands', () => {
33
+ const plan = pstackPluginPlan('claude');
34
+ assert.equal(plan.executable, 'claude');
35
+ assert.deepEqual(plan.commands, [
36
+ ['plugin', 'marketplace', 'add', PSTACK_MARKETPLACE_REPOSITORY],
37
+ ['plugin', 'install', PSTACK_PLUGIN_IDENTIFIER],
38
+ ]);
39
+ });
40
+
41
+ test('the Codex plan adds the plugin with the Codex verb', () => {
42
+ const plan = pstackPluginPlan('codex');
43
+ assert.equal(plan.executable, 'codex');
44
+ assert.deepEqual(plan.commands, [
45
+ ['plugin', 'marketplace', 'add', PSTACK_MARKETPLACE_REPOSITORY],
46
+ ['plugin', 'add', PSTACK_PLUGIN_IDENTIFIER],
47
+ ]);
48
+ });
49
+
50
+ test('an unsupported host names the hosts this installer knows', () => {
51
+ assert.throws(() => pstackPluginPlan('cursor'), /cursor/);
52
+ assert.deepEqual(PSTACK_PLUGIN_HOSTS, ['claude', 'codex']);
53
+ });
54
+
55
+ test('each host installs into the managed root this run wrote', () => {
56
+ const runner = recordingRunner();
57
+ const outcome = installPstackPlugin(ROOTS, runner);
58
+ assert.equal(outcome.status, 'installed');
59
+ assert.deepEqual(runner.calls.map(call => [call.executable, ...call.commandArguments]), [
60
+ ['claude', 'plugin', 'marketplace', 'add', PSTACK_MARKETPLACE_REPOSITORY],
61
+ ['claude', 'plugin', 'install', PSTACK_PLUGIN_IDENTIFIER],
62
+ ['codex', 'plugin', 'marketplace', 'add', PSTACK_MARKETPLACE_REPOSITORY],
63
+ ['codex', 'plugin', 'add', PSTACK_PLUGIN_IDENTIFIER],
64
+ ]);
65
+ assert.equal(runner.calls[0].environment.CLAUDE_CONFIG_DIR, ROOTS.claudeRoot);
66
+ assert.equal(runner.calls[2].environment.CODEX_HOME, ROOTS.codexHome);
67
+ });
68
+
69
+ test('an absent host command skips that host and leaves the other installed', () => {
70
+ const runner = recordingRunner((executable) => (executable === 'codex'
71
+ ? { status: null, error: Object.assign(new Error('spawn codex ENOENT'), { code: 'ENOENT' }) }
72
+ : { status: 0, stderr: '' }));
73
+ const outcome = installPstackPlugin(ROOTS, runner);
74
+ assert.equal(outcome.hosts.find(host => host.host === 'claude').status, 'installed');
75
+ const codex = outcome.hosts.find(host => host.host === 'codex');
76
+ assert.equal(codex.status, 'skipped');
77
+ assert.match(codex.warning, /codex/);
78
+ assert.equal(runner.calls.filter(call => call.executable === 'codex').length, 1);
79
+ });
80
+
81
+ test('a failing command reports that host and still installs the next one', () => {
82
+ const runner = recordingRunner((executable, commandArguments) => (
83
+ executable === 'claude' && commandArguments[1] === 'marketplace'
84
+ ? { status: 1, stderr: 'marketplace unreachable' }
85
+ : { status: 0, stderr: '' }));
86
+ const outcome = installPstackPlugin(ROOTS, runner);
87
+ assert.equal(outcome.status, 'failed');
88
+ const claude = outcome.hosts.find(host => host.host === 'claude');
89
+ assert.equal(claude.status, 'failed');
90
+ assert.match(claude.warning, /marketplace unreachable/);
91
+ assert.equal(outcome.hosts.find(host => host.host === 'codex').status, 'installed');
92
+ assert.equal(runner.calls.filter(call => call.executable === 'claude').length, 1);
93
+ });
94
+
95
+ test('an executable override replaces the host command for that host only', () => {
96
+ const runner = recordingRunner();
97
+ installPstackPlugin(
98
+ { ...ROOTS, environment: { CDE_CODEX_EXECUTABLE: '/opt/codex/bin/codex' } },
99
+ runner,
100
+ );
101
+ assert.deepEqual([...new Set(runner.calls.map(call => call.executable))], [
102
+ 'claude',
103
+ '/opt/codex/bin/codex',
104
+ ]);
105
+ });
106
+
107
+ test('the opt-out flag and variable both turn the step off', () => {
108
+ assert.equal(shouldInstallPstackPlugin([], {}), true);
109
+ assert.equal(shouldInstallPstackPlugin(['--no-pstack'], {}), false);
110
+ assert.equal(shouldInstallPstackPlugin([], { CDE_INSTALL_PSTACK: '0' }), false);
111
+ assert.equal(shouldInstallPstackPlugin([], { CDE_INSTALL_PSTACK: '1' }), true);
112
+ });
113
+
114
+ test('a selected host list installs only that host', () => {
115
+ const runner = recordingRunner();
116
+ const outcome = installPstackPlugin({ ...ROOTS, hosts: ['codex'] }, runner);
117
+ assert.deepEqual(outcome.hosts.map(host => host.host), ['codex']);
118
+ assert.deepEqual([...new Set(runner.calls.map(call => call.executable))], ['codex']);
119
+ });
120
+
121
+ test('a non-Windows platform launches the host command directly', () => {
122
+ const invocation = hostCommandInvocation('claude', ['plugin', 'install', PSTACK_PLUGIN_IDENTIFIER], 'linux');
123
+ assert.deepEqual(invocation, {
124
+ file: 'claude',
125
+ args: ['plugin', 'install', PSTACK_PLUGIN_IDENTIFIER],
126
+ windowsVerbatimArguments: false,
127
+ });
128
+ });
129
+
130
+ test('Windows launches the host command through cmd.exe, which alone can run a .cmd shim', () => {
131
+ const invocation = hostCommandInvocation(
132
+ 'claude', ['plugin', 'install', PSTACK_PLUGIN_IDENTIFIER], 'win32', WINDOWS_ENVIRONMENT,
133
+ );
134
+ assert.equal(invocation.file, WINDOWS_ENVIRONMENT.ComSpec);
135
+ assert.deepEqual(invocation.args, [
136
+ '/d',
137
+ '/s',
138
+ '/c',
139
+ `""claude" plugin install ${PSTACK_PLUGIN_IDENTIFIER}"`,
140
+ ]);
141
+ assert.equal(invocation.windowsVerbatimArguments, true);
142
+ });
143
+
144
+ test('a Windows executable path holding a space stays one quoted token', () => {
145
+ const invocation = hostCommandInvocation(
146
+ 'C:\\Program Files\\nodejs\\claude.cmd',
147
+ ['plugin', 'marketplace', 'add', PSTACK_MARKETPLACE_REPOSITORY],
148
+ 'win32',
149
+ WINDOWS_ENVIRONMENT,
150
+ );
151
+ assert.equal(
152
+ invocation.args.at(-1),
153
+ `""C:\\Program Files\\nodejs\\claude.cmd" plugin marketplace add ${PSTACK_MARKETPLACE_REPOSITORY}"`,
154
+ );
155
+ });
156
+
157
+ test('a Windows environment without ComSpec falls back to the bare shell name', () => {
158
+ const invocation = hostCommandInvocation('claude', ['plugin'], 'win32', {});
159
+ assert.equal(invocation.file, 'cmd.exe');
160
+ });
161
+
162
+ test("cmd.exe's command-not-found exit code reads as an absent host, not a failure", () => {
163
+ const runner = recordingRunner((executable) => (executable === 'codex'
164
+ ? {
165
+ status: 9009,
166
+ stderr: "'codex' is not recognized as an internal or external command,\noperable program or batch file.",
167
+ }
168
+ : { status: 0, stderr: '' }));
169
+ const outcome = installPstackPlugin(ROOTS, runner);
170
+ const codex = outcome.hosts.find(host => host.host === 'codex');
171
+ assert.equal(codex.status, 'skipped');
172
+ assert.equal(outcome.status, 'installed');
173
+ assert.equal(runner.calls.filter(call => call.executable === 'codex').length, 1);
174
+ });
175
+
176
+ test('a non-zero exit that is not command-not-found still reads as a failure', () => {
177
+ const runner = recordingRunner(() => ({ status: 9009, stderr: 'the marketplace rejected the catalog' }));
178
+ const outcome = installPstackPlugin({ ...ROOTS, hosts: ['claude'] }, runner);
179
+ assert.equal(outcome.hosts[0].status, 'failed');
180
+ assert.match(outcome.hosts[0].warning, /rejected the catalog/);
181
+ });
@@ -53,10 +53,6 @@ const CLEAN_CODER_POLICY_REFERENCES = [
53
53
  '<managed-root>/rules/code-standards.md',
54
54
  'packages/claude-dev-env/rules/code-standards.md',
55
55
  ],
56
- [
57
- '<managed-root>/rules/file-global-constants.md',
58
- 'packages/claude-dev-env/rules/file-global-constants.md',
59
- ],
60
56
  [
61
57
  '<managed-root>/rules/windows-filesystem-safe.md',
62
58
  'packages/claude-dev-env/rules/windows-filesystem-safe.md',
@@ -118,6 +118,11 @@ test('resolveInstallRoot names ~/.codex/rules and allows files under it', () =>
118
118
  });
119
119
  const expectedDirectory = join(homeDirectory, DEFAULT_CODEX_DIRECTORY_NAME, CODEX_RULES_DIRECTORY_NAME);
120
120
  assert.equal(resolution.codexRulesInstallDirectory, expectedDirectory);
121
+ assert.equal(
122
+ resolution.codexHomeDirectory,
123
+ join(homeDirectory, DEFAULT_CODEX_DIRECTORY_NAME),
124
+ 'the pstack plugin step installs into this Codex home',
125
+ );
121
126
  assert.equal(
122
127
  isAllowedInstallDestination(join(expectedDirectory, CODEX_RULES_SHIPPED_FILE_NAME), resolution),
123
128
  true,
@@ -140,6 +145,7 @@ test('CODEX_HOME relocates the Codex rules destination', () => {
140
145
  resolution.codexRulesInstallDirectory,
141
146
  join(relocatedHome, CODEX_RULES_DIRECTORY_NAME),
142
147
  );
148
+ assert.equal(resolution.codexHomeDirectory, relocatedHome);
143
149
  });
144
150
 
145
151
  test('a full install copies shipped Codex rules and leaves a local default.rules in place', () => {
@@ -43,13 +43,6 @@ function runInstaller(homeDirectory, extraArguments, environmentOverrides = {})
43
43
  });
44
44
  }
45
45
 
46
- function runSelector(selectorPath, selectionInput) {
47
- return JSON.parse(execFileSync('node', [selectorPath], {
48
- encoding: 'utf8',
49
- input: JSON.stringify(selectionInput),
50
- }));
51
- }
52
-
53
46
  test('resolveInstallRoot names ~/.cursor/rules and allows generated mdc files under it', () => {
54
47
  const homeDirectory = join(tmpdir(), 'cdev-cursor-rules-home');
55
48
  const resolution = resolveInstallRoot({
@@ -111,246 +104,22 @@ test('--only journal is rejected before Cursor rules change; --only core writes
111
104
  CURSOR_RULES_DIRECTORY_NAME,
112
105
  'asd-ste100-language.mdc',
113
106
  );
114
- const preferencePath = join(
115
- homeDirectory,
116
- '.agents',
117
- 'rules',
118
- 'pstack-model-preferences.codex.json',
119
- );
107
+ const policyPath = join(homeDirectory, '.agents', 'rules', 'subagent-model-policy.json');
120
108
  assert.throws(
121
109
  () => runInstaller(homeDirectory, ['--only', 'journal']),
122
110
  error => error.status === 1 && /Unknown group\(s\): journal/.test(error.stderr),
123
111
  );
124
112
  assert.equal(existsSync(generatedPath), false);
125
- assert.equal(existsSync(preferencePath), false);
113
+ assert.equal(existsSync(policyPath), false);
126
114
 
127
115
  runInstaller(homeDirectory, ['--only', 'core']);
128
116
  assert.equal(existsSync(generatedPath), true);
129
- assert.equal(existsSync(preferencePath), true);
117
+ assert.equal(existsSync(policyPath), true);
130
118
  } finally {
131
119
  rmSync(homeDirectory, { recursive: true, force: true });
132
120
  }
133
121
  });
134
122
 
135
- for (const targetName of [null, 'profile']) {
136
- test('pstack rules survive reinstall and sync for ' + (targetName ?? 'main'), () => {
137
- const homeDirectory = mkdtempSync(join(tmpdir(), 'cdev-pstack-'));
138
- try {
139
- const targetArguments = targetName
140
- ? ['--target', join(homeDirectory, targetName)]
141
- : [];
142
- const resolution = resolveInstallRoot({
143
- homeDirectory,
144
- environment: {},
145
- explicitTarget: targetName ? join(homeDirectory, targetName) : null,
146
- });
147
- const sharedPath = join(resolution.agentsHome, 'rules', 'pstack-models.mdc');
148
- const cursorPath = join(resolution.cursorRulesInstallDirectory, 'pstack-models.mdc');
149
- const preferencesPath = join(
150
- resolution.agentsHome,
151
- 'rules',
152
- 'pstack-model-preferences.codex.json',
153
- );
154
- const policyPath = join(
155
- resolution.agentsHome,
156
- 'rules',
157
- 'subagent-model-policy.json',
158
- );
159
- const selectorPath = join(
160
- resolution.agentsHome,
161
- 'scripts',
162
- 'select_pstack_models.mjs',
163
- );
164
- const managedSelectorPath = join(
165
- resolution.managedRoot,
166
- 'scripts',
167
- 'select_pstack_models.mjs',
168
- );
169
- runInstaller(homeDirectory, [...targetArguments, '--only', 'core']);
170
-
171
- const installedText = readFileSync(sharedPath, 'utf8');
172
- assert.doesNotMatch(installedText, /gpt-|claude-|grok-/i);
173
- assert.match(installedText, /feature, refactoring: reliable code execution/);
174
- assert.match(installedText, /interrogate reviewers: independent adversarial review/);
175
- assert.match(installedText, /alwaysApply: true/);
176
- assert.match(installedText, /replaces the fixed model defaults in pstack skills/);
177
- assert.match(installedText, /Run the installed selector once for each agent/);
178
- assert.equal(readFileSync(cursorPath, 'utf8'), installedText);
179
- assert.equal(isAllowedInstallDestination(sharedPath, resolution), true);
180
- assert.equal(existsSync(selectorPath), true);
181
- assert.equal(existsSync(policyPath), true);
182
-
183
- const savedPreferences = JSON.parse(readFileSync(preferencesPath, 'utf8'));
184
- assert.deepEqual(savedPreferences, {
185
- host: 'codex',
186
- defaultEffort: 'medium',
187
- modelsByRole: {
188
- 'feature, refactoring': ['gpt-5.6-sol'],
189
- 'bug-fix': ['gpt-5.6-sol'],
190
- 'perf-issue': ['gpt-6-astra'],
191
- hillclimb: ['gpt-6-astra'],
192
- 'judgment and prose': ['gpt-6-astra'],
193
- 'hardest tasks': ['gpt-6-astra'],
194
- 'how explorer': ['gpt-5.6-terra'],
195
- 'how explainer': ['gpt-5.6-sol'],
196
- 'how critics': ['gpt-5.6-terra', 'gpt-5.6-sol', 'gpt-6-astra'],
197
- 'why investigators': ['gpt-5.6-terra'],
198
- 'why synthesizer': ['gpt-6-astra'],
199
- 'reflect tooling': ['gpt-5.6-sol'],
200
- 'reflect judgment, divergent, synthesizer': ['gpt-6-astra'],
201
- 'arena runners': ['gpt-5.6-terra', 'gpt-5.6-sol', 'gpt-6-astra'],
202
- 'arena cross-judge pool': ['gpt-5.6-sol', 'gpt-6-astra'],
203
- 'swarm workers': ['gpt-5.6-luna'],
204
- 'architect runners': ['gpt-5.6-sol', 'gpt-6-astra'],
205
- 'interrogate reviewers': [
206
- 'gpt-5.6-terra',
207
- 'gpt-5.6-sol',
208
- 'gpt-6-astra',
209
- ],
210
- },
211
- });
212
- for (const role of Object.keys(savedPreferences.modelsByRole)) {
213
- assert.equal(installedText.includes(role + ':'), true);
214
- }
215
- const codexSelection = runSelector(selectorPath, {
216
- host: 'codex',
217
- inventoryHost: 'codex',
218
- role: 'feature, refactoring',
219
- delegationIndex: 0,
220
- availableModelIds: ['gpt-5.6-sol'],
221
- confirmedSuitableModelIds: [],
222
- parentFallback: {
223
- isAllowed: false,
224
- hasMaterialCapabilityLoss: false,
225
- },
226
- });
227
- assert.equal(codexSelection.canDelegate, false);
228
- assert.equal(codexSelection.failure, 'no-supported-model');
229
- assert.deepEqual(codexSelection.nativeSpawnArguments, {});
230
-
231
- const claudePreferencesPath = join(
232
- resolution.agentsHome,
233
- 'rules',
234
- 'pstack-model-preferences.claude.json',
235
- );
236
- const seededClaudePreferences = JSON.parse(
237
- readFileSync(claudePreferencesPath, 'utf8'),
238
- );
239
- assert.deepEqual(seededClaudePreferences, {
240
- host: 'claude',
241
- modelsByRole: {
242
- 'feature, refactoring': ['opus'],
243
- 'bug-fix': ['opus'],
244
- 'perf-issue': ['fable'],
245
- hillclimb: ['fable'],
246
- 'judgment and prose': ['fable'],
247
- 'hardest tasks': ['fable'],
248
- 'how explorer': ['sonnet'],
249
- 'how explainer': ['opus'],
250
- 'how critics': ['sonnet', 'opus', 'fable'],
251
- 'why investigators': ['sonnet'],
252
- 'why synthesizer': ['fable'],
253
- 'reflect tooling': ['opus'],
254
- 'reflect judgment, divergent, synthesizer': ['fable'],
255
- 'arena runners': ['sonnet', 'opus', 'fable'],
256
- 'arena cross-judge pool': ['opus', 'fable'],
257
- 'swarm workers': ['sonnet'],
258
- 'architect runners': ['opus', 'fable'],
259
- 'interrogate reviewers': ['sonnet', 'opus', 'fable'],
260
- },
261
- });
262
- const claudeSelection = runSelector(selectorPath, {
263
- host: 'claude',
264
- inventoryHost: 'claude',
265
- role: 'swarm workers',
266
- delegationIndex: 0,
267
- availableModelIds: ['sonnet', 'opus', 'fable'],
268
- confirmedSuitableModelIds: [],
269
- parentFallback: {
270
- isAllowed: false,
271
- hasMaterialCapabilityLoss: false,
272
- },
273
- });
274
- assert.deepEqual(claudeSelection.nativeSpawnArguments, {
275
- model: 'sonnet',
276
- });
277
-
278
- writeFileSync(claudePreferencesPath, JSON.stringify({
279
- host: 'claude',
280
- modelsByRole: {
281
- 'feature, refactoring': ['confirmed-claude-model'],
282
- },
283
- }));
284
- const editedClaudeSelection = runSelector(selectorPath, {
285
- host: 'claude',
286
- inventoryHost: 'claude',
287
- role: 'feature, refactoring',
288
- delegationIndex: 0,
289
- availableModelIds: ['confirmed-claude-model'],
290
- confirmedSuitableModelIds: [],
291
- parentFallback: {
292
- isAllowed: false,
293
- hasMaterialCapabilityLoss: false,
294
- },
295
- });
296
- assert.deepEqual(editedClaudeSelection.nativeSpawnArguments, {
297
- model: 'confirmed-claude-model',
298
- });
299
-
300
- const manifest = JSON.parse(readFileSync(resolution.manifestFilePath, 'utf8'));
301
- assert.equal(manifest.files.includes(sharedPath), true);
302
- assert.equal(manifest.files.includes(managedSelectorPath), true);
303
- assert.equal(manifest.files.includes(preferencesPath), false);
304
- assert.equal(manifest.files.includes(claudePreferencesPath), false);
305
-
306
- writeFileSync(sharedPath, 'stale');
307
- writeFileSync(preferencesPath, JSON.stringify({
308
- host: 'codex',
309
- modelsByRole: {
310
- 'feature, refactoring': ['user-edited-codex-model'],
311
- },
312
- }));
313
- runInstaller(homeDirectory, [...targetArguments, '--only', 'core']);
314
- assert.equal(readFileSync(sharedPath, 'utf8'), installedText);
315
- assert.deepEqual(
316
- JSON.parse(readFileSync(preferencesPath, 'utf8'))
317
- .modelsByRole['feature, refactoring'],
318
- ['user-edited-codex-model'],
319
- );
320
- assert.deepEqual(
321
- JSON.parse(readFileSync(claudePreferencesPath, 'utf8'))
322
- .modelsByRole['feature, refactoring'],
323
- ['confirmed-claude-model'],
324
- );
325
-
326
- const syncArguments = [
327
- join(resolution.managedRoot, 'scripts', 'sync_to_cursor.py'),
328
- '--claude-root', resolution.managedRoot,
329
- '--cursor-root', resolution.cursorInstallDirectory,
330
- '--quiet',
331
- ];
332
- execFileSync('python', syncArguments);
333
- execFileSync('python', [...syncArguments, '--check']);
334
- assert.equal(readFileSync(cursorPath, 'utf8'), installedText);
335
- assert.equal(readFileSync(sharedPath, 'utf8'), installedText);
336
- if (targetName) {
337
- assert.equal(
338
- existsSync(join(homeDirectory, '.agents', 'rules', 'pstack-models.mdc')),
339
- false,
340
- );
341
- }
342
-
343
- runInstaller(homeDirectory, [...targetArguments, '--uninstall']);
344
- assert.equal(existsSync(sharedPath), false);
345
- assert.equal(existsSync(cursorPath), false);
346
- assert.equal(existsSync(selectorPath), false);
347
- assert.equal(existsSync(preferencesPath), true);
348
- assert.equal(existsSync(claudePreferencesPath), true);
349
- } finally {
350
- rmSync(homeDirectory, { recursive: true, force: true });
351
- }
352
- });
353
- }
354
123
 
355
124
  test('seeds one editable policy and one native Codex routing hook', () => {
356
125
  const homeDirectory = mkdtempSync(join(tmpdir(), 'cdev-routing-install-'));
@@ -667,22 +436,3 @@ test('a blocked shared rule destination rolls back generated Cursor files', () =
667
436
  }
668
437
  });
669
438
 
670
- test('a failed install rolls back newly seeded host preference files', () => {
671
- const homeDirectory = mkdtempSync(join(tmpdir(), 'cdev-pstack-seed-rollback-'));
672
- try {
673
- const preferencesDirectory = join(homeDirectory, '.agents', 'rules');
674
- assert.throws(() => runInstaller(
675
- homeDirectory,
676
- ['--only', 'core'],
677
- { CLAUDE_DEV_ENV_INSTALL_FAULT: 'after_file_staging' },
678
- ));
679
- for (const hostName of ['claude', 'codex']) {
680
- assert.equal(
681
- existsSync(join(preferencesDirectory, 'pstack-model-preferences.' + hostName + '.json')),
682
- false,
683
- );
684
- }
685
- } finally {
686
- rmSync(homeDirectory, { recursive: true, force: true });
687
- }
688
- });