claude-dev-env 8.31.1 → 8.32.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/.agents/skills/pull-request/SKILL.md +4 -3
- package/.agents/skills/pull-request/reference/publication-tasks.md +1 -1
- package/.agents/skills/pull-request/scripts/pull_request.py +5 -2
- package/.agents/skills/pull-request/scripts/test_pull_request.py +32 -0
- package/bin/codex-skill-load-block.mjs +87 -34
- package/bin/codex-skill-load-block.test.mjs +112 -4
- package/bin/install.mjs +23 -9
- package/bin/install.question-presentation.test.mjs +2 -2
- package/bin/install.test.mjs +105 -0
- package/bin/seed-codex-pstack-models.mjs +2 -2
- package/bin/seed-codex-pstack-models.test.mjs +52 -1
- package/commands/fix-followups.md +3 -3
- package/commands/sr-loop.md +4 -3
- package/hooks/advisory/test_pr_done_reminder.py +2 -1
- package/hooks/blocking/block_main_commit.py +2 -10
- package/hooks/blocking/test_block_main_commit.py +13 -0
- package/hooks/hooks_constants/block_main_commit_constants.py +9 -0
- package/hooks/hooks_constants/pr_done_reminder_constants.py +1 -1
- package/hooks/pyproject.toml +0 -1
- package/hooks/validators/README.md +0 -1
- package/hooks/validators/health_check.py +0 -1
- package/hooks/validators/run_all_validators.py +0 -13
- package/hooks/validators/test_run_all_validators.py +5 -36
- package/package.json +1 -1
- package/rules/agent-merges-its-own-green-pull-request.md +1 -1
- package/rules/flag-non-breaking-findings.md +1 -1
- package/rules/git-workflow.md +2 -7
- package/rules/review-closure-is-a-check.md +1 -1
- package/scripts/dev_env_scripts_constants/followup_constants.py +1 -1
- package/scripts/test_followup_cli.py +2 -1
- package/hooks/validators/git_checks.py +0 -110
- package/hooks/validators/test_git_checks.py +0 -112
|
@@ -32,7 +32,7 @@ process-local author value. Keep the parent environment unchanged.
|
|
|
32
32
|
|
|
33
33
|
Use this skill for one pull request action:
|
|
34
34
|
|
|
35
|
-
- Create a draft
|
|
35
|
+
- Create a pull request, ready for review by default or a draft with `--draft`.
|
|
36
36
|
- Edit a pull request title or body.
|
|
37
37
|
- Add a pull request comment.
|
|
38
38
|
- Submit a pull request review.
|
|
@@ -91,7 +91,8 @@ Process steps in order.
|
|
|
91
91
|
|
|
92
92
|
Resolve the repository, action, and selected GitHub author. For create, resolve
|
|
93
93
|
the source branch, base, and head. For other actions, resolve one existing pull
|
|
94
|
-
request target. Create
|
|
94
|
+
request target. Create publishes a ready pull request, and `--draft` publishes a
|
|
95
|
+
draft. Record the target before any write.
|
|
95
96
|
|
|
96
97
|
### 2. Author the title and body
|
|
97
98
|
|
|
@@ -185,7 +186,7 @@ only after remote readback proves the requested state.
|
|
|
185
186
|
## Examples
|
|
186
187
|
|
|
187
188
|
Create example: the writer produces `pr-body.md`. The linter exits `0` for
|
|
188
|
-
`pr-create`. `pull_request.py create` publishes one
|
|
189
|
+
`pr-create`. `pull_request.py create` publishes one ready pull request. The
|
|
189
190
|
readback matches the title, body, head SHA, and draft state.
|
|
190
191
|
|
|
191
192
|
Rejected comment example: the comment body names a worktree file. The linter
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
When the session exposes a task tool, register each line as one session task
|
|
4
4
|
before starting the process. This file is a task seed catalog.
|
|
5
5
|
|
|
6
|
-
1. Resolve one repository, action, and selected author. For create, resolve the source branch, base, and head. For other actions, resolve one existing pull request target. Create
|
|
6
|
+
1. Resolve one repository, action, and selected author. For create, resolve the source branch, base, and head. For other actions, resolve one existing pull request target. Create publishes a ready pull request, and `--draft` publishes a draft.
|
|
7
7
|
2. Record the title and body file for create or full body rewrite, written by you or drafted by `pstack:poteto-agent` with a PR-writing brief. Mark `N/A` for comment or review bodies that do not rewrite the pull request description.
|
|
8
8
|
3. Validate every title, body, artifact, and local path input. Reject inline body input.
|
|
9
9
|
4. Resolve the active managed root and run `<managed-root>/scripts/durable_post_lint.py` for the matching action. Record exit code `0` before credential lookup or network work.
|
|
@@ -43,6 +43,7 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
43
43
|
create.add_argument("--head", required=True)
|
|
44
44
|
create.add_argument("--title", required=True)
|
|
45
45
|
create.add_argument("--body-file", required=True, type=Path)
|
|
46
|
+
create.add_argument("--draft", dest="is_draft", action="store_true")
|
|
46
47
|
edit = all_actions.add_parser(ACTION_EDIT, allow_abbrev=False)
|
|
47
48
|
edit.add_argument("--repo", required=True)
|
|
48
49
|
edit.add_argument("--number", required=True)
|
|
@@ -105,11 +106,10 @@ def _linter_arguments(
|
|
|
105
106
|
|
|
106
107
|
|
|
107
108
|
def _create_arguments(arguments: argparse.Namespace) -> list[str]:
|
|
108
|
-
|
|
109
|
+
all_arguments = [
|
|
109
110
|
"gh",
|
|
110
111
|
"pr",
|
|
111
112
|
"create",
|
|
112
|
-
"--draft",
|
|
113
113
|
"--repo",
|
|
114
114
|
arguments.repo,
|
|
115
115
|
"--base",
|
|
@@ -121,6 +121,9 @@ def _create_arguments(arguments: argparse.Namespace) -> list[str]:
|
|
|
121
121
|
"--body-file",
|
|
122
122
|
str(arguments.body_file),
|
|
123
123
|
]
|
|
124
|
+
if arguments.is_draft:
|
|
125
|
+
all_arguments.append("--draft")
|
|
126
|
+
return all_arguments
|
|
124
127
|
|
|
125
128
|
|
|
126
129
|
def _edit_arguments(arguments: argparse.Namespace) -> list[str]:
|
|
@@ -47,7 +47,38 @@ ACTION_CASES = [
|
|
|
47
47
|
"gh",
|
|
48
48
|
"pr",
|
|
49
49
|
"create",
|
|
50
|
+
"--repo",
|
|
51
|
+
REPOSITORY,
|
|
52
|
+
"--base",
|
|
53
|
+
"main",
|
|
54
|
+
"--head",
|
|
55
|
+
"feature/post",
|
|
56
|
+
"--title",
|
|
57
|
+
PR_TITLE,
|
|
58
|
+
"--body-file",
|
|
59
|
+
BODY_FILENAME,
|
|
60
|
+
],
|
|
61
|
+
),
|
|
62
|
+
(
|
|
63
|
+
[
|
|
64
|
+
"create",
|
|
65
|
+
"--repo",
|
|
66
|
+
REPOSITORY,
|
|
67
|
+
"--base",
|
|
68
|
+
"main",
|
|
69
|
+
"--head",
|
|
70
|
+
"feature/post",
|
|
71
|
+
"--title",
|
|
72
|
+
PR_TITLE,
|
|
73
|
+
"--body-file",
|
|
74
|
+
BODY_FILENAME,
|
|
50
75
|
"--draft",
|
|
76
|
+
],
|
|
77
|
+
"pr-create",
|
|
78
|
+
[
|
|
79
|
+
"gh",
|
|
80
|
+
"pr",
|
|
81
|
+
"create",
|
|
51
82
|
"--repo",
|
|
52
83
|
REPOSITORY,
|
|
53
84
|
"--base",
|
|
@@ -58,6 +89,7 @@ ACTION_CASES = [
|
|
|
58
89
|
PR_TITLE,
|
|
59
90
|
"--body-file",
|
|
60
91
|
BODY_FILENAME,
|
|
92
|
+
"--draft",
|
|
61
93
|
],
|
|
62
94
|
),
|
|
63
95
|
(
|
|
@@ -50,7 +50,8 @@ function withPackageGuidanceBlock(guidanceText, packageGuidanceText) {
|
|
|
50
50
|
PACKAGE_GUIDANCE_BLOCK_END,
|
|
51
51
|
block,
|
|
52
52
|
);
|
|
53
|
-
|
|
53
|
+
if (replacedText !== null) return replacedText;
|
|
54
|
+
return guidanceText ? `${guidanceText}\n${block}` : block;
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
function comparablePath(filePath) {
|
|
@@ -66,28 +67,14 @@ function linksToPackageGuidance(agentsPath, allPackageGuidancePaths) {
|
|
|
66
67
|
return allPackageGuidancePaths.some(eachPath => comparablePath(eachPath) === linkTarget);
|
|
67
68
|
}
|
|
68
69
|
|
|
69
|
-
|
|
70
|
-
* Keep CODEX_HOME/AGENTS.md carrying the skill-load block.
|
|
71
|
-
*
|
|
72
|
-
* A symbolic link that points at the package guidance (the shared agents-home
|
|
73
|
-
* copy, or the retired copy under the Claude home) is replaced by a regular
|
|
74
|
-
* file holding the skill-load block and a package-guidance block, so Codex
|
|
75
|
-
* keeps both after the package moves its guidance. A file that already holds
|
|
76
|
-
* the package-guidance block gets the current guidance text. A link to any
|
|
77
|
-
* other file is left alone.
|
|
78
|
-
*
|
|
79
|
-
* Returns the file path when the file changed, and null otherwise.
|
|
80
|
-
*/
|
|
81
|
-
export function writeCodexAgentsGuidance(codexHome, packageGuidanceText, allPackageGuidancePaths) {
|
|
70
|
+
function writeCodexGuidanceFile(codexHome, allPackageGuidancePaths, transformGuidance) {
|
|
82
71
|
const agentsPath = join(codexHome, 'AGENTS.md');
|
|
83
72
|
const agentsEntry = lstatSync(agentsPath, { throwIfNoEntry: false });
|
|
73
|
+
if (agentsEntry && !agentsEntry.isFile() && !agentsEntry.isSymbolicLink()) return null;
|
|
84
74
|
const isPackageGuidanceLink = agentsEntry?.isSymbolicLink() ?? false;
|
|
85
75
|
if (isPackageGuidanceLink && !linksToPackageGuidance(agentsPath, allPackageGuidancePaths)) return null;
|
|
86
76
|
const currentText = agentsEntry && !isPackageGuidanceLink ? readFileSync(agentsPath, 'utf8') : '';
|
|
87
|
-
|
|
88
|
-
if (isPackageGuidanceLink || updatedText.includes(PACKAGE_GUIDANCE_BLOCK_START)) {
|
|
89
|
-
updatedText = withPackageGuidanceBlock(updatedText, packageGuidanceText);
|
|
90
|
-
}
|
|
77
|
+
const updatedText = transformGuidance(currentText);
|
|
91
78
|
if (!isPackageGuidanceLink && updatedText === currentText) return null;
|
|
92
79
|
mkdirSync(codexHome, { recursive: true });
|
|
93
80
|
if (isPackageGuidanceLink) unlinkSync(agentsPath);
|
|
@@ -95,6 +82,39 @@ export function writeCodexAgentsGuidance(codexHome, packageGuidanceText, allPack
|
|
|
95
82
|
return agentsPath;
|
|
96
83
|
}
|
|
97
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Keep CODEX_HOME/AGENTS.md carrying the skill-load block and the package guidance.
|
|
87
|
+
*
|
|
88
|
+
* The skill-load block goes first, and the package-guidance block holds the
|
|
89
|
+
* current guidance text. A symbolic link that points at the package guidance
|
|
90
|
+
* (the shared agents-home copy, or the retired copy under the Claude home) is
|
|
91
|
+
* replaced by a regular file holding both blocks, so Codex keeps both after
|
|
92
|
+
* the package moves its guidance. A link to any other file is left alone.
|
|
93
|
+
*
|
|
94
|
+
* Returns the file path when the file changed, and null otherwise.
|
|
95
|
+
*/
|
|
96
|
+
export function writeCodexAgentsGuidance(codexHome, packageGuidanceText, allPackageGuidancePaths) {
|
|
97
|
+
return writeCodexGuidanceFile(
|
|
98
|
+
codexHome,
|
|
99
|
+
allPackageGuidancePaths,
|
|
100
|
+
currentText => withPackageGuidanceBlock(withSkillLoadBlock(currentText), packageGuidanceText),
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Keep CODEX_HOME/AGENTS.md carrying the package guidance, with no skill-load block.
|
|
106
|
+
*
|
|
107
|
+
* Links follow the same rule as writeCodexAgentsGuidance. Returns the file
|
|
108
|
+
* path when the file changed, and null otherwise.
|
|
109
|
+
*/
|
|
110
|
+
export function writeCodexPackageGuidance(codexHome, packageGuidanceText, allPackageGuidancePaths) {
|
|
111
|
+
return writeCodexGuidanceFile(
|
|
112
|
+
codexHome,
|
|
113
|
+
allPackageGuidancePaths,
|
|
114
|
+
currentText => withPackageGuidanceBlock(currentText, packageGuidanceText),
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
98
118
|
export function writeCodexQuestionGuidance(codexHome, policyText) {
|
|
99
119
|
const agentsPath = join(codexHome, 'AGENTS.md');
|
|
100
120
|
const agentsEntry = lstatSync(agentsPath, { throwIfNoEntry: false });
|
|
@@ -112,30 +132,63 @@ export function writeCodexQuestionGuidance(codexHome, policyText) {
|
|
|
112
132
|
return agentsPath;
|
|
113
133
|
}
|
|
114
134
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
135
|
+
const ALL_PACKAGE_MANAGED_BLOCK_MARKERS = [
|
|
136
|
+
[SKILL_LOAD_BLOCK_START, SKILL_LOAD_BLOCK_END],
|
|
137
|
+
[PACKAGE_GUIDANCE_BLOCK_START, PACKAGE_GUIDANCE_BLOCK_END],
|
|
138
|
+
[QUESTION_PRESENTATION_BLOCK_START, QUESTION_PRESENTATION_BLOCK_END],
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Report whether the guidance holds at least one package-managed block and
|
|
143
|
+
* nothing else but blank lines.
|
|
144
|
+
*/
|
|
145
|
+
export function hasOnlyPackageManagedBlocks(guidanceText) {
|
|
146
|
+
let remainingText = guidanceText;
|
|
147
|
+
let removedBlockCount = 0;
|
|
148
|
+
for (const [blockStart, blockEnd] of ALL_PACKAGE_MANAGED_BLOCK_MARKERS) {
|
|
149
|
+
const withoutBlock = withBlockReplaced(remainingText, blockStart, blockEnd, '');
|
|
150
|
+
if (withoutBlock === null) continue;
|
|
151
|
+
remainingText = withoutBlock;
|
|
152
|
+
removedBlockCount += 1;
|
|
153
|
+
}
|
|
154
|
+
return removedBlockCount > 0 && remainingText.trim() === '';
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function removeCodexGuidanceBlock(codexHome, blockStart, blockEnd) {
|
|
158
|
+
const agentsPath = join(codexHome, 'AGENTS.md');
|
|
159
|
+
const agentsEntry = lstatSync(agentsPath, { throwIfNoEntry: false });
|
|
160
|
+
if (!agentsEntry?.isFile()) return null;
|
|
161
|
+
const currentText = readFileSync(agentsPath, 'utf8');
|
|
162
|
+
const updatedText = withBlockReplaced(currentText, blockStart, blockEnd, '');
|
|
163
|
+
if (updatedText === null) return null;
|
|
164
|
+
const isLeftEmpty = updatedText.trim() === '';
|
|
165
|
+
if (isLeftEmpty) unlinkSync(agentsPath);
|
|
166
|
+
if (!isLeftEmpty) writeFileSync(agentsPath, updatedText, 'utf8');
|
|
167
|
+
return agentsPath;
|
|
121
168
|
}
|
|
122
169
|
|
|
123
170
|
/**
|
|
124
171
|
* Remove the managed question block while keeping surrounding guidance bytes.
|
|
125
172
|
*
|
|
173
|
+
* A file left holding only blank lines is deleted.
|
|
174
|
+
*
|
|
126
175
|
* @param {string} codexHome
|
|
127
176
|
* @returns {string|null} The changed path, or null when no block was removed.
|
|
128
177
|
*/
|
|
129
178
|
export function removeCodexQuestionGuidance(codexHome) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (!agentsEntry?.isFile()) return null;
|
|
133
|
-
const currentText = readFileSync(agentsPath, 'utf8');
|
|
134
|
-
const updatedText = withBlockReplaced(
|
|
135
|
-
currentText, QUESTION_PRESENTATION_BLOCK_START, QUESTION_PRESENTATION_BLOCK_END, '',
|
|
179
|
+
return removeCodexGuidanceBlock(
|
|
180
|
+
codexHome, QUESTION_PRESENTATION_BLOCK_START, QUESTION_PRESENTATION_BLOCK_END,
|
|
136
181
|
);
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Remove the managed package-guidance block while keeping surrounding guidance bytes.
|
|
186
|
+
*
|
|
187
|
+
* A file left holding only blank lines is deleted.
|
|
188
|
+
*
|
|
189
|
+
* @param {string} codexHome
|
|
190
|
+
* @returns {string|null} The changed path, or null when no block was removed.
|
|
191
|
+
*/
|
|
192
|
+
export function removeCodexPackageGuidance(codexHome) {
|
|
193
|
+
return removeCodexGuidanceBlock(codexHome, PACKAGE_GUIDANCE_BLOCK_START, PACKAGE_GUIDANCE_BLOCK_END);
|
|
141
194
|
}
|
|
@@ -11,9 +11,11 @@ import {
|
|
|
11
11
|
SKILL_LOAD_BLOCK_END,
|
|
12
12
|
SKILL_LOAD_BLOCK_START,
|
|
13
13
|
SKILL_LOAD_INSTRUCTION,
|
|
14
|
+
removeCodexPackageGuidance,
|
|
14
15
|
removeCodexQuestionGuidance,
|
|
15
16
|
withSkillLoadBlock,
|
|
16
17
|
writeCodexAgentsGuidance,
|
|
18
|
+
writeCodexPackageGuidance,
|
|
17
19
|
writeCodexQuestionGuidance,
|
|
18
20
|
} from './codex-skill-load-block.mjs';
|
|
19
21
|
|
|
@@ -50,16 +52,19 @@ test('the instruction names the skill and binds spawned helpers', () => {
|
|
|
50
52
|
assert.match(SKILL_LOAD_INSTRUCTION, /spawn_agent/);
|
|
51
53
|
});
|
|
52
54
|
|
|
53
|
-
test('a missing Codex guidance file is created holding
|
|
55
|
+
test('a missing Codex guidance file is created holding the skill-load block and the package guidance', (context) => {
|
|
54
56
|
const homes = makeHomes(context);
|
|
55
57
|
|
|
56
58
|
const writtenPath = writeGuidance(homes);
|
|
57
59
|
|
|
58
60
|
assert.equal(writtenPath, homes.agentsPath);
|
|
59
|
-
assert.equal(
|
|
61
|
+
assert.equal(
|
|
62
|
+
readFileSync(homes.agentsPath, 'utf8'),
|
|
63
|
+
`${EXPECTED_BLOCK}\n${guidanceBlock(PACKAGE_GUIDANCE)}`,
|
|
64
|
+
);
|
|
60
65
|
});
|
|
61
66
|
|
|
62
|
-
test('existing guidance keeps every line
|
|
67
|
+
test('existing guidance keeps every line between the skill-load block and the package guidance', (context) => {
|
|
63
68
|
const homes = makeHomes(context);
|
|
64
69
|
mkdirSync(homes.codexHome);
|
|
65
70
|
writeFileSync(homes.agentsPath, '# pstack model configuration\n\nbug-fix: gpt-6-astra\n');
|
|
@@ -68,7 +73,7 @@ test('existing guidance keeps every line and gains the skill-load block first',
|
|
|
68
73
|
|
|
69
74
|
assert.equal(
|
|
70
75
|
readFileSync(homes.agentsPath, 'utf8'),
|
|
71
|
-
`${EXPECTED_BLOCK}\n# pstack model configuration\n\nbug-fix: gpt-6-astra\n`,
|
|
76
|
+
`${EXPECTED_BLOCK}\n# pstack model configuration\n\nbug-fix: gpt-6-astra\n\n${guidanceBlock(PACKAGE_GUIDANCE)}`,
|
|
72
77
|
);
|
|
73
78
|
});
|
|
74
79
|
|
|
@@ -138,6 +143,78 @@ test('a link to a file the package does not own is left alone', (context) => {
|
|
|
138
143
|
assert.equal(readFileSync(ownGuidancePath, 'utf8'), 'My own Codex notes\n');
|
|
139
144
|
});
|
|
140
145
|
|
|
146
|
+
function writePackageGuidance(homes) {
|
|
147
|
+
return writeCodexPackageGuidance(homes.codexHome, PACKAGE_GUIDANCE, homes.allPackageGuidancePaths);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
test('package guidance creates a missing Codex file holding only the package block', (context) => {
|
|
151
|
+
const homes = makeHomes(context);
|
|
152
|
+
|
|
153
|
+
assert.equal(writePackageGuidance(homes), homes.agentsPath);
|
|
154
|
+
assert.equal(readFileSync(homes.agentsPath, 'utf8'), guidanceBlock(PACKAGE_GUIDANCE));
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('package guidance replaces its block in place and a repeat write changes nothing', (context) => {
|
|
158
|
+
const homes = makeHomes(context);
|
|
159
|
+
mkdirSync(homes.codexHome);
|
|
160
|
+
writeFileSync(homes.agentsPath, `Before\n${guidanceBlock('Old guidance\n')}After\n`);
|
|
161
|
+
|
|
162
|
+
assert.equal(writePackageGuidance(homes), homes.agentsPath);
|
|
163
|
+
const expectedGuidance = `Before\n${guidanceBlock(PACKAGE_GUIDANCE)}After\n`;
|
|
164
|
+
assert.equal(readFileSync(homes.agentsPath, 'utf8'), expectedGuidance);
|
|
165
|
+
assert.equal(writePackageGuidance(homes), null);
|
|
166
|
+
assert.equal(readFileSync(homes.agentsPath, 'utf8'), expectedGuidance);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('package guidance appends its block after custom notes', (context) => {
|
|
170
|
+
const homes = makeHomes(context);
|
|
171
|
+
mkdirSync(homes.codexHome);
|
|
172
|
+
writeFileSync(homes.agentsPath, 'Custom agents\n');
|
|
173
|
+
|
|
174
|
+
writePackageGuidance(homes);
|
|
175
|
+
|
|
176
|
+
assert.equal(
|
|
177
|
+
readFileSync(homes.agentsPath, 'utf8'),
|
|
178
|
+
`Custom agents\n\n${guidanceBlock(PACKAGE_GUIDANCE)}`,
|
|
179
|
+
);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
for (const linkName of ['retiredGuidancePath', 'sharedGuidancePath']) {
|
|
183
|
+
test(`package guidance turns a link at ${linkName} into a file with only the package block`, (context) => {
|
|
184
|
+
const homes = makeHomes(context);
|
|
185
|
+
mkdirSync(homes.codexHome);
|
|
186
|
+
symlinkSync(homes[linkName], homes.agentsPath);
|
|
187
|
+
|
|
188
|
+
assert.equal(writePackageGuidance(homes), homes.agentsPath);
|
|
189
|
+
assert.equal(lstatSync(homes.agentsPath).isSymbolicLink(), false);
|
|
190
|
+
assert.equal(readFileSync(homes.agentsPath, 'utf8'), guidanceBlock(PACKAGE_GUIDANCE));
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
test('package guidance leaves a link to a file the package does not own unchanged', (context) => {
|
|
195
|
+
const homes = makeHomes(context);
|
|
196
|
+
mkdirSync(homes.codexHome);
|
|
197
|
+
const ownGuidancePath = join(homes.root, 'notes', 'codex.md');
|
|
198
|
+
mkdirSync(join(homes.root, 'notes'));
|
|
199
|
+
writeFileSync(ownGuidancePath, 'My own Codex notes\n');
|
|
200
|
+
symlinkSync(ownGuidancePath, homes.agentsPath);
|
|
201
|
+
|
|
202
|
+
assert.equal(writePackageGuidance(homes), null);
|
|
203
|
+
assert.equal(lstatSync(homes.agentsPath).isSymbolicLink(), true);
|
|
204
|
+
assert.equal(readFileSync(ownGuidancePath, 'utf8'), 'My own Codex notes\n');
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('package guidance leaves an existing directory and its notes unchanged', (context) => {
|
|
208
|
+
const homes = makeHomes(context);
|
|
209
|
+
mkdirSync(homes.agentsPath, { recursive: true });
|
|
210
|
+
const notesPath = join(homes.agentsPath, 'notes.md');
|
|
211
|
+
writeFileSync(notesPath, 'Directory notes\n');
|
|
212
|
+
|
|
213
|
+
assert.equal(writePackageGuidance(homes), null);
|
|
214
|
+
assert.equal(lstatSync(homes.agentsPath).isDirectory(), true);
|
|
215
|
+
assert.equal(readFileSync(notesPath, 'utf8'), 'Directory notes\n');
|
|
216
|
+
});
|
|
217
|
+
|
|
141
218
|
test('question guidance creates a missing Codex file and repeat writes preserve bytes', (context) => {
|
|
142
219
|
const homes = makeHomes(context);
|
|
143
220
|
const expectedBlock = `${QUESTION_PRESENTATION_BLOCK_START}\n${QUESTION_POLICY}${QUESTION_PRESENTATION_BLOCK_END}\n`;
|
|
@@ -245,6 +322,37 @@ test('question removal preserves a guidance directory and its notes', (context)
|
|
|
245
322
|
assert.equal(readFileSync(notesPath, 'utf8'), 'Directory notes\n');
|
|
246
323
|
});
|
|
247
324
|
|
|
325
|
+
test('package and question removal delete a file left with only blank lines', (context) => {
|
|
326
|
+
const homes = makeHomes(context);
|
|
327
|
+
writePackageGuidance(homes);
|
|
328
|
+
writeCodexQuestionGuidance(homes.codexHome, QUESTION_POLICY);
|
|
329
|
+
|
|
330
|
+
assert.equal(removeCodexPackageGuidance(homes.codexHome), homes.agentsPath);
|
|
331
|
+
assert.equal(removeCodexQuestionGuidance(homes.codexHome), homes.agentsPath);
|
|
332
|
+
assert.equal(lstatSync(homes.agentsPath, { throwIfNoEntry: false }), undefined);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
test('package removal keeps the custom text around its block', (context) => {
|
|
336
|
+
const homes = makeHomes(context);
|
|
337
|
+
mkdirSync(homes.codexHome);
|
|
338
|
+
writeFileSync(homes.agentsPath, `Before\n${guidanceBlock(PACKAGE_GUIDANCE)}After\n`);
|
|
339
|
+
|
|
340
|
+
assert.equal(removeCodexPackageGuidance(homes.codexHome), homes.agentsPath);
|
|
341
|
+
assert.equal(readFileSync(homes.agentsPath, 'utf8'), 'Before\nAfter\n');
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
test('package removal leaves a linked file and its target unchanged', (context) => {
|
|
345
|
+
const homes = makeHomes(context);
|
|
346
|
+
mkdirSync(homes.codexHome);
|
|
347
|
+
mkdirSync(dirname(homes.sharedGuidancePath), { recursive: true });
|
|
348
|
+
writeFileSync(homes.sharedGuidancePath, guidanceBlock(PACKAGE_GUIDANCE));
|
|
349
|
+
symlinkSync(homes.sharedGuidancePath, homes.agentsPath);
|
|
350
|
+
|
|
351
|
+
assert.equal(removeCodexPackageGuidance(homes.codexHome), null);
|
|
352
|
+
assert.equal(lstatSync(homes.agentsPath).isSymbolicLink(), true);
|
|
353
|
+
assert.equal(readFileSync(homes.sharedGuidancePath, 'utf8'), guidanceBlock(PACKAGE_GUIDANCE));
|
|
354
|
+
});
|
|
355
|
+
|
|
248
356
|
test('question removal leaves a missing guidance file absent', (context) => {
|
|
249
357
|
const homes = makeHomes(context);
|
|
250
358
|
|
package/bin/install.mjs
CHANGED
|
@@ -53,8 +53,10 @@ import {
|
|
|
53
53
|
} from './install-pstack-plugin.mjs';
|
|
54
54
|
import { seedCodexPstackModels } from './seed-codex-pstack-models.mjs';
|
|
55
55
|
import {
|
|
56
|
+
removeCodexPackageGuidance,
|
|
56
57
|
removeCodexQuestionGuidance,
|
|
57
58
|
writeCodexAgentsGuidance,
|
|
59
|
+
writeCodexPackageGuidance,
|
|
58
60
|
writeCodexQuestionGuidance,
|
|
59
61
|
} from './codex-skill-load-block.mjs';
|
|
60
62
|
import {
|
|
@@ -2606,7 +2608,7 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
|
|
|
2606
2608
|
];
|
|
2607
2609
|
|
|
2608
2610
|
const allInstalledFiles = [];
|
|
2609
|
-
const
|
|
2611
|
+
const allCodexGuidancePaths = [];
|
|
2610
2612
|
const allUserOwnedPreferencePaths = new Set();
|
|
2611
2613
|
const summary = {};
|
|
2612
2614
|
for (const directory of CONTENT_DIRECTORIES) {
|
|
@@ -2758,6 +2760,8 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
|
|
|
2758
2760
|
console.log(` Pstack: retired release store removed from ${retiredPstack.removedStorePath}`);
|
|
2759
2761
|
}
|
|
2760
2762
|
}
|
|
2763
|
+
const packageGuidanceText = selectedGroups ? '' : readFileSync(join(PACKAGE_ROOT, 'AGENTS.md'), 'utf8');
|
|
2764
|
+
const allPackageGuidancePaths = [join(CLAUDE_HOME, 'AGENTS.md'), join(AGENTS_HOME, 'AGENTS.md')];
|
|
2761
2765
|
if (!selectedGroups && shouldInstallPstackPlugin()) {
|
|
2762
2766
|
const pstackPlugin = installPstackPluginForHosts();
|
|
2763
2767
|
summary.pstackPlugin = pstackPlugin;
|
|
@@ -2773,8 +2777,8 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
|
|
|
2773
2777
|
}
|
|
2774
2778
|
const skillLoadGuidancePath = writeCodexAgentsGuidance(
|
|
2775
2779
|
INSTALL_ROOT_RESOLUTION.codexHomeDirectory,
|
|
2776
|
-
|
|
2777
|
-
|
|
2780
|
+
packageGuidanceText,
|
|
2781
|
+
allPackageGuidancePaths,
|
|
2778
2782
|
);
|
|
2779
2783
|
if (skillLoadGuidancePath) {
|
|
2780
2784
|
console.log(` \u2713 ${skillLoadGuidancePath} (Codex skill-load line)`);
|
|
@@ -2787,13 +2791,22 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
|
|
|
2787
2791
|
}
|
|
2788
2792
|
}
|
|
2789
2793
|
if (!selectedGroups) {
|
|
2794
|
+
const packageGuidancePath = writeCodexPackageGuidance(
|
|
2795
|
+
INSTALL_ROOT_RESOLUTION.codexHomeDirectory,
|
|
2796
|
+
packageGuidanceText,
|
|
2797
|
+
allPackageGuidancePaths,
|
|
2798
|
+
);
|
|
2799
|
+
if (packageGuidancePath) {
|
|
2800
|
+
allCodexGuidancePaths.push(packageGuidancePath);
|
|
2801
|
+
console.log(` \u2713 ${packageGuidancePath} (Codex package guidance)`);
|
|
2802
|
+
}
|
|
2790
2803
|
const questionGuidancePath = writeCodexQuestionGuidance(
|
|
2791
2804
|
INSTALL_ROOT_RESOLUTION.codexHomeDirectory,
|
|
2792
2805
|
readFileSync(join(PACKAGE_ROOT, 'rules', 'question-presentation.md'), 'utf8'),
|
|
2793
2806
|
);
|
|
2794
|
-
if (questionGuidancePath)
|
|
2807
|
+
if (questionGuidancePath) allCodexGuidancePaths.push(questionGuidancePath);
|
|
2795
2808
|
}
|
|
2796
|
-
syncWrittenPaths([...allInstalledFiles, ...
|
|
2809
|
+
syncWrittenPaths([...allInstalledFiles, ...allCodexGuidancePaths, ...publishedPointerPaths]);
|
|
2797
2810
|
throwIfFault(FAULT_PHASES.AFTER_FILE_STAGING);
|
|
2798
2811
|
throwIfFault(FAULT_PHASES.BEFORE_DURABLE_PROMOTION);
|
|
2799
2812
|
const shouldInstallAnyHooks = shouldInstallAllHooks || (allowedHookFiles && allowedHookFiles.size > 0);
|
|
@@ -2834,7 +2847,7 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
|
|
|
2834
2847
|
summary.hookGroups = totalHookGroups;
|
|
2835
2848
|
summary.codexHookGroups = totalCodexHookGroups;
|
|
2836
2849
|
console.log(` Hook groups: ${totalHookGroups} merged into settings.json, ${totalCodexHookGroups} merged into hooks.json`);
|
|
2837
|
-
syncWrittenPaths([...allInstalledFiles, ...
|
|
2850
|
+
syncWrittenPaths([...allInstalledFiles, ...allCodexGuidancePaths, ...publishedPointerPaths]);
|
|
2838
2851
|
throwIfFault(FAULT_PHASES.AFTER_SETTINGS_WRITE);
|
|
2839
2852
|
|
|
2840
2853
|
console.warn(
|
|
@@ -2856,7 +2869,7 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
|
|
|
2856
2869
|
console.warn(` Git hooks: ${gitHookInstallationResult.hooksPathConfigurationResult.reason}`);
|
|
2857
2870
|
}
|
|
2858
2871
|
console.log(` Git hook shims: ${gitHookInstallationResult.createdShimPaths.length} files (${KNOWN_GIT_HOOK_NAMES.join(', ')})`);
|
|
2859
|
-
syncWrittenPaths([...allInstalledFiles, ...
|
|
2872
|
+
syncWrittenPaths([...allInstalledFiles, ...allCodexGuidancePaths, ...publishedPointerPaths]);
|
|
2860
2873
|
throwIfFault(FAULT_PHASES.AFTER_GIT_CONFIG);
|
|
2861
2874
|
throwIfFault(FAULT_PHASES.AFTER_LINK_PUBLICATION);
|
|
2862
2875
|
|
|
@@ -2876,7 +2889,7 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
|
|
|
2876
2889
|
console.warn(` ${mypyIniInstallResult.expectedLine}`);
|
|
2877
2890
|
}
|
|
2878
2891
|
} else {
|
|
2879
|
-
syncWrittenPaths([...allInstalledFiles, ...
|
|
2892
|
+
syncWrittenPaths([...allInstalledFiles, ...allCodexGuidancePaths, ...publishedPointerPaths]);
|
|
2880
2893
|
throwIfFault(FAULT_PHASES.AFTER_SETTINGS_WRITE);
|
|
2881
2894
|
throwIfFault(FAULT_PHASES.AFTER_GIT_CONFIG);
|
|
2882
2895
|
throwIfFault(FAULT_PHASES.AFTER_LINK_PUBLICATION);
|
|
@@ -2964,7 +2977,7 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
|
|
|
2964
2977
|
? manifestFilesWithFailedPrunes(allManagedInstalledFiles, failedPrunePaths)
|
|
2965
2978
|
: unionOnComparisonKey(priorManifestFiles || [], allManagedInstalledFiles);
|
|
2966
2979
|
writeManifest(manifestFiles, manifestSkillNames, summary.managedPermissions.managedPermissions);
|
|
2967
|
-
syncWrittenPaths([...allInstalledFiles, ...
|
|
2980
|
+
syncWrittenPaths([...allInstalledFiles, ...allCodexGuidancePaths, MANIFEST_FILE, plan.settingsPath, ...publishedPointerPaths]);
|
|
2968
2981
|
throwIfFault(FAULT_PHASES.AFTER_MANIFEST_WRITE);
|
|
2969
2982
|
console.log(`\nInstalled ${PACKAGE_NAME}:`);
|
|
2970
2983
|
for (const directory of CONTENT_DIRECTORIES) {
|
|
@@ -3143,6 +3156,7 @@ function executeUninstallPlan(plan, helpers = {}) {
|
|
|
3143
3156
|
` ${plan.skippedFiles.length} manifest record(s) skipped — each names a path outside ${CLAUDE_HOME}, outside ${MYPY_INI_INSTALL_PATH}, outside ${INSTALL_ROOT_RESOLUTION.codexRulesInstallDirectory}, outside ${INSTALL_ROOT_RESOLUTION.cursorInstallDirectory}, and outside ${AGENTS_HOME}`,
|
|
3144
3157
|
);
|
|
3145
3158
|
}
|
|
3159
|
+
removeCodexPackageGuidance(INSTALL_ROOT_RESOLUTION.codexHomeDirectory);
|
|
3146
3160
|
removeCodexQuestionGuidance(INSTALL_ROOT_RESOLUTION.codexHomeDirectory);
|
|
3147
3161
|
throwIfFault(FAULT_PHASES.AFTER_FILE_STAGING);
|
|
3148
3162
|
|
|
@@ -81,7 +81,7 @@ test('uninstall removes question-only Codex guidance', (context) => {
|
|
|
81
81
|
assert.equal(existsSync(codexGuidancePath), false);
|
|
82
82
|
});
|
|
83
83
|
|
|
84
|
-
test('uninstall removes the
|
|
84
|
+
test('uninstall removes the managed blocks and preserves surrounding custom guidance', (context) => {
|
|
85
85
|
const homeDirectory = makeScratchHome(context);
|
|
86
86
|
const codexGuidancePath = join(homeDirectory, '.codex', 'AGENTS.md');
|
|
87
87
|
mkdirSync(dirname(codexGuidancePath), { recursive: true });
|
|
@@ -94,7 +94,7 @@ test('uninstall removes the question block and preserves surrounding custom guid
|
|
|
94
94
|
const removal = installInScratchHome(homeDirectory, ['--uninstall']);
|
|
95
95
|
|
|
96
96
|
assert.equal(removal.status, 0, removal.stdout + removal.stderr);
|
|
97
|
-
assert.equal(readFileSync(codexGuidancePath, 'utf8'), `${customGuidance}\nTrailing notes\n`);
|
|
97
|
+
assert.equal(readFileSync(codexGuidancePath, 'utf8'), `${customGuidance}\n\nTrailing notes\n`);
|
|
98
98
|
});
|
|
99
99
|
|
|
100
100
|
for (const eachPriorGuidance of [null, '# Personal guidance\n']) {
|