claude-dev-env 7.7.0 → 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.
- package/.agents/agents/clean-coder.md +5 -6
- package/.agents/agents/issue-tracker.md +1 -1
- package/.agents/agents/pr-description-writer.md +1 -1
- package/.agents/agents/skill-writer-agent.md +1 -12
- package/.agents/agents/test_agent_frontmatter.py +2 -19
- package/.agents/skills/e-code-review/reference/fix.md +4 -2
- package/.agents/skills/e-code-review/reference/loop.md +4 -2
- package/.agents/skills/issue-tracker/SKILL.md +1 -1
- package/.agents/skills/orchestrator/SKILL.md +4 -5
- package/.agents/skills/pull-request/SKILL.md +8 -7
- package/.agents/skills/pull-request/reference/publication-tasks.md +1 -1
- package/.agents/skills/team-advisor/SKILL.md +2 -3
- package/_shared/advisor/advisor-protocol.md +0 -1
- package/_shared/advisor/reference/lifecycle.md +0 -1
- package/_shared/advisor/reference/warm-up.md +1 -2
- package/audit-rubrics/prompts/category-e-dead-code.md +3 -3
- package/bin/install-constants.mjs +0 -5
- package/bin/install-pstack-plugin.mjs +192 -0
- package/bin/install-pstack-plugin.test.mjs +181 -0
- package/bin/install.agents-home.test.mjs +0 -4
- package/bin/install.codex-rules.test.mjs +6 -0
- package/bin/install.cursor-rules.test.mjs +3 -253
- package/bin/install.mjs +55 -160
- package/bin/install.test.mjs +110 -149
- package/bin/prune-retired-pstack-store.mjs +83 -0
- package/bin/prune-retired-pstack-store.test.mjs +105 -0
- package/bin/prune-session-continuity.mjs +57 -0
- package/bin/prune-session-continuity.test.mjs +70 -0
- package/bin/resolve-install-root.mjs +2 -0
- package/bin/resolve-install-root.test.mjs +69 -0
- package/bin/subagent-model-policy.test.mjs +56 -19
- package/docs/CODE_RULES.md +1 -1
- package/docs/agent-spawn-protocol.md +1 -1
- package/docs/codex-compatibility.md +4 -16
- package/docs/nas-ssh-invocation.md +1 -1
- package/docs/references/prose-style-enforcement.md +2 -2
- package/docs/subagent-model-routing.md +21 -2
- package/hooks/HOOK_SPECS_PROMPT_WORKFLOW.md +1 -3
- package/hooks/blocking/code_rules_constants_config.py +1 -108
- package/hooks/blocking/code_rules_enforcer.py +9 -5
- package/hooks/blocking/code_rules_test_layout.py +6 -7
- package/hooks/blocking/test_code_rules_constants_config.py +13 -0
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -1
- package/hooks/blocking/test_code_rules_enforcer_dispatch_wiring.py +15 -0
- package/hooks/blocking/test_code_rules_enforcer_split_constants_config.py +0 -60
- package/hooks/blocking/test_context_control_policy_files.py +0 -11
- package/hooks/hooks_constants/blocking_check_limits.py +1 -1
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +0 -1
- package/hooks/hooks_constants/setup_project_paths_constants.py +1 -2
- package/hooks/routing/subagent_model_routing.test.mjs +65 -28
- package/hooks/session/test_untracked_repo_detector.py +1 -1
- package/hooks/validators/code_quality_checks.py +15 -2
- package/hooks/validators/test_code_quality_checks.py +22 -0
- package/package.json +1 -2
- package/rules/ci-owns-the-gate.md +58 -0
- package/rules/code-standards.md +1 -2
- package/rules/docstring-prose-matches-implementation.md +2 -2
- package/rules/failure-blast-radius.md +1 -1
- package/rules/git-workflow.md +23 -11
- package/rules/orphan-css-class.md +1 -1
- package/rules/paired-test-coverage.md +1 -1
- package/rules/retired-hook-prose.md +1 -1
- package/rules/subagent-model-policy.json +1 -1
- package/rules/verify-before-asking.md +1 -1
- package/rules/windows-filesystem-safe.md +1 -1
- package/rules/workflow-substitution-slots.md +1 -1
- package/scripts/Sync-RepoMain.ps1 +71 -6
- package/scripts/policy_lint/adapter_detectors.py +116 -10
- package/scripts/policy_lint/config/approved_test_pairs.py +12 -0
- package/scripts/policy_lint/config/constants.py +3 -0
- package/scripts/tests/Sync-RepoMain.Tests.ps1 +108 -0
- package/scripts/tests/test_policy_lint_rules_validator_baseline.py +126 -0
- package/.agents/skills/session-continuity/AGENTS.md +0 -1
- package/.agents/skills/session-continuity/CLAUDE.md +0 -1
- package/.agents/skills/session-continuity/README.md +0 -223
- package/.agents/skills/session-continuity/SKILL.md +0 -125
- package/.agents/skills/session-continuity/continuity.mjs +0 -445
- package/.agents/skills/session-continuity/continuity.test.mjs +0 -381
- package/bin/install-session-continuity.mjs +0 -131
- package/bin/install-session-continuity.test.mjs +0 -106
- package/bin/install.pstack-plugin.test.mjs +0 -115
- package/bin/pstack-codex-discovery.test.mjs +0 -79
- package/bin/pstack-model-preferences.claude.json +0 -23
- package/bin/pstack-model-preferences.codex.json +0 -24
- package/bin/pstack.mjs +0 -392
- package/bin/pstack.test.mjs +0 -347
- package/bin/refresh-pstack-plugin-skills.test.mjs +0 -211
- package/bin/select-pstack-models.test.mjs +0 -727
- package/hooks/blocking/test_code_rules_enforcer_file_global_constants.py +0 -231
- package/rules/agent-spawn-protocol.md +0 -9
- package/rules/ask-user-question-required.md +0 -31
- package/rules/claims-as-quotes.md +0 -55
- package/rules/confirm-implementation-forks.md +0 -11
- package/rules/file-global-constants.md +0 -81
- package/rules/hedging-claims.md +0 -7
- package/rules/measurement-denominators.md +0 -39
- package/rules/prompt-workflow-context-controls.md +0 -19
- package/rules/pstack-host-mapping.md +0 -48
- package/rules/pstack-models.md +0 -142
- package/rules/vault-context.md +0 -5
- package/scripts/pstack-adapters/common.md +0 -17
- package/scripts/pstack-adapters/create-skill.md +0 -14
- package/scripts/pstack-adapters/host-claude.md +0 -7
- package/scripts/pstack-adapters/host-codex.md +0 -11
- package/scripts/pstack-adapters/host-cursor.md +0 -7
- package/scripts/pstack.lock.json +0 -11
- package/scripts/refresh_pstack_plugin_skills.mjs +0 -200
- package/scripts/select_pstack_models.mjs +0 -469
- /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
|
|
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(
|
|
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(
|
|
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
|
-
});
|