claude-dev-env 2.35.0 → 4.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/skills/_shared/advisor/scripts/README.md +1 -1
- package/.agents/skills/autoconverge/SKILL.md +19 -15
- package/.agents/skills/autoconverge/reference/gotchas.md +3 -3
- package/.agents/skills/autoconverge/reference/multi-pr.md +6 -2
- package/.agents/skills/autoconverge/workflow/converge.contract.test.mjs +11 -0
- package/.agents/skills/autoconverge/workflow/converge.mjs +26 -33
- package/.agents/skills/autoconverge/workflow/converge.run-input.test.mjs +26 -2
- package/.agents/skills/autoconverge/workflow/converge_multi.mjs +12 -28
- package/.agents/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +16 -0
- package/.agents/skills/team-advisor/SKILL.md +11 -11
- package/_shared/advisor/advisor-protocol.md +51 -51
- package/_shared/advisor/reference/advisor-block.md +9 -9
- package/_shared/advisor/reference/astra-rung.md +39 -0
- package/_shared/advisor/reference/identity.md +4 -4
- package/_shared/advisor/reference/lifecycle.md +6 -6
- package/_shared/advisor/reference/spawn-walk-log.md +9 -9
- package/_shared/advisor/reference/third-party-bind.md +11 -11
- package/_shared/advisor/reference/warm-up.md +13 -13
- package/_shared/advisor/scripts/codex_astra_advisor.py +308 -0
- package/_shared/advisor/scripts/codex_astra_preflight.py +115 -0
- package/_shared/advisor/scripts/codex_astra_reply.py +159 -0
- package/_shared/advisor/scripts/config/advisor_scripts_constants/advisor_route_constants.py +3 -3
- package/_shared/advisor/scripts/config/advisor_scripts_constants/astra_advisor_constants.py +38 -0
- package/_shared/advisor/scripts/model_tier_run_validator.py +17 -18
- package/_shared/advisor/scripts/tests/test_codex_astra_advisor.py +271 -0
- package/_shared/advisor/scripts/tests/test_codex_astra_preflight.py +24 -0
- package/_shared/advisor/scripts/tests/test_codex_astra_reply.py +42 -0
- package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +55 -30
- package/_shared/advisor/scripts/tests/test_tier_model_ids.py +14 -4
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +14 -0
- package/_shared/pr-loop/scripts/terminology_sweep.py +138 -50
- package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +13 -1
- package/bin/cde.mjs +175 -0
- package/bin/cde.test.mjs +222 -0
- package/bin/install-constants.mjs +4 -0
- package/bin/install.cursor-rules.test.mjs +233 -2
- package/bin/install.mjs +49 -3
- package/bin/pstack-model-preferences.codex.json +23 -0
- package/bin/select-pstack-models.test.mjs +378 -0
- package/docs/references/team-advisor-skill.md +3 -3
- package/hooks/hooks_constants/tool_names.py +20 -0
- package/hooks/validators/README.md +35 -1
- package/hooks/validators/fast_save_validators.py +63 -89
- package/hooks/validators/run_all_validators.py +16 -8
- package/hooks/validators/test_fast_save_validators_policy_lint.py +25 -0
- package/hooks/validators/test_run_all_validators_policy_lint.py +52 -0
- package/package.json +3 -2
- package/rules/pstack-models.md +119 -0
- package/scripts/cde_lint.py +258 -0
- package/scripts/check.ps1 +18 -6
- package/scripts/policy_lint/__init__.py +41 -0
- package/scripts/policy_lint/adapter_configuration.py +91 -0
- package/scripts/policy_lint/adapter_detectors.py +368 -0
- package/scripts/policy_lint/adapter_pairing.py +145 -0
- package/scripts/policy_lint/adapter_support.py +97 -0
- package/scripts/policy_lint/adapters.py +376 -0
- package/scripts/policy_lint/config/__init__.py +1 -0
- package/scripts/policy_lint/config/constants.py +77 -0
- package/scripts/policy_lint/engine.py +224 -0
- package/scripts/policy_lint/model.py +388 -0
- package/scripts/policy_lint/registry.py +171 -0
- package/scripts/policy_lint/render.py +91 -0
- package/scripts/policy_lint/selection.py +372 -0
- package/scripts/policy_lint/selection_git.py +147 -0
- package/scripts/repository_checks/__init__.py +1 -0
- package/scripts/repository_checks/claude_md.py +108 -0
- package/scripts/repository_checks/config/__init__.py +1 -0
- package/scripts/repository_checks/config/constants.py +150 -0
- package/scripts/repository_checks/env_var_documentation.py +243 -0
- package/scripts/repository_checks/hook_modules.py +34 -0
- package/scripts/repository_checks/models.py +37 -0
- package/scripts/repository_checks/package_inventory.py +118 -0
- package/scripts/repository_checks/pytest_testpaths.py +84 -0
- package/scripts/repository_checks/reporting.py +39 -0
- package/scripts/repository_checks/runner.py +93 -0
- package/scripts/repository_checks/tracked_secrets.py +147 -0
- package/scripts/repository_checks/tracked_tree.py +28 -0
- package/scripts/repository_policy.py +120 -0
- package/scripts/select_pstack_models.mjs +304 -0
- package/scripts/tests/repository_policy_test_support.py +89 -0
- package/scripts/tests/test_cde_lint.py +398 -0
- package/scripts/tests/test_claude_md.py +68 -0
- package/scripts/tests/test_env_var_documentation.py +133 -0
- package/scripts/tests/test_hook_modules.py +17 -0
- package/scripts/tests/test_models.py +26 -0
- package/scripts/tests/test_package_inventory.py +96 -0
- package/scripts/tests/test_policy_lint_rules_engine.py +381 -0
- package/scripts/tests/test_policy_lint_rules_registry.py +369 -0
- package/scripts/tests/test_policy_lint_selection.py +373 -0
- package/scripts/tests/test_pytest_testpaths.py +66 -0
- package/scripts/tests/test_reporting.py +54 -0
- package/scripts/tests/test_repository_policy.py +71 -0
- package/scripts/tests/test_runner.py +59 -0
- package/scripts/tests/test_tracked_secrets.py +175 -0
- package/scripts/tests/test_tracked_tree.py +28 -0
- package/_shared/advisor/reference/sol-rung.md +0 -39
- package/_shared/advisor/scripts/codex_sol_advisor.py +0 -581
- package/_shared/advisor/scripts/config/advisor_scripts_constants/sol_advisor_constants.py +0 -38
- package/_shared/advisor/scripts/tests/test_codex_sol_advisor.py +0 -740
package/bin/cde.test.mjs
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import assert from "node:assert/strict";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import test from "node:test";
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
buildLintCommand,
|
|
10
|
+
createHelpText,
|
|
11
|
+
findPython,
|
|
12
|
+
main,
|
|
13
|
+
pythonInterpreterNames,
|
|
14
|
+
runLintCommand,
|
|
15
|
+
} from "./cde.mjs";
|
|
16
|
+
|
|
17
|
+
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
18
|
+
const lintScriptPath = path.join(packageRoot, "scripts", "cde_lint.py");
|
|
19
|
+
const packageManifest = JSON.parse(
|
|
20
|
+
readFileSync(new URL("../package.json", import.meta.url), "utf8"),
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
function createFakeChild() {
|
|
25
|
+
const childProcess = new EventEmitter();
|
|
26
|
+
childProcess.kill = (signalName) => {
|
|
27
|
+
childProcess.lastSignalName = signalName;
|
|
28
|
+
};
|
|
29
|
+
return childProcess;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
test("package exposes cde through its bin map", () => {
|
|
34
|
+
assert.equal(packageManifest.bin.cde, "bin/cde.mjs");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
test("help describes lint source modes", () => {
|
|
39
|
+
const helpText = createHelpText();
|
|
40
|
+
assert.match(helpText, /cde lint/);
|
|
41
|
+
assert.match(helpText, /--files/);
|
|
42
|
+
assert.match(helpText, /--staged/);
|
|
43
|
+
assert.match(helpText, /--base/);
|
|
44
|
+
assert.match(helpText, /--repository/);
|
|
45
|
+
assert.match(helpText, /--text-as/);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
test("prefers py then python on Windows and python3 then python on POSIX", () => {
|
|
50
|
+
assert.deepEqual(pythonInterpreterNames("win32"), ["py", "python"]);
|
|
51
|
+
assert.deepEqual(pythonInterpreterNames("linux"), ["python3", "python"]);
|
|
52
|
+
assert.deepEqual(pythonInterpreterNames("darwin"), ["python3", "python"]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
test("falls back to the next interpreter when the first cannot run", () => {
|
|
57
|
+
const selectedName = findPython(["py", "python"], (commandName) => commandName === "python");
|
|
58
|
+
assert.equal(selectedName, "python");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
test("forwards lint arguments to the python command without a shell", () => {
|
|
63
|
+
const command = buildLintCommand("python-test", ["--files", "a.py", "--format", "json"]);
|
|
64
|
+
assert.equal(command.executable, "python-test");
|
|
65
|
+
assert.equal(command.arguments[0], lintScriptPath);
|
|
66
|
+
assert.deepEqual(command.arguments.slice(1), ["--files", "a.py", "--format", "json"]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
test("strips the launcher python override from forwarded arguments", async () => {
|
|
71
|
+
let receivedCommand;
|
|
72
|
+
const exitCode = await main(["lint", "--python", "python-test", "--staged"], {
|
|
73
|
+
findPython: async () => "unused-python",
|
|
74
|
+
runCommand: async (command) => {
|
|
75
|
+
receivedCommand = command;
|
|
76
|
+
return 0;
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
assert.equal(exitCode, 0);
|
|
80
|
+
assert.equal(receivedCommand.executable, "python-test");
|
|
81
|
+
assert.deepEqual(receivedCommand.arguments, [lintScriptPath, "--staged"]);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
test("unknown command returns invalid input without spawning", async () => {
|
|
86
|
+
let wasRun = false;
|
|
87
|
+
const messages = [];
|
|
88
|
+
const originalWrite = process.stderr.write;
|
|
89
|
+
process.stderr.write = (message) => messages.push(message);
|
|
90
|
+
try {
|
|
91
|
+
const exitCode = await main(["not-lint"], {
|
|
92
|
+
findPython: async () => "python",
|
|
93
|
+
runCommand: async () => {
|
|
94
|
+
wasRun = true;
|
|
95
|
+
return 0;
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
assert.equal(exitCode, 2);
|
|
99
|
+
} finally {
|
|
100
|
+
process.stderr.write = originalWrite;
|
|
101
|
+
}
|
|
102
|
+
assert.equal(wasRun, false);
|
|
103
|
+
assert.match(messages.join(""), /lint/);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
test("missing python override value returns invalid input without spawning", async () => {
|
|
108
|
+
let wasRun = false;
|
|
109
|
+
const exitCode = await main(["lint", "--python"], {
|
|
110
|
+
findPython: async () => "python",
|
|
111
|
+
runCommand: async () => {
|
|
112
|
+
wasRun = true;
|
|
113
|
+
return 0;
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
assert.equal(exitCode, 2);
|
|
117
|
+
assert.equal(wasRun, false);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
test("missing interpreter returns invalid input without spawning", async () => {
|
|
122
|
+
let wasRun = false;
|
|
123
|
+
const originalWrite = process.stderr.write;
|
|
124
|
+
process.stderr.write = () => true;
|
|
125
|
+
try {
|
|
126
|
+
const exitCode = await main(["lint", "--staged"], {
|
|
127
|
+
findPython: async () => undefined,
|
|
128
|
+
runCommand: async () => {
|
|
129
|
+
wasRun = true;
|
|
130
|
+
return 0;
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
assert.equal(exitCode, 2);
|
|
134
|
+
} finally {
|
|
135
|
+
process.stderr.write = originalWrite;
|
|
136
|
+
}
|
|
137
|
+
assert.equal(wasRun, false);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
test("forwards child exit status", async () => {
|
|
142
|
+
const childProcess = createFakeChild();
|
|
143
|
+
const exitPromise = runLintCommand(
|
|
144
|
+
{ executable: "python", arguments: [lintScriptPath] },
|
|
145
|
+
{
|
|
146
|
+
runChild: () => childProcess,
|
|
147
|
+
},
|
|
148
|
+
);
|
|
149
|
+
childProcess.emit("close", 3, null);
|
|
150
|
+
assert.equal(await exitPromise, 3);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
test("forwards SIGINT to the child and never enables a shell", async () => {
|
|
155
|
+
const childProcess = createFakeChild();
|
|
156
|
+
const processEmitter = new EventEmitter();
|
|
157
|
+
let spawnOptions;
|
|
158
|
+
const exitPromise = runLintCommand(
|
|
159
|
+
{ executable: "python", arguments: [lintScriptPath, "--staged"] },
|
|
160
|
+
{
|
|
161
|
+
processEmitter,
|
|
162
|
+
runChild: (_executable, _allArguments, options) => {
|
|
163
|
+
spawnOptions = options;
|
|
164
|
+
return childProcess;
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
);
|
|
168
|
+
processEmitter.emit("SIGINT");
|
|
169
|
+
childProcess.emit("close", 0, null);
|
|
170
|
+
assert.equal(await exitPromise, 0);
|
|
171
|
+
assert.equal(childProcess.lastSignalName, "SIGINT");
|
|
172
|
+
assert.equal(spawnOptions.shell, false);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
test("tolerates forwarding a signal to a dead child", async () => {
|
|
177
|
+
const childProcess = createFakeChild();
|
|
178
|
+
childProcess.kill = () => {
|
|
179
|
+
throw new Error("child already exited");
|
|
180
|
+
};
|
|
181
|
+
const processEmitter = new EventEmitter();
|
|
182
|
+
const exitPromise = runLintCommand(
|
|
183
|
+
{ executable: "python", arguments: [lintScriptPath] },
|
|
184
|
+
{ processEmitter, runChild: () => childProcess },
|
|
185
|
+
);
|
|
186
|
+
processEmitter.emit("SIGTERM");
|
|
187
|
+
childProcess.emit("close", 0, null);
|
|
188
|
+
assert.equal(await exitPromise, 0);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
test("reports a child start failure as invalid input", async () => {
|
|
193
|
+
const childProcess = createFakeChild();
|
|
194
|
+
const processEmitter = new EventEmitter();
|
|
195
|
+
const messages = [];
|
|
196
|
+
processEmitter.stderr = { write: (message) => messages.push(message) };
|
|
197
|
+
const exitPromise = runLintCommand(
|
|
198
|
+
{ executable: "missing-python", arguments: [lintScriptPath] },
|
|
199
|
+
{ processEmitter, runChild: () => childProcess },
|
|
200
|
+
);
|
|
201
|
+
childProcess.emit("error", new Error("not found"));
|
|
202
|
+
assert.equal(await exitPromise, 2);
|
|
203
|
+
assert.deepEqual(messages, ["Unable to start the policy linter.\n"]);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
test("reports a synchronous child start failure as invalid input", async () => {
|
|
208
|
+
const processEmitter = new EventEmitter();
|
|
209
|
+
const messages = [];
|
|
210
|
+
processEmitter.stderr = { write: (message) => messages.push(message) };
|
|
211
|
+
const exitCode = await runLintCommand(
|
|
212
|
+
{ executable: "missing-python", arguments: [lintScriptPath] },
|
|
213
|
+
{
|
|
214
|
+
processEmitter,
|
|
215
|
+
runChild: () => {
|
|
216
|
+
throw new Error("not found");
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
);
|
|
220
|
+
assert.equal(exitCode, 2);
|
|
221
|
+
assert.deepEqual(messages, ["Unable to start the policy linter.\n"]);
|
|
222
|
+
});
|
|
@@ -55,6 +55,10 @@ export const RUN_BACKUP_DIRECTORY_NAME_PATTERN =
|
|
|
55
55
|
*/
|
|
56
56
|
export const PACKAGE_AGENTS_HOME_DIRECTORY_NAME = '.agents';
|
|
57
57
|
|
|
58
|
+
export const PSTACK_MODEL_RULE_FILE_NAME = 'pstack-models.mdc';
|
|
59
|
+
export const PSTACK_CODEX_MODEL_PREFERENCES_FILE_NAME =
|
|
60
|
+
'pstack-model-preferences.codex.json';
|
|
61
|
+
|
|
58
62
|
/**
|
|
59
63
|
* The directory name skill directories carry in a package source and under
|
|
60
64
|
* the agents home.
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { test } from 'node:test';
|
|
2
2
|
import { strict as assert } from 'node:assert';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
mkdtempSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
writeFileSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
existsSync,
|
|
9
|
+
rmSync,
|
|
10
|
+
} from 'node:fs';
|
|
4
11
|
import { tmpdir } from 'node:os';
|
|
5
12
|
import { dirname, join } from 'node:path';
|
|
6
13
|
import { fileURLToPath } from 'node:url';
|
|
@@ -18,7 +25,7 @@ const THIS_DIRECTORY = dirname(fileURLToPath(import.meta.url));
|
|
|
18
25
|
const INSTALLER_PATH = join(THIS_DIRECTORY, 'install.mjs');
|
|
19
26
|
const PACKAGE_DIRECTORY = dirname(THIS_DIRECTORY);
|
|
20
27
|
|
|
21
|
-
function runInstaller(homeDirectory, extraArguments) {
|
|
28
|
+
function runInstaller(homeDirectory, extraArguments, environmentOverrides = {}) {
|
|
22
29
|
return execFileSync('node', [INSTALLER_PATH, ...extraArguments], {
|
|
23
30
|
cwd: PACKAGE_DIRECTORY,
|
|
24
31
|
encoding: 'utf8',
|
|
@@ -26,11 +33,21 @@ function runInstaller(homeDirectory, extraArguments) {
|
|
|
26
33
|
...process.env,
|
|
27
34
|
HOME: homeDirectory,
|
|
28
35
|
USERPROFILE: homeDirectory,
|
|
36
|
+
CODEX_HOME: join(homeDirectory, '.codex'),
|
|
37
|
+
CLAUDE_CONFIG_DIR: join(homeDirectory, '.claude'),
|
|
29
38
|
GIT_CONFIG_GLOBAL: join(homeDirectory, '.gitconfig'),
|
|
39
|
+
...environmentOverrides,
|
|
30
40
|
},
|
|
31
41
|
});
|
|
32
42
|
}
|
|
33
43
|
|
|
44
|
+
function runSelector(selectorPath, selectionInput) {
|
|
45
|
+
return JSON.parse(execFileSync('node', [selectorPath], {
|
|
46
|
+
encoding: 'utf8',
|
|
47
|
+
input: JSON.stringify(selectionInput),
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
50
|
+
|
|
34
51
|
test('resolveInstallRoot names ~/.cursor/rules and allows generated mdc files under it', () => {
|
|
35
52
|
const homeDirectory = join(tmpdir(), 'cdev-cursor-rules-home');
|
|
36
53
|
const resolution = resolveInstallRoot({
|
|
@@ -92,11 +109,225 @@ test('--only journal skips Cursor rule generation; --only core writes them', ()
|
|
|
92
109
|
CURSOR_RULES_DIRECTORY_NAME,
|
|
93
110
|
'asd-ste100-language.mdc',
|
|
94
111
|
);
|
|
112
|
+
const preferencePath = join(
|
|
113
|
+
homeDirectory,
|
|
114
|
+
'.agents',
|
|
115
|
+
'rules',
|
|
116
|
+
'pstack-model-preferences.codex.json',
|
|
117
|
+
);
|
|
95
118
|
runInstaller(homeDirectory, ['--only', 'journal']);
|
|
96
119
|
assert.equal(existsSync(generatedPath), false);
|
|
120
|
+
assert.equal(existsSync(preferencePath), false);
|
|
97
121
|
|
|
98
122
|
runInstaller(homeDirectory, ['--only', 'core']);
|
|
99
123
|
assert.equal(existsSync(generatedPath), true);
|
|
124
|
+
assert.equal(existsSync(preferencePath), true);
|
|
125
|
+
} finally {
|
|
126
|
+
rmSync(homeDirectory, { recursive: true, force: true });
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
for (const targetName of [null, 'profile']) {
|
|
131
|
+
test('pstack rules survive reinstall and sync for ' + (targetName ?? 'main'), () => {
|
|
132
|
+
const homeDirectory = mkdtempSync(join(tmpdir(), 'cdev-pstack-'));
|
|
133
|
+
try {
|
|
134
|
+
const targetArguments = targetName
|
|
135
|
+
? ['--target', join(homeDirectory, targetName)]
|
|
136
|
+
: [];
|
|
137
|
+
const resolution = resolveInstallRoot({
|
|
138
|
+
homeDirectory,
|
|
139
|
+
environment: {},
|
|
140
|
+
explicitTarget: targetName ? join(homeDirectory, targetName) : null,
|
|
141
|
+
});
|
|
142
|
+
const sharedPath = join(resolution.agentsHome, 'rules', 'pstack-models.mdc');
|
|
143
|
+
const cursorPath = join(resolution.cursorRulesInstallDirectory, 'pstack-models.mdc');
|
|
144
|
+
const preferencesPath = join(
|
|
145
|
+
resolution.agentsHome,
|
|
146
|
+
'rules',
|
|
147
|
+
'pstack-model-preferences.codex.json',
|
|
148
|
+
);
|
|
149
|
+
const selectorPath = join(
|
|
150
|
+
resolution.agentsHome,
|
|
151
|
+
'scripts',
|
|
152
|
+
'select_pstack_models.mjs',
|
|
153
|
+
);
|
|
154
|
+
const managedSelectorPath = join(
|
|
155
|
+
resolution.managedRoot,
|
|
156
|
+
'scripts',
|
|
157
|
+
'select_pstack_models.mjs',
|
|
158
|
+
);
|
|
159
|
+
runInstaller(homeDirectory, [...targetArguments, '--only', 'core']);
|
|
160
|
+
|
|
161
|
+
const installedText = readFileSync(sharedPath, 'utf8');
|
|
162
|
+
assert.doesNotMatch(installedText, /gpt-|claude-|grok-/i);
|
|
163
|
+
assert.match(installedText, /feature, refactoring: reliable code execution/);
|
|
164
|
+
assert.match(installedText, /interrogate reviewers: independent adversarial review/);
|
|
165
|
+
assert.match(installedText, /alwaysApply: true/);
|
|
166
|
+
assert.match(installedText, /replaces the fixed model defaults in pstack skills/);
|
|
167
|
+
assert.match(installedText, /Run the installed selector once for each agent/);
|
|
168
|
+
assert.equal(readFileSync(cursorPath, 'utf8'), installedText);
|
|
169
|
+
assert.equal(isAllowedInstallDestination(sharedPath, resolution), true);
|
|
170
|
+
assert.equal(existsSync(selectorPath), true);
|
|
171
|
+
|
|
172
|
+
const savedPreferences = JSON.parse(readFileSync(preferencesPath, 'utf8'));
|
|
173
|
+
assert.deepEqual(savedPreferences, {
|
|
174
|
+
host: 'codex',
|
|
175
|
+
modelsByRole: {
|
|
176
|
+
'feature, refactoring': ['gpt-5.6-sol'],
|
|
177
|
+
'bug-fix': ['gpt-5.6-sol'],
|
|
178
|
+
'perf-issue': ['gpt-6-astra'],
|
|
179
|
+
hillclimb: ['gpt-6-astra'],
|
|
180
|
+
'judgment and prose': ['gpt-6-astra'],
|
|
181
|
+
'hardest tasks': ['gpt-6-astra'],
|
|
182
|
+
'how explorer': ['gpt-5.6-terra'],
|
|
183
|
+
'how explainer': ['gpt-5.6-sol'],
|
|
184
|
+
'how critics': ['gpt-5.6-terra', 'gpt-5.6-sol', 'gpt-6-astra'],
|
|
185
|
+
'why investigators': ['gpt-5.6-terra'],
|
|
186
|
+
'why synthesizer': ['gpt-6-astra'],
|
|
187
|
+
'reflect tooling': ['gpt-5.6-sol'],
|
|
188
|
+
'reflect judgment, divergent, synthesizer': ['gpt-6-astra'],
|
|
189
|
+
'arena runners': ['gpt-5.6-terra', 'gpt-5.6-sol', 'gpt-6-astra'],
|
|
190
|
+
'arena cross-judge pool': ['gpt-5.6-sol', 'gpt-6-astra'],
|
|
191
|
+
'swarm workers': ['gpt-5.6-luna'],
|
|
192
|
+
'architect runners': ['gpt-5.6-sol', 'gpt-6-astra'],
|
|
193
|
+
'interrogate reviewers': [
|
|
194
|
+
'gpt-5.6-terra',
|
|
195
|
+
'gpt-5.6-sol',
|
|
196
|
+
'gpt-6-astra',
|
|
197
|
+
],
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
for (const role of Object.keys(savedPreferences.modelsByRole)) {
|
|
201
|
+
assert.equal(installedText.includes(role + ':'), true);
|
|
202
|
+
}
|
|
203
|
+
const codexSelection = runSelector(selectorPath, {
|
|
204
|
+
host: 'codex',
|
|
205
|
+
inventoryHost: 'codex',
|
|
206
|
+
role: 'feature, refactoring',
|
|
207
|
+
delegationIndex: 0,
|
|
208
|
+
availableModelIds: ['gpt-5.6-sol'],
|
|
209
|
+
confirmedSuitableModelIds: [],
|
|
210
|
+
parentFallback: {
|
|
211
|
+
isAllowed: false,
|
|
212
|
+
hasMaterialCapabilityLoss: false,
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
assert.deepEqual(codexSelection.nativeSpawnArguments, {
|
|
216
|
+
model: 'gpt-5.6-sol',
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const foreignPreferencesPath = join(
|
|
220
|
+
resolution.agentsHome,
|
|
221
|
+
'rules',
|
|
222
|
+
'pstack-model-preferences.claude.json',
|
|
223
|
+
);
|
|
224
|
+
writeFileSync(foreignPreferencesPath, JSON.stringify({
|
|
225
|
+
host: 'claude',
|
|
226
|
+
modelsByRole: {
|
|
227
|
+
'feature, refactoring': ['confirmed-claude-model'],
|
|
228
|
+
},
|
|
229
|
+
}));
|
|
230
|
+
const foreignSelection = runSelector(selectorPath, {
|
|
231
|
+
host: 'claude',
|
|
232
|
+
inventoryHost: 'claude',
|
|
233
|
+
role: 'feature, refactoring',
|
|
234
|
+
delegationIndex: 0,
|
|
235
|
+
availableModelIds: ['confirmed-claude-model'],
|
|
236
|
+
confirmedSuitableModelIds: [],
|
|
237
|
+
parentFallback: {
|
|
238
|
+
isAllowed: false,
|
|
239
|
+
hasMaterialCapabilityLoss: false,
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
assert.deepEqual(foreignSelection.nativeSpawnArguments, {
|
|
243
|
+
model: 'confirmed-claude-model',
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
const manifest = JSON.parse(readFileSync(resolution.manifestFilePath, 'utf8'));
|
|
247
|
+
assert.equal(manifest.files.includes(sharedPath), true);
|
|
248
|
+
assert.equal(manifest.files.includes(managedSelectorPath), true);
|
|
249
|
+
assert.equal(manifest.files.includes(preferencesPath), false);
|
|
250
|
+
assert.equal(manifest.files.includes(foreignPreferencesPath), false);
|
|
251
|
+
|
|
252
|
+
writeFileSync(sharedPath, 'stale');
|
|
253
|
+
writeFileSync(preferencesPath, JSON.stringify({
|
|
254
|
+
host: 'codex',
|
|
255
|
+
modelsByRole: {
|
|
256
|
+
'feature, refactoring': ['user-edited-codex-model'],
|
|
257
|
+
},
|
|
258
|
+
}));
|
|
259
|
+
runInstaller(homeDirectory, [...targetArguments, '--only', 'core']);
|
|
260
|
+
assert.equal(readFileSync(sharedPath, 'utf8'), installedText);
|
|
261
|
+
assert.deepEqual(
|
|
262
|
+
JSON.parse(readFileSync(preferencesPath, 'utf8'))
|
|
263
|
+
.modelsByRole['feature, refactoring'],
|
|
264
|
+
['user-edited-codex-model'],
|
|
265
|
+
);
|
|
266
|
+
assert.deepEqual(
|
|
267
|
+
JSON.parse(readFileSync(foreignPreferencesPath, 'utf8'))
|
|
268
|
+
.modelsByRole['feature, refactoring'],
|
|
269
|
+
['confirmed-claude-model'],
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
const syncArguments = [
|
|
273
|
+
join(resolution.managedRoot, 'scripts', 'sync_to_cursor.py'),
|
|
274
|
+
'--claude-root', resolution.managedRoot,
|
|
275
|
+
'--cursor-root', resolution.cursorInstallDirectory,
|
|
276
|
+
'--quiet',
|
|
277
|
+
];
|
|
278
|
+
execFileSync('python', syncArguments);
|
|
279
|
+
execFileSync('python', [...syncArguments, '--check']);
|
|
280
|
+
assert.equal(readFileSync(cursorPath, 'utf8'), installedText);
|
|
281
|
+
assert.equal(readFileSync(sharedPath, 'utf8'), installedText);
|
|
282
|
+
if (targetName) {
|
|
283
|
+
assert.equal(
|
|
284
|
+
existsSync(join(homeDirectory, '.agents', 'rules', 'pstack-models.mdc')),
|
|
285
|
+
false,
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
runInstaller(homeDirectory, [...targetArguments, '--uninstall']);
|
|
290
|
+
assert.equal(existsSync(sharedPath), false);
|
|
291
|
+
assert.equal(existsSync(cursorPath), false);
|
|
292
|
+
assert.equal(existsSync(selectorPath), false);
|
|
293
|
+
assert.equal(existsSync(preferencesPath), true);
|
|
294
|
+
assert.equal(existsSync(foreignPreferencesPath), true);
|
|
295
|
+
} finally {
|
|
296
|
+
rmSync(homeDirectory, { recursive: true, force: true });
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
test('a blocked shared rule destination rolls back generated Cursor files', () => {
|
|
302
|
+
const homeDirectory = mkdtempSync(join(tmpdir(), 'cdev-pstack-rollback-'));
|
|
303
|
+
try {
|
|
304
|
+
const sharedRulesPath = join(homeDirectory, '.agents', 'rules');
|
|
305
|
+
mkdirSync(dirname(sharedRulesPath), { recursive: true });
|
|
306
|
+
writeFileSync(sharedRulesPath, 'keep-existing-file');
|
|
307
|
+
assert.throws(() => runInstaller(homeDirectory, ['--only', 'core']));
|
|
308
|
+
assert.equal(readFileSync(sharedRulesPath, 'utf8'), 'keep-existing-file');
|
|
309
|
+
assert.equal(existsSync(join(homeDirectory, '.cursor', 'rules', 'pstack-models.mdc')), false);
|
|
310
|
+
assert.equal(existsSync(join(homeDirectory, '.cursor', '.sync-manifest.json')), false);
|
|
311
|
+
} finally {
|
|
312
|
+
rmSync(homeDirectory, { recursive: true, force: true });
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test('a failed install rolls back a newly seeded Codex preference file', () => {
|
|
317
|
+
const homeDirectory = mkdtempSync(join(tmpdir(), 'cdev-pstack-seed-rollback-'));
|
|
318
|
+
try {
|
|
319
|
+
const preferencesPath = join(
|
|
320
|
+
homeDirectory,
|
|
321
|
+
'.agents',
|
|
322
|
+
'rules',
|
|
323
|
+
'pstack-model-preferences.codex.json',
|
|
324
|
+
);
|
|
325
|
+
assert.throws(() => runInstaller(
|
|
326
|
+
homeDirectory,
|
|
327
|
+
['--only', 'core'],
|
|
328
|
+
{ CLAUDE_DEV_ENV_INSTALL_FAULT: 'after_file_staging' },
|
|
329
|
+
));
|
|
330
|
+
assert.equal(existsSync(preferencesPath), false);
|
|
100
331
|
} finally {
|
|
101
332
|
rmSync(homeDirectory, { recursive: true, force: true });
|
|
102
333
|
}
|
package/bin/install.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, copyFileSync, unlinkSync, rmSync, rmdirSync, renameSync, realpathSync, lstatSync } from 'node:fs';
|
|
3
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, copyFileSync, unlinkSync, rmSync, rmdirSync, renameSync, realpathSync, lstatSync, constants as filesystemConstants } from 'node:fs';
|
|
4
4
|
import { join, dirname, resolve, relative, basename, isAbsolute, extname } from 'node:path';
|
|
5
5
|
import { homedir } from 'node:os';
|
|
6
6
|
import { execSync, execFileSync, spawnSync } from 'node:child_process';
|
|
@@ -26,6 +26,9 @@ import {
|
|
|
26
26
|
SETTINGS_FILE_NAME,
|
|
27
27
|
CODEX_RULES_PACKAGE_DIRECTORY_NAME,
|
|
28
28
|
CURSOR_SYNC_SCRIPT_FILE_NAME,
|
|
29
|
+
CURSOR_RULES_DIRECTORY_NAME,
|
|
30
|
+
PSTACK_MODEL_RULE_FILE_NAME,
|
|
31
|
+
PSTACK_CODEX_MODEL_PREFERENCES_FILE_NAME,
|
|
29
32
|
WINDOWS_PYTHON_LAUNCHER_COMMAND,
|
|
30
33
|
PYTHON_PROBE_TIMEOUT_MILLISECONDS,
|
|
31
34
|
} from './install-constants.mjs';
|
|
@@ -422,6 +425,31 @@ export function runCursorRuleSync(pythonCommand, scriptPath, claudeRoot, cursorR
|
|
|
422
425
|
);
|
|
423
426
|
}
|
|
424
427
|
|
|
428
|
+
function seedCodexPstackPreferences() {
|
|
429
|
+
const preferenceDirectory = join(AGENTS_HOME, CURSOR_RULES_DIRECTORY_NAME);
|
|
430
|
+
const preferencePath = join(
|
|
431
|
+
preferenceDirectory,
|
|
432
|
+
PSTACK_CODEX_MODEL_PREFERENCES_FILE_NAME,
|
|
433
|
+
);
|
|
434
|
+
const preferenceSource = join(
|
|
435
|
+
PACKAGE_ROOT,
|
|
436
|
+
'bin',
|
|
437
|
+
PSTACK_CODEX_MODEL_PREFERENCES_FILE_NAME,
|
|
438
|
+
);
|
|
439
|
+
mkdirSync(preferenceDirectory, { recursive: true });
|
|
440
|
+
try {
|
|
441
|
+
copyFileSync(
|
|
442
|
+
preferenceSource,
|
|
443
|
+
preferencePath,
|
|
444
|
+
filesystemConstants.COPYFILE_EXCL,
|
|
445
|
+
);
|
|
446
|
+
return preferencePath;
|
|
447
|
+
} catch (copyError) {
|
|
448
|
+
if (copyError.code === 'EEXIST') return null;
|
|
449
|
+
throw copyError;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
425
453
|
/**
|
|
426
454
|
* Formats an absolute interpreter path as a settings.json hook command prefix:
|
|
427
455
|
* forward-slash separators, double-quoted when the path contains a space so the
|
|
@@ -2130,6 +2158,7 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
|
|
|
2130
2158
|
];
|
|
2131
2159
|
|
|
2132
2160
|
const allInstalledFiles = [];
|
|
2161
|
+
const allUserOwnedPreferencePaths = new Set();
|
|
2133
2162
|
const summary = {};
|
|
2134
2163
|
for (const directory of CONTENT_DIRECTORIES) {
|
|
2135
2164
|
const hasFullAccess = !allowedDirectories || allowedDirectories.has(directory);
|
|
@@ -2237,6 +2266,20 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
|
|
|
2237
2266
|
const generatedCursorPaths = collectManagedCursorSyncPaths(cursorRoot);
|
|
2238
2267
|
allInstalledFiles.push(...generatedCursorPaths);
|
|
2239
2268
|
summary.cursorRules = { created: generatedCursorPaths.length, updated: 0, paths: generatedCursorPaths };
|
|
2269
|
+
syncWrittenPaths(allInstalledFiles);
|
|
2270
|
+
const pstackRuleSource = join(INSTALL_ROOT_RESOLUTION.cursorRulesInstallDirectory, PSTACK_MODEL_RULE_FILE_NAME);
|
|
2271
|
+
const sharedRuleDirectory = join(AGENTS_HOME, CURSOR_RULES_DIRECTORY_NAME);
|
|
2272
|
+
const sharedRulePath = join(sharedRuleDirectory, PSTACK_MODEL_RULE_FILE_NAME);
|
|
2273
|
+
mkdirSync(sharedRuleDirectory, { recursive: true });
|
|
2274
|
+
copyFileSync(pstackRuleSource, sharedRulePath);
|
|
2275
|
+
allInstalledFiles.push(sharedRulePath);
|
|
2276
|
+
syncWrittenPaths(allInstalledFiles);
|
|
2277
|
+
const seededPreferencePath = seedCodexPstackPreferences();
|
|
2278
|
+
if (seededPreferencePath) {
|
|
2279
|
+
allInstalledFiles.push(seededPreferencePath);
|
|
2280
|
+
allUserOwnedPreferencePaths.add(seededPreferencePath);
|
|
2281
|
+
}
|
|
2282
|
+
syncWrittenPaths(allInstalledFiles);
|
|
2240
2283
|
}
|
|
2241
2284
|
let skillsCreated = 0;
|
|
2242
2285
|
let skillsUpdated = 0;
|
|
@@ -2420,9 +2463,12 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
|
|
|
2420
2463
|
const manifestSkillNames = didPruneFinish
|
|
2421
2464
|
? [...installedSkillNames].sort()
|
|
2422
2465
|
: unionOfSkillNames(priorManifestSkills || [], [...installedSkillNames]).sort();
|
|
2466
|
+
const allManagedInstalledFiles = allInstalledFiles.filter(
|
|
2467
|
+
eachPath => !allUserOwnedPreferencePaths.has(eachPath),
|
|
2468
|
+
);
|
|
2423
2469
|
const manifestFiles = didPruneFinish
|
|
2424
|
-
? manifestFilesWithFailedPrunes(
|
|
2425
|
-
: unionOnComparisonKey(priorManifestFiles || [],
|
|
2470
|
+
? manifestFilesWithFailedPrunes(allManagedInstalledFiles, failedPrunePaths)
|
|
2471
|
+
: unionOnComparisonKey(priorManifestFiles || [], allManagedInstalledFiles);
|
|
2426
2472
|
const managedPermissionDenyEntries = summary.managedPermissions.managedDenyEntries;
|
|
2427
2473
|
writeManifest(
|
|
2428
2474
|
manifestFiles,
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"host": "codex",
|
|
3
|
+
"modelsByRole": {
|
|
4
|
+
"feature, refactoring": ["gpt-5.6-sol"],
|
|
5
|
+
"bug-fix": ["gpt-5.6-sol"],
|
|
6
|
+
"perf-issue": ["gpt-6-astra"],
|
|
7
|
+
"hillclimb": ["gpt-6-astra"],
|
|
8
|
+
"judgment and prose": ["gpt-6-astra"],
|
|
9
|
+
"hardest tasks": ["gpt-6-astra"],
|
|
10
|
+
"how explorer": ["gpt-5.6-terra"],
|
|
11
|
+
"how explainer": ["gpt-5.6-sol"],
|
|
12
|
+
"how critics": ["gpt-5.6-terra", "gpt-5.6-sol", "gpt-6-astra"],
|
|
13
|
+
"why investigators": ["gpt-5.6-terra"],
|
|
14
|
+
"why synthesizer": ["gpt-6-astra"],
|
|
15
|
+
"reflect tooling": ["gpt-5.6-sol"],
|
|
16
|
+
"reflect judgment, divergent, synthesizer": ["gpt-6-astra"],
|
|
17
|
+
"arena runners": ["gpt-5.6-terra", "gpt-5.6-sol", "gpt-6-astra"],
|
|
18
|
+
"arena cross-judge pool": ["gpt-5.6-sol", "gpt-6-astra"],
|
|
19
|
+
"swarm workers": ["gpt-5.6-luna"],
|
|
20
|
+
"architect runners": ["gpt-5.6-sol", "gpt-6-astra"],
|
|
21
|
+
"interrogate reviewers": ["gpt-5.6-terra", "gpt-5.6-sol", "gpt-6-astra"]
|
|
22
|
+
}
|
|
23
|
+
}
|