claude-dev-env 8.10.0 → 8.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_regression.py +85 -3
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_regression.py +34 -0
- package/_shared/pr-loop/scripts/fix_pr_test_proof.py +233 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/fix_pr_test_proof_constants.py +47 -0
- package/_shared/pr-loop/scripts/test_fix_pr_test_proof.py +186 -0
- package/bin/codex-skill-load-block.mjs +93 -0
- package/bin/codex-skill-load-block.test.mjs +134 -0
- package/bin/install.agents-home.test.mjs +76 -1
- package/bin/install.mjs +55 -31
- package/bin/install.test.mjs +57 -0
- package/bin/install.transaction.test.mjs +29 -7
- package/bin/seed-codex-pstack-models.mjs +27 -0
- package/bin/seed-codex-pstack-models.test.mjs +72 -0
- package/docs/CODE_RULES.md +5 -1
- package/hooks/blocking/code_rules_enforcer.py +1 -3
- package/hooks/blocking/code_rules_magic_values.py +1 -3
- package/hooks/blocking/code_rules_paired_test.py +5 -6
- package/hooks/blocking/pii_prevention_blocker_parts/repository_exemption.py +3 -3
- package/hooks/blocking/tdd_enforcer_parts/__init__.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_ephemeral.py +1 -46
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +2 -40
- package/hooks/blocking/tests/test_repository_exemption.py +7 -0
- package/hooks/followup_ledger.py +8 -1
- package/hooks/git-hooks/test_installed_policy_lint.py +4 -2
- package/hooks/hooks.json +15 -0
- package/hooks/hooks_constants/followup_ledger_constants.py +4 -0
- package/hooks/hooks_constants/messages.py +0 -1
- package/hooks/hooks_constants/nested_project_hooks_constants.py +37 -0
- package/hooks/hooks_constants/test_messages.py +1 -7
- package/hooks/lifecycle/nested_project_hooks.py +395 -0
- package/hooks/lifecycle/test_nested_project_hooks.py +251 -0
- package/hooks/test_followup_ledger.py +18 -0
- package/package.json +2 -1
- package/pstack-codex-models.md +37 -0
- package/rules/bdd.md +1 -1
- package/rules/build-needs-a-user.md +53 -0
- package/rules/code-standards.md +1 -1
- package/rules/flag-non-breaking-findings.md +7 -0
- package/rules/paired-test-coverage.md +4 -2
- package/rules/testing.md +5 -1
- package/scripts/cde_lint.py +47 -1
- package/scripts/policy_lint/adapter_callers.py +124 -0
- package/scripts/policy_lint/adapter_pairing.py +4 -2
- package/scripts/policy_lint/adapter_support.py +4 -3
- package/scripts/policy_lint/adapters.py +15 -0
- package/scripts/policy_lint/check_catalog.py +24 -0
- package/scripts/policy_lint/config/approved_test_pairs.py +3 -2
- package/scripts/policy_lint/config/check_catalog_constants.py +13 -0
- package/scripts/policy_lint/config/constants.py +18 -0
- package/scripts/policy_lint/engine.py +9 -3
- package/scripts/policy_lint/model.py +6 -2
- package/scripts/policy_lint/registry.py +6 -0
- package/scripts/policy_lint/selection.py +2 -0
- package/scripts/repository_checks/tracked_private_terms.py +15 -1
- package/scripts/tests/test_cde_lint.py +50 -0
- package/scripts/tests/test_policy_lint_rules_archive.py +16 -0
- package/scripts/tests/test_policy_lint_rules_callers.py +140 -0
- package/scripts/tests/test_policy_lint_rules_check_catalog.py +25 -1
- package/scripts/tests/test_policy_lint_rules_pairing.py +31 -2
- package/scripts/tests/test_policy_lint_selection.py +35 -0
- package/scripts/tests/test_tracked_private_terms.py +35 -0
- package/hooks/blocking/tdd_enforcer.py +0 -195
- package/hooks/blocking/tdd_enforcer_parts/candidate_paths.py +0 -142
- package/hooks/blocking/tdd_enforcer_parts/config/__init__.py +0 -1
- package/hooks/blocking/tdd_enforcer_parts/config/tdd_enforcer_constants.py +0 -49
- package/hooks/blocking/tdd_enforcer_parts/content_hash_store.py +0 -298
- package/hooks/blocking/tdd_enforcer_parts/decisions.py +0 -96
- package/hooks/blocking/tdd_enforcer_parts/freshness.py +0 -95
- package/hooks/blocking/tdd_enforcer_parts/git_tracking.py +0 -132
- package/hooks/blocking/tdd_enforcer_parts/path_classification.py +0 -119
- package/hooks/blocking/tdd_enforcer_parts/tests/test_candidate_paths.py +0 -31
- package/hooks/blocking/tdd_enforcer_parts/tests/test_content_hash_store.py +0 -256
- package/hooks/blocking/tdd_enforcer_parts/tests/test_decisions.py +0 -46
- package/hooks/blocking/tdd_enforcer_parts/tests/test_freshness.py +0 -28
- package/hooks/blocking/tdd_enforcer_parts/tests/test_git_tracking.py +0 -111
- package/hooks/blocking/tdd_enforcer_parts/tests/test_path_classification.py +0 -36
- package/hooks/blocking/test_tdd_enforcer.py +0 -1506
- package/hooks/blocking/test_tdd_enforcer_restore.py +0 -108
- package/hooks/blocking/test_tdd_enforcer_scratchpad.py +0 -105
- package/scripts/codex_compat_watcher.py +0 -502
- package/scripts/tests/test_codex_compat_watcher.py +0 -599
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { lstatSync, mkdirSync, readFileSync, readlinkSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join, resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
export const SKILL_LOAD_BLOCK_START = '<!-- claude-dev-env skill load: start -->';
|
|
5
|
+
export const SKILL_LOAD_BLOCK_END = '<!-- claude-dev-env skill load: end -->';
|
|
6
|
+
export const SKILL_LOAD_INSTRUCTION = 'Load the pstack:poteto-mode skill before your first tool call or reply: '
|
|
7
|
+
+ 'open its SKILL.md from your skills list and follow it. This binds every Codex agent, '
|
|
8
|
+
+ 'including a helper started with spawn_agent, whatever its task message says.';
|
|
9
|
+
export const PACKAGE_GUIDANCE_BLOCK_START = '<!-- claude-dev-env package guidance: start -->';
|
|
10
|
+
export const PACKAGE_GUIDANCE_BLOCK_END = '<!-- claude-dev-env package guidance: end -->';
|
|
11
|
+
|
|
12
|
+
const WINDOWS_LONG_PATH_PREFIX = '\\\\?\\';
|
|
13
|
+
|
|
14
|
+
function managedBlock(blockStart, blockBody, blockEnd) {
|
|
15
|
+
const terminatedBody = blockBody.endsWith('\n') ? blockBody : `${blockBody}\n`;
|
|
16
|
+
return `${blockStart}\n${terminatedBody}${blockEnd}\n`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function withBlockReplaced(guidanceText, blockStart, blockEnd, block) {
|
|
20
|
+
const startIndex = guidanceText.indexOf(blockStart);
|
|
21
|
+
const endIndex = startIndex === -1 ? -1 : guidanceText.indexOf(blockEnd, startIndex);
|
|
22
|
+
if (endIndex === -1) return null;
|
|
23
|
+
let afterBlockIndex = endIndex + blockEnd.length;
|
|
24
|
+
if (guidanceText[afterBlockIndex] === '\n') afterBlockIndex += 1;
|
|
25
|
+
return guidanceText.slice(0, startIndex) + block + guidanceText.slice(afterBlockIndex);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Return the Codex guidance text with the package's skill-load block in it.
|
|
30
|
+
*
|
|
31
|
+
* Codex starts a spawn_agent helper without running any hook, so the global
|
|
32
|
+
* AGENTS.md is the one channel that reaches every helper. A block already in
|
|
33
|
+
* the text is replaced where it stands; otherwise the block goes first.
|
|
34
|
+
*/
|
|
35
|
+
export function withSkillLoadBlock(guidanceText) {
|
|
36
|
+
const block = managedBlock(SKILL_LOAD_BLOCK_START, SKILL_LOAD_INSTRUCTION, SKILL_LOAD_BLOCK_END);
|
|
37
|
+
const replacedText = withBlockReplaced(guidanceText, SKILL_LOAD_BLOCK_START, SKILL_LOAD_BLOCK_END, block);
|
|
38
|
+
if (replacedText !== null) return replacedText;
|
|
39
|
+
return guidanceText ? `${block}\n${guidanceText}` : block;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function withPackageGuidanceBlock(guidanceText, packageGuidanceText) {
|
|
43
|
+
const block = managedBlock(PACKAGE_GUIDANCE_BLOCK_START, packageGuidanceText, PACKAGE_GUIDANCE_BLOCK_END);
|
|
44
|
+
const replacedText = withBlockReplaced(
|
|
45
|
+
guidanceText,
|
|
46
|
+
PACKAGE_GUIDANCE_BLOCK_START,
|
|
47
|
+
PACKAGE_GUIDANCE_BLOCK_END,
|
|
48
|
+
block,
|
|
49
|
+
);
|
|
50
|
+
return replacedText ?? `${guidanceText}\n${block}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function comparablePath(filePath) {
|
|
54
|
+
const withoutPrefix = filePath.startsWith(WINDOWS_LONG_PATH_PREFIX)
|
|
55
|
+
? filePath.slice(WINDOWS_LONG_PATH_PREFIX.length)
|
|
56
|
+
: filePath;
|
|
57
|
+
const resolvedPath = resolve(withoutPrefix);
|
|
58
|
+
return process.platform === 'win32' ? resolvedPath.toLowerCase() : resolvedPath;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function linksToPackageGuidance(agentsPath, allPackageGuidancePaths) {
|
|
62
|
+
const linkTarget = comparablePath(resolve(dirname(agentsPath), readlinkSync(agentsPath)));
|
|
63
|
+
return allPackageGuidancePaths.some(eachPath => comparablePath(eachPath) === linkTarget);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Keep CODEX_HOME/AGENTS.md carrying the skill-load block.
|
|
68
|
+
*
|
|
69
|
+
* A symbolic link that points at the package guidance (the shared agents-home
|
|
70
|
+
* copy, or the retired copy under the Claude home) is replaced by a regular
|
|
71
|
+
* file holding the skill-load block and a package-guidance block, so Codex
|
|
72
|
+
* keeps both after the package moves its guidance. A file that already holds
|
|
73
|
+
* the package-guidance block gets the current guidance text. A link to any
|
|
74
|
+
* other file is left alone.
|
|
75
|
+
*
|
|
76
|
+
* Returns the file path when the file changed, and null otherwise.
|
|
77
|
+
*/
|
|
78
|
+
export function writeCodexAgentsGuidance(codexHome, packageGuidanceText, allPackageGuidancePaths) {
|
|
79
|
+
const agentsPath = join(codexHome, 'AGENTS.md');
|
|
80
|
+
const agentsEntry = lstatSync(agentsPath, { throwIfNoEntry: false });
|
|
81
|
+
const isPackageGuidanceLink = agentsEntry?.isSymbolicLink() ?? false;
|
|
82
|
+
if (isPackageGuidanceLink && !linksToPackageGuidance(agentsPath, allPackageGuidancePaths)) return null;
|
|
83
|
+
const currentText = agentsEntry && !isPackageGuidanceLink ? readFileSync(agentsPath, 'utf8') : '';
|
|
84
|
+
let updatedText = withSkillLoadBlock(currentText);
|
|
85
|
+
if (isPackageGuidanceLink || updatedText.includes(PACKAGE_GUIDANCE_BLOCK_START)) {
|
|
86
|
+
updatedText = withPackageGuidanceBlock(updatedText, packageGuidanceText);
|
|
87
|
+
}
|
|
88
|
+
if (!isPackageGuidanceLink && updatedText === currentText) return null;
|
|
89
|
+
mkdirSync(codexHome, { recursive: true });
|
|
90
|
+
if (isPackageGuidanceLink) unlinkSync(agentsPath);
|
|
91
|
+
writeFileSync(agentsPath, updatedText, 'utf8');
|
|
92
|
+
return agentsPath;
|
|
93
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import { strict as assert } from 'node:assert';
|
|
3
|
+
import { lstatSync, mkdtempSync, mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import {
|
|
7
|
+
PACKAGE_GUIDANCE_BLOCK_END,
|
|
8
|
+
PACKAGE_GUIDANCE_BLOCK_START,
|
|
9
|
+
SKILL_LOAD_BLOCK_END,
|
|
10
|
+
SKILL_LOAD_BLOCK_START,
|
|
11
|
+
SKILL_LOAD_INSTRUCTION,
|
|
12
|
+
withSkillLoadBlock,
|
|
13
|
+
writeCodexAgentsGuidance,
|
|
14
|
+
} from './codex-skill-load-block.mjs';
|
|
15
|
+
|
|
16
|
+
const EXPECTED_BLOCK = `${SKILL_LOAD_BLOCK_START}\n${SKILL_LOAD_INSTRUCTION}\n${SKILL_LOAD_BLOCK_END}\n`;
|
|
17
|
+
const PACKAGE_GUIDANCE = 'Status\n\nChanged / proof / blocked.\n';
|
|
18
|
+
|
|
19
|
+
function guidanceBlock(guidanceText) {
|
|
20
|
+
return `${PACKAGE_GUIDANCE_BLOCK_START}\n${guidanceText}${PACKAGE_GUIDANCE_BLOCK_END}\n`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function makeHomes(context) {
|
|
24
|
+
const root = mkdtempSync(join(tmpdir(), 'cde-skill-load-'));
|
|
25
|
+
context.after(() => rmSync(root, { recursive: true, force: true }));
|
|
26
|
+
const codexHome = join(root, '.codex');
|
|
27
|
+
const retiredGuidancePath = join(root, '.claude', 'AGENTS.md');
|
|
28
|
+
const sharedGuidancePath = join(root, '.agents', 'AGENTS.md');
|
|
29
|
+
return {
|
|
30
|
+
root,
|
|
31
|
+
codexHome,
|
|
32
|
+
agentsPath: join(codexHome, 'AGENTS.md'),
|
|
33
|
+
retiredGuidancePath,
|
|
34
|
+
sharedGuidancePath,
|
|
35
|
+
allPackageGuidancePaths: [retiredGuidancePath, sharedGuidancePath],
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function writeGuidance(homes) {
|
|
40
|
+
return writeCodexAgentsGuidance(homes.codexHome, PACKAGE_GUIDANCE, homes.allPackageGuidancePaths);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
test('the instruction names the skill and binds spawned helpers', () => {
|
|
44
|
+
assert.match(SKILL_LOAD_INSTRUCTION, /pstack:poteto-mode/);
|
|
45
|
+
assert.match(SKILL_LOAD_INSTRUCTION, /spawn_agent/);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('a missing Codex guidance file is created holding only the skill-load block', (context) => {
|
|
49
|
+
const homes = makeHomes(context);
|
|
50
|
+
|
|
51
|
+
const writtenPath = writeGuidance(homes);
|
|
52
|
+
|
|
53
|
+
assert.equal(writtenPath, homes.agentsPath);
|
|
54
|
+
assert.equal(readFileSync(homes.agentsPath, 'utf8'), EXPECTED_BLOCK);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('existing guidance keeps every line and gains the skill-load block first', (context) => {
|
|
58
|
+
const homes = makeHomes(context);
|
|
59
|
+
mkdirSync(homes.codexHome);
|
|
60
|
+
writeFileSync(homes.agentsPath, '# pstack model configuration\n\nbug-fix: gpt-6-astra\n');
|
|
61
|
+
|
|
62
|
+
writeGuidance(homes);
|
|
63
|
+
|
|
64
|
+
assert.equal(
|
|
65
|
+
readFileSync(homes.agentsPath, 'utf8'),
|
|
66
|
+
`${EXPECTED_BLOCK}\n# pstack model configuration\n\nbug-fix: gpt-6-astra\n`,
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('a repeat install leaves the file unchanged and reports no write', (context) => {
|
|
71
|
+
const homes = makeHomes(context);
|
|
72
|
+
mkdirSync(homes.codexHome);
|
|
73
|
+
writeFileSync(homes.agentsPath, 'Custom agents\n');
|
|
74
|
+
writeGuidance(homes);
|
|
75
|
+
const afterFirstInstall = readFileSync(homes.agentsPath, 'utf8');
|
|
76
|
+
|
|
77
|
+
assert.equal(writeGuidance(homes), null);
|
|
78
|
+
assert.equal(readFileSync(homes.agentsPath, 'utf8'), afterFirstInstall);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('an outdated block is replaced in place and the text around it is kept', () => {
|
|
82
|
+
const outdated = `Intro\n${SKILL_LOAD_BLOCK_START}\nOld wording\n${SKILL_LOAD_BLOCK_END}\nOutro\n`;
|
|
83
|
+
|
|
84
|
+
assert.equal(withSkillLoadBlock(outdated), `Intro\n${EXPECTED_BLOCK}Outro\n`);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('a start marker with no end marker gets a fresh block and keeps the stray text', () => {
|
|
88
|
+
const broken = `${SKILL_LOAD_BLOCK_START}\nHalf a block\n`;
|
|
89
|
+
|
|
90
|
+
assert.equal(withSkillLoadBlock(broken), `${EXPECTED_BLOCK}\n${broken}`);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
for (const linkName of ['retiredGuidancePath', 'sharedGuidancePath']) {
|
|
94
|
+
test(`a link to package guidance at ${linkName} becomes a file with both blocks`, (context) => {
|
|
95
|
+
const homes = makeHomes(context);
|
|
96
|
+
mkdirSync(homes.codexHome);
|
|
97
|
+
symlinkSync(homes[linkName], homes.agentsPath);
|
|
98
|
+
|
|
99
|
+
const writtenPath = writeGuidance(homes);
|
|
100
|
+
|
|
101
|
+
assert.equal(writtenPath, homes.agentsPath);
|
|
102
|
+
assert.equal(lstatSync(homes.agentsPath).isSymbolicLink(), false);
|
|
103
|
+
assert.equal(
|
|
104
|
+
readFileSync(homes.agentsPath, 'utf8'),
|
|
105
|
+
`${EXPECTED_BLOCK}\n${guidanceBlock(PACKAGE_GUIDANCE)}`,
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
test('a converted file takes the new package guidance on the next install', (context) => {
|
|
111
|
+
const homes = makeHomes(context);
|
|
112
|
+
mkdirSync(homes.codexHome);
|
|
113
|
+
writeFileSync(homes.agentsPath, `${EXPECTED_BLOCK}\n${guidanceBlock('Old guidance\n')}`);
|
|
114
|
+
|
|
115
|
+
writeGuidance(homes);
|
|
116
|
+
|
|
117
|
+
assert.equal(
|
|
118
|
+
readFileSync(homes.agentsPath, 'utf8'),
|
|
119
|
+
`${EXPECTED_BLOCK}\n${guidanceBlock(PACKAGE_GUIDANCE)}`,
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('a link to a file the package does not own is left alone', (context) => {
|
|
124
|
+
const homes = makeHomes(context);
|
|
125
|
+
mkdirSync(homes.codexHome);
|
|
126
|
+
const ownGuidancePath = join(homes.root, 'notes', 'codex.md');
|
|
127
|
+
mkdirSync(join(homes.root, 'notes'));
|
|
128
|
+
writeFileSync(ownGuidancePath, 'My own Codex notes\n');
|
|
129
|
+
symlinkSync(ownGuidancePath, homes.agentsPath);
|
|
130
|
+
|
|
131
|
+
assert.equal(writeGuidance(homes), null);
|
|
132
|
+
assert.equal(lstatSync(homes.agentsPath).isSymbolicLink(), true);
|
|
133
|
+
assert.equal(readFileSync(ownGuidancePath, 'utf8'), 'My own Codex notes\n');
|
|
134
|
+
});
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
writeFileSync,
|
|
19
19
|
} from 'node:fs';
|
|
20
20
|
import { tmpdir } from 'node:os';
|
|
21
|
-
import { dirname, join } from 'node:path';
|
|
21
|
+
import { dirname, join, resolve } from 'node:path';
|
|
22
22
|
import { fileURLToPath } from 'node:url';
|
|
23
23
|
import { CONTENT_DIRECTORIES } from './install.mjs';
|
|
24
24
|
import {
|
|
@@ -209,6 +209,81 @@ test('a full install writes skills and agents under .agents and points .claude a
|
|
|
209
209
|
}
|
|
210
210
|
});
|
|
211
211
|
|
|
212
|
+
/**
|
|
213
|
+
* @param {string} hubPath
|
|
214
|
+
* @returns {string}
|
|
215
|
+
*/
|
|
216
|
+
function resolveHubImportTarget(hubPath) {
|
|
217
|
+
const hubText = readFileSync(hubPath, 'utf8').trim();
|
|
218
|
+
assert.match(hubText, /^@\S+$/, 'the Claude hub holds one import line');
|
|
219
|
+
return resolve(dirname(hubPath), hubText.slice(1));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
for (const eachLayout of [
|
|
223
|
+
{ label: 'default home', extraArguments: [], claudeHomeName: '.claude', agentsHomeName: '.agents' },
|
|
224
|
+
{
|
|
225
|
+
label: 'profile root',
|
|
226
|
+
extraArguments: ['--target', '.claude-profile-a'],
|
|
227
|
+
claudeHomeName: '.claude-profile-a',
|
|
228
|
+
agentsHomeName: '.claude-profile-a.agents',
|
|
229
|
+
},
|
|
230
|
+
]) {
|
|
231
|
+
test(`a ${eachLayout.label} install loads the package guidance from the agents home through the Claude hub`, () => {
|
|
232
|
+
const homeDirectory = mkdtempSync(join(tmpdir(), 'cdev-agents-hub-'));
|
|
233
|
+
const claudeHome = join(homeDirectory, eachLayout.claudeHomeName);
|
|
234
|
+
const sharedGuidancePath = join(homeDirectory, eachLayout.agentsHomeName, 'AGENTS.md');
|
|
235
|
+
try {
|
|
236
|
+
runInstaller(homeDirectory, eachLayout.extraArguments.map(
|
|
237
|
+
(eachArgument) => (eachArgument.startsWith('.') ? join(homeDirectory, eachArgument) : eachArgument),
|
|
238
|
+
));
|
|
239
|
+
|
|
240
|
+
assert.equal(
|
|
241
|
+
readFileSync(sharedGuidancePath, 'utf8'),
|
|
242
|
+
readFileSync(join(PACKAGE_DIRECTORY, 'AGENTS.md'), 'utf8'),
|
|
243
|
+
'the package guidance lives once in the agents home',
|
|
244
|
+
);
|
|
245
|
+
assert.equal(
|
|
246
|
+
resolveHubImportTarget(join(claudeHome, 'CLAUDE.md')),
|
|
247
|
+
sharedGuidancePath,
|
|
248
|
+
'the Claude hub import resolves to the shared guidance file',
|
|
249
|
+
);
|
|
250
|
+
assert.equal(
|
|
251
|
+
existsSync(join(claudeHome, 'AGENTS.md')),
|
|
252
|
+
false,
|
|
253
|
+
'no second guidance copy lands in the Claude home',
|
|
254
|
+
);
|
|
255
|
+
} finally {
|
|
256
|
+
rmSync(homeDirectory, { recursive: true, force: true });
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
test('an upgrade moves the guidance copy an earlier install left in the Claude home into the run backup', () => {
|
|
262
|
+
const homeDirectory = mkdtempSync(join(tmpdir(), 'cdev-agents-hub-upgrade-'));
|
|
263
|
+
const claudeHome = join(homeDirectory, '.claude');
|
|
264
|
+
const retiredGuidancePath = join(claudeHome, 'AGENTS.md');
|
|
265
|
+
const retiredGuidanceText = 'guidance an earlier install copied here\n';
|
|
266
|
+
try {
|
|
267
|
+
runInstaller(homeDirectory, []);
|
|
268
|
+
writeFileSync(retiredGuidancePath, retiredGuidanceText);
|
|
269
|
+
const manifestPath = join(claudeHome, '.claude-dev-env-manifest.json');
|
|
270
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
271
|
+
manifest.files.push(retiredGuidancePath);
|
|
272
|
+
writeFileSync(manifestPath, JSON.stringify(manifest));
|
|
273
|
+
|
|
274
|
+
runInstaller(homeDirectory, []);
|
|
275
|
+
|
|
276
|
+
assert.equal(existsSync(retiredGuidancePath), false, 'the old Claude-home copy is gone');
|
|
277
|
+
const backupRoot = join(claudeHome, '.claude-dev-env-pruned');
|
|
278
|
+
const backupName = readdirSync(backupRoot, { recursive: true })
|
|
279
|
+
.find(name => name.endsWith('AGENTS.md'));
|
|
280
|
+
assert.ok(backupName, 'the old copy sits in the run backup');
|
|
281
|
+
assert.equal(readFileSync(join(backupRoot, backupName), 'utf8'), retiredGuidanceText);
|
|
282
|
+
} finally {
|
|
283
|
+
rmSync(homeDirectory, { recursive: true, force: true });
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
212
287
|
test('installs omit retired agents in each active agents home', () => {
|
|
213
288
|
const runRoot = mkdtempSync(join(tmpdir(), 'cdev-active-roots-'));
|
|
214
289
|
const homeDirectory = join(runRoot, 'home');
|
package/bin/install.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, copyFileSync, unlinkSync, rmSync, rmdirSync, renameSync, realpathSync, lstatSync, constants as filesystemConstants } from 'node:fs';
|
|
4
|
-
import { join, dirname, resolve, relative, basename, isAbsolute, extname } from 'node:path';
|
|
4
|
+
import { join, dirname, resolve, relative, basename, isAbsolute, extname, sep } from 'node:path';
|
|
5
5
|
import { homedir } from 'node:os';
|
|
6
6
|
import { execSync, execFileSync, spawnSync } from 'node:child_process';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
@@ -44,6 +44,8 @@ import {
|
|
|
44
44
|
installPstackPlugin,
|
|
45
45
|
shouldInstallPstackPlugin,
|
|
46
46
|
} from './install-pstack-plugin.mjs';
|
|
47
|
+
import { seedCodexPstackModels } from './seed-codex-pstack-models.mjs';
|
|
48
|
+
import { writeCodexAgentsGuidance } from './codex-skill-load-block.mjs';
|
|
47
49
|
import {
|
|
48
50
|
continuityHostConfigurationPaths,
|
|
49
51
|
removeContinuityHooks,
|
|
@@ -1173,11 +1175,9 @@ export function copyTree(sourceBase, destBase, options = {}) {
|
|
|
1173
1175
|
* @param {string} backupName The managed file name used in the backup path.
|
|
1174
1176
|
* @returns {string|null} The backup path when existing content differs.
|
|
1175
1177
|
*/
|
|
1176
|
-
function backupHubBeforeOverwrite(destPath,
|
|
1178
|
+
function backupHubBeforeOverwrite(destPath, incomingText, backupName) {
|
|
1177
1179
|
if (!existsSync(destPath)) return null;
|
|
1178
|
-
|
|
1179
|
-
const incomingBytes = readFileSync(incomingPath);
|
|
1180
|
-
if (existingBytes.equals(incomingBytes)) return null;
|
|
1180
|
+
if (readFileSync(destPath, 'utf8') === incomingText) return null;
|
|
1181
1181
|
const backupsDir = join(CLAUDE_HOME, 'backups');
|
|
1182
1182
|
mkdirSync(backupsDir, { recursive: true });
|
|
1183
1183
|
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
@@ -2371,6 +2371,15 @@ function pruneStaleFilesAcrossManagedRoots(priorInstalledFiles, currentInstalled
|
|
|
2371
2371
|
skillsPrunedCount += rootOutcome.prunedCount;
|
|
2372
2372
|
}
|
|
2373
2373
|
}
|
|
2374
|
+
const retiredClaudeGuidancePath = join(CLAUDE_HOME, 'AGENTS.md');
|
|
2375
|
+
const retiredGuidanceOutcome = pruneStaleInstalledFiles(
|
|
2376
|
+
priorInstalledFiles?.filter(priorFile => resolve(priorFile) === retiredClaudeGuidancePath) ?? null,
|
|
2377
|
+
currentInstalledFiles,
|
|
2378
|
+
CLAUDE_HOME,
|
|
2379
|
+
backupRoot,
|
|
2380
|
+
);
|
|
2381
|
+
prunedCount += retiredGuidanceOutcome.prunedCount;
|
|
2382
|
+
failedPaths.push(...retiredGuidanceOutcome.failedPaths);
|
|
2374
2383
|
return { prunedCount, skillsPrunedCount, failedPaths };
|
|
2375
2384
|
}
|
|
2376
2385
|
|
|
@@ -2769,6 +2778,25 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
|
|
|
2769
2778
|
if (!selectedGroups && shouldInstallPstackPlugin()) {
|
|
2770
2779
|
const pstackPlugin = installPstackPluginForHosts();
|
|
2771
2780
|
summary.pstackPlugin = pstackPlugin;
|
|
2781
|
+
if (pstackPlugin.hosts.some(host => host.host === 'codex' && host.status === 'installed')) {
|
|
2782
|
+
summary.codexPstackModels = seedCodexPstackModels(
|
|
2783
|
+
INSTALL_ROOT_RESOLUTION.codexHomeDirectory,
|
|
2784
|
+
);
|
|
2785
|
+
if (summary.codexPstackModels) {
|
|
2786
|
+
allInstalledFiles.push(...summary.codexPstackModels);
|
|
2787
|
+
for (const eachPath of summary.codexPstackModels) {
|
|
2788
|
+
allUserOwnedPreferencePaths.add(eachPath);
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
const skillLoadGuidancePath = writeCodexAgentsGuidance(
|
|
2792
|
+
INSTALL_ROOT_RESOLUTION.codexHomeDirectory,
|
|
2793
|
+
readFileSync(join(PACKAGE_ROOT, 'AGENTS.md'), 'utf8'),
|
|
2794
|
+
[join(CLAUDE_HOME, 'AGENTS.md'), join(AGENTS_HOME, 'AGENTS.md')],
|
|
2795
|
+
);
|
|
2796
|
+
if (skillLoadGuidancePath) {
|
|
2797
|
+
console.log(` \u2713 ${skillLoadGuidancePath} (Codex skill-load line)`);
|
|
2798
|
+
}
|
|
2799
|
+
}
|
|
2772
2800
|
for (const eachHost of pstackPlugin.hosts) {
|
|
2773
2801
|
console.log(eachHost.warning
|
|
2774
2802
|
? ` Pstack (${eachHost.host}): ${eachHost.status} \u2014 ${eachHost.warning}`
|
|
@@ -2877,31 +2905,27 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
|
|
|
2877
2905
|
);
|
|
2878
2906
|
}
|
|
2879
2907
|
|
|
2880
|
-
const claudeHubSource = join(PACKAGE_ROOT, '.claude', 'CLAUDE.md');
|
|
2881
|
-
if (existsSync(claudeHubSource)) {
|
|
2882
|
-
const claudeHubDest = join(CLAUDE_HOME, 'CLAUDE.md');
|
|
2883
|
-
const backupPath = backupHubBeforeOverwrite(claudeHubDest, claudeHubSource, 'CLAUDE.md');
|
|
2884
|
-
if (backupPath) {
|
|
2885
|
-
console.log(
|
|
2886
|
-
` \u21bb ${relative(CLAUDE_HOME, backupPath)} (previous CLAUDE.md hub preserved)`
|
|
2887
|
-
);
|
|
2888
|
-
}
|
|
2889
|
-
copyFileSync(claudeHubSource, claudeHubDest);
|
|
2890
|
-
allInstalledFiles.push(claudeHubDest);
|
|
2891
|
-
console.log(` \u2713 ${relative(CLAUDE_HOME, claudeHubDest)} (hub)`);
|
|
2892
|
-
}
|
|
2893
2908
|
const agentsHubSource = join(PACKAGE_ROOT, 'AGENTS.md');
|
|
2894
2909
|
if (existsSync(agentsHubSource)) {
|
|
2895
|
-
const agentsHubDest = join(
|
|
2896
|
-
const
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2910
|
+
const agentsHubDest = join(AGENTS_HOME, 'AGENTS.md');
|
|
2911
|
+
const claudeHubDest = join(CLAUDE_HOME, 'CLAUDE.md');
|
|
2912
|
+
const claudeHubText = `@${relative(CLAUDE_HOME, agentsHubDest).split(sep).join('/')}\n`;
|
|
2913
|
+
const allHubWrites = [
|
|
2914
|
+
{ destPath: agentsHubDest, text: readFileSync(agentsHubSource, 'utf8'), label: 'AGENTS.md', role: 'shared guidance' },
|
|
2915
|
+
{ destPath: claudeHubDest, text: claudeHubText, label: 'CLAUDE.md', role: 'hub' },
|
|
2916
|
+
];
|
|
2917
|
+
mkdirSync(AGENTS_HOME, { recursive: true });
|
|
2918
|
+
for (const eachHubWrite of allHubWrites) {
|
|
2919
|
+
const backupPath = backupHubBeforeOverwrite(eachHubWrite.destPath, eachHubWrite.text, eachHubWrite.label);
|
|
2920
|
+
if (backupPath) {
|
|
2921
|
+
console.log(
|
|
2922
|
+
` \u21bb ${relative(CLAUDE_HOME, backupPath)} (previous ${eachHubWrite.label} preserved)`
|
|
2923
|
+
);
|
|
2924
|
+
}
|
|
2925
|
+
writeFileSync(eachHubWrite.destPath, eachHubWrite.text);
|
|
2926
|
+
allInstalledFiles.push(eachHubWrite.destPath);
|
|
2927
|
+
console.log(` \u2713 ${eachHubWrite.destPath} (${eachHubWrite.role})`);
|
|
2901
2928
|
}
|
|
2902
|
-
copyFileSync(agentsHubSource, agentsHubDest);
|
|
2903
|
-
allInstalledFiles.push(agentsHubDest);
|
|
2904
|
-
console.log(` \u2713 ${relative(CLAUDE_HOME, agentsHubDest)} (canonical guidance)`);
|
|
2905
2929
|
}
|
|
2906
2930
|
const isFullInstall = !selectedGroups;
|
|
2907
2931
|
const didPruneRun = isFullInstall && UNRESOLVED_DEPENDENCY_NAMES.length === 0;
|
|
@@ -3322,10 +3346,10 @@ Profile selection (--profile/--profiles) is mutually exclusive with --target.
|
|
|
3322
3346
|
Target selection finishes before any mutation. Each target writes its own manifest
|
|
3323
3347
|
with package, version, targetIdentity, managedRoot, files, and skills.
|
|
3324
3348
|
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3349
|
+
The package guidance lands once at ~/.agents/AGENTS.md, and ~/.claude/CLAUDE.md
|
|
3350
|
+
holds one import line that loads it. If either file already exists with other
|
|
3351
|
+
contents, the installer writes the previous contents to
|
|
3352
|
+
~/.claude/backups/<name>.<timestamp>.bak first.
|
|
3329
3353
|
`);
|
|
3330
3354
|
}
|
|
3331
3355
|
|
package/bin/install.test.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
readdirSync,
|
|
12
12
|
existsSync,
|
|
13
13
|
copyFileSync,
|
|
14
|
+
lstatSync,
|
|
14
15
|
} from 'node:fs';
|
|
15
16
|
import { tmpdir } from 'node:os';
|
|
16
17
|
import { join, dirname } from 'node:path';
|
|
@@ -52,6 +53,11 @@ import {
|
|
|
52
53
|
retainNewestRunBackupOnly,
|
|
53
54
|
} from './install.mjs';
|
|
54
55
|
import { EVER_SHIPPED_SKILL_NAMES } from './ever-shipped-skills.mjs';
|
|
56
|
+
import {
|
|
57
|
+
SKILL_LOAD_BLOCK_START,
|
|
58
|
+
SKILL_LOAD_INSTRUCTION,
|
|
59
|
+
withSkillLoadBlock,
|
|
60
|
+
} from './codex-skill-load-block.mjs';
|
|
55
61
|
|
|
56
62
|
|
|
57
63
|
test('installer reports the authoritative Git hook names', () => {
|
|
@@ -3064,6 +3070,53 @@ test('a full install adds the pstack marketplace and plugin on both hosts', t =>
|
|
|
3064
3070
|
]);
|
|
3065
3071
|
assert.match(installerOutput, /Pstack \(claude\): installed/);
|
|
3066
3072
|
assert.match(installerOutput, /Pstack \(codex\): installed/);
|
|
3073
|
+
const codexHome = join(sandbox.homeDirectory, '.codex');
|
|
3074
|
+
const sheet = readFileSync(join(codexHome, 'pstack-models.md'), 'utf8');
|
|
3075
|
+
const agents = readFileSync(join(codexHome, 'AGENTS.md'), 'utf8');
|
|
3076
|
+
assert.match(sheet, /^session hook: on$/m);
|
|
3077
|
+
assert.match(agents, /^feature, refactoring: gpt-6-sol$/m);
|
|
3078
|
+
assert.doesNotMatch(agents, /^session hook:/m);
|
|
3079
|
+
assert.ok(agents.startsWith(`${SKILL_LOAD_BLOCK_START}\n${SKILL_LOAD_INSTRUCTION}\n`));
|
|
3080
|
+
assert.equal(existsSync(join(sandbox.homeDirectory, '.claude', 'pstack-models.md')), false);
|
|
3081
|
+
const manifestPath = join(sandbox.homeDirectory, '.claude', '.claude-dev-env-manifest.json');
|
|
3082
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
3083
|
+
assert.equal(manifest.files.includes(join(codexHome, 'pstack-models.md')), false);
|
|
3084
|
+
assert.equal(manifest.files.includes(join(codexHome, 'AGENTS.md')), false);
|
|
3085
|
+
});
|
|
3086
|
+
|
|
3087
|
+
test('a repeat install preserves Codex pstack configuration', t => {
|
|
3088
|
+
const sandbox = pstackPluginSandbox(t);
|
|
3089
|
+
const codexHome = join(sandbox.homeDirectory, '.codex');
|
|
3090
|
+
runPstackInstaller(sandbox.homeDirectory, [], sandbox.environment);
|
|
3091
|
+
writeFileSync(join(codexHome, 'AGENTS.md'), 'Custom Codex guidance\n');
|
|
3092
|
+
writeFileSync(join(codexHome, 'pstack-models.md'), 'session hook: off\n');
|
|
3093
|
+
|
|
3094
|
+
runPstackInstaller(sandbox.homeDirectory, [], sandbox.environment);
|
|
3095
|
+
|
|
3096
|
+
assert.equal(
|
|
3097
|
+
readFileSync(join(codexHome, 'AGENTS.md'), 'utf8'),
|
|
3098
|
+
withSkillLoadBlock('Custom Codex guidance\n'),
|
|
3099
|
+
);
|
|
3100
|
+
assert.equal(readFileSync(join(codexHome, 'pstack-models.md'), 'utf8'), 'session hook: off\n');
|
|
3101
|
+
});
|
|
3102
|
+
|
|
3103
|
+
test('a Codex guidance link to the retired Claude copy becomes a file Codex can read', t => {
|
|
3104
|
+
const sandbox = pstackPluginSandbox(t);
|
|
3105
|
+
const codexHome = join(sandbox.homeDirectory, '.codex');
|
|
3106
|
+
const codexAgentsPath = join(codexHome, 'AGENTS.md');
|
|
3107
|
+
mkdirSync(codexHome, { recursive: true });
|
|
3108
|
+
symlinkSync(join(sandbox.homeDirectory, '.claude', 'AGENTS.md'), codexAgentsPath);
|
|
3109
|
+
|
|
3110
|
+
runPstackInstaller(sandbox.homeDirectory, [], sandbox.environment);
|
|
3111
|
+
|
|
3112
|
+
const packageGuidance = readFileSync(
|
|
3113
|
+
join(dirname(fileURLToPath(import.meta.url)), '..', 'AGENTS.md'),
|
|
3114
|
+
'utf8',
|
|
3115
|
+
);
|
|
3116
|
+
assert.equal(lstatSync(codexAgentsPath).isSymbolicLink(), false);
|
|
3117
|
+
const codexGuidance = readFileSync(codexAgentsPath, 'utf8');
|
|
3118
|
+
assert.ok(codexGuidance.startsWith(`${SKILL_LOAD_BLOCK_START}\n${SKILL_LOAD_INSTRUCTION}\n`));
|
|
3119
|
+
assert.ok(codexGuidance.includes(packageGuidance));
|
|
3067
3120
|
});
|
|
3068
3121
|
|
|
3069
3122
|
test('an absent host command is reported and leaves the other host installed', t => {
|
|
@@ -3085,6 +3138,8 @@ test('an absent host command is reported and leaves the other host installed', t
|
|
|
3085
3138
|
sandbox.recordedCommands().filter(eachCommand => eachCommand.startsWith('codex')),
|
|
3086
3139
|
[],
|
|
3087
3140
|
);
|
|
3141
|
+
assert.equal(existsSync(join(sandbox.homeDirectory, '.codex', 'pstack-models.md')), false);
|
|
3142
|
+
assert.equal(existsSync(join(sandbox.homeDirectory, '.codex', 'AGENTS.md')), false);
|
|
3088
3143
|
});
|
|
3089
3144
|
|
|
3090
3145
|
test('--no-pstack leaves both hosts untouched', t => {
|
|
@@ -3096,6 +3151,8 @@ test('--no-pstack leaves both hosts untouched', t => {
|
|
|
3096
3151
|
|
|
3097
3152
|
assert.deepEqual(sandbox.recordedCommands(), []);
|
|
3098
3153
|
assert.doesNotMatch(installerOutput, /Pstack \(/);
|
|
3154
|
+
assert.equal(existsSync(join(sandbox.homeDirectory, '.codex', 'pstack-models.md')), false);
|
|
3155
|
+
assert.equal(existsSync(join(sandbox.homeDirectory, '.codex', 'AGENTS.md')), false);
|
|
3099
3156
|
});
|
|
3100
3157
|
|
|
3101
3158
|
function continuityCommandCount(configurationPath) {
|
|
@@ -270,13 +270,6 @@ test('runWithInstallTransaction commits and discards journal on success', () =>
|
|
|
270
270
|
rmSync(box.root, { recursive: true, force: true });
|
|
271
271
|
});
|
|
272
272
|
|
|
273
|
-
/**
|
|
274
|
-
* Spawn the real installer against a sandbox home with optional fault phase.
|
|
275
|
-
*
|
|
276
|
-
* @param {string} homeDirectory
|
|
277
|
-
* @param {string[]} extraArguments
|
|
278
|
-
* @param {{ faultPhase?: string|null }} [options]
|
|
279
|
-
*/
|
|
280
273
|
function runInstaller(homeDirectory, extraArguments, options = {}) {
|
|
281
274
|
const childEnvironment = {
|
|
282
275
|
...process.env,
|
|
@@ -285,6 +278,7 @@ function runInstaller(homeDirectory, extraArguments, options = {}) {
|
|
|
285
278
|
USERPROFILE: homeDirectory,
|
|
286
279
|
GIT_CONFIG_GLOBAL: join(homeDirectory, '.gitconfig'),
|
|
287
280
|
CODEX_HOME: join(homeDirectory, '.codex'),
|
|
281
|
+
...options.environment,
|
|
288
282
|
};
|
|
289
283
|
if (options.faultPhase) {
|
|
290
284
|
childEnvironment[INSTALL_FAULT_ENV] = options.faultPhase;
|
|
@@ -298,6 +292,34 @@ function runInstaller(homeDirectory, extraArguments, options = {}) {
|
|
|
298
292
|
});
|
|
299
293
|
}
|
|
300
294
|
|
|
295
|
+
test('a later install fault removes newly seeded Codex pstack files', () => {
|
|
296
|
+
const homeDirectory = mkdtempSync(join(tmpdir(), 'cdev-txn-codex-pstack-'));
|
|
297
|
+
try {
|
|
298
|
+
const codexCommandPath = join(
|
|
299
|
+
homeDirectory,
|
|
300
|
+
process.platform === 'win32' ? 'codex.cmd' : 'codex',
|
|
301
|
+
);
|
|
302
|
+
writeFileSync(
|
|
303
|
+
codexCommandPath,
|
|
304
|
+
process.platform === 'win32' ? '@echo off\r\nexit /b 0\r\n' : '#!/bin/sh\nexit 0\n',
|
|
305
|
+
{ mode: 0o755 },
|
|
306
|
+
);
|
|
307
|
+
const failedRun = runInstaller(homeDirectory, [], {
|
|
308
|
+
faultPhase: FAULT_PHASES.AFTER_MANIFEST_WRITE,
|
|
309
|
+
environment: {
|
|
310
|
+
CDE_INSTALL_PSTACK: '1',
|
|
311
|
+
CDE_CODEX_EXECUTABLE: codexCommandPath,
|
|
312
|
+
},
|
|
313
|
+
});
|
|
314
|
+
assert.notEqual(failedRun.status, 0, failedRun.stdout + failedRun.stderr);
|
|
315
|
+
assert.match(`${failedRun.stdout}${failedRun.stderr}`, /after_manifest_write/);
|
|
316
|
+
assert.equal(existsSync(join(homeDirectory, '.codex', 'pstack-models.md')), false);
|
|
317
|
+
assert.equal(existsSync(join(homeDirectory, '.codex', 'AGENTS.md')), false);
|
|
318
|
+
} finally {
|
|
319
|
+
rmSync(homeDirectory, { recursive: true, force: true });
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
|
|
301
323
|
function seedPriorInstall(homeDirectory) {
|
|
302
324
|
const claudeDirectory = join(homeDirectory, '.claude');
|
|
303
325
|
const rulesFile = join(claudeDirectory, 'rules', 'prior.md');
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { copyFileSync, lstatSync, mkdirSync, readFileSync, unlinkSync, writeFileSync, constants as filesystemConstants } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
const CODEX_PRESET_PATH = fileURLToPath(new URL('../pstack-codex-models.md', import.meta.url));
|
|
6
|
+
|
|
7
|
+
function pathExists(path) {
|
|
8
|
+
return lstatSync(path, { throwIfNoEntry: false }) !== undefined;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function seedCodexPstackModels(codexHome, writeAgentGuidance = writeFileSync) {
|
|
12
|
+
const sheetPath = join(codexHome, 'pstack-models.md');
|
|
13
|
+
const agentsPath = join(codexHome, 'AGENTS.md');
|
|
14
|
+
if (pathExists(sheetPath) || pathExists(agentsPath)) return null;
|
|
15
|
+
|
|
16
|
+
const preset = readFileSync(CODEX_PRESET_PATH, 'utf8');
|
|
17
|
+
const agentGuidance = preset.replace(/^session hook: on\n?$/m, '');
|
|
18
|
+
mkdirSync(codexHome, { recursive: true });
|
|
19
|
+
copyFileSync(CODEX_PRESET_PATH, sheetPath, filesystemConstants.COPYFILE_EXCL);
|
|
20
|
+
try {
|
|
21
|
+
writeAgentGuidance(agentsPath, agentGuidance, { encoding: 'utf8', flag: 'wx' });
|
|
22
|
+
} catch (writeError) {
|
|
23
|
+
unlinkSync(sheetPath);
|
|
24
|
+
throw writeError;
|
|
25
|
+
}
|
|
26
|
+
return [sheetPath, agentsPath];
|
|
27
|
+
}
|