claude-dev-env 8.11.0 → 8.12.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/e-code-review/SKILL.md +24 -71
- package/.agents/skills/e-code-review/reference/fix.md +7 -78
- package/.agents/skills/e-code-review/reference/high.md +129 -0
- package/.agents/skills/e-code-review/reference/low.md +20 -51
- package/.agents/skills/e-code-review/reference/max.md +169 -0
- package/.agents/skills/e-code-review/reference/medium.md +7 -191
- package/.agents/skills/e-code-review/reference/xhigh.md +47 -91
- package/_shared/pr-loop/scripts/fix_pr_test_proof.py +113 -12
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/fix_pr_test_proof_constants.py +5 -1
- package/_shared/pr-loop/scripts/test_fix_pr_test_proof.py +63 -3
- package/bin/codex-skill-load-block.mjs +67 -19
- package/bin/codex-skill-load-block.test.mjs +87 -25
- package/bin/install.agents-home.test.mjs +9 -9
- package/bin/install.mjs +4 -2
- package/bin/install.test.mjs +20 -0
- package/commands/sr-loop.md +3 -1
- package/hooks/blocking/pii_prevention_blocker_parts/repository_exemption.py +3 -3
- package/hooks/blocking/tests/test_repository_exemption.py +7 -0
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +0 -1
- package/package.json +1 -1
- package/rules/build-needs-a-user.md +1 -1
- package/rules/testing.md +1 -1
- package/scripts/policy_lint/config/constants.py +1 -1
- package/scripts/repository_checks/tracked_private_terms.py +15 -1
- package/scripts/tests/test_policy_lint_rules_callers.py +14 -0
- package/scripts/tests/test_tracked_private_terms.py +35 -0
- package/.agents/skills/e-code-review/preflight-proposal.contract.test.mjs +0 -20
- package/.agents/skills/e-code-review/reference/effort-evaluation.md +0 -35
- package/.agents/skills/e-code-review/reference/loop.md +0 -261
- package/.agents/skills/e-code-review/reference/mode-contract.test.mjs +0 -67
- package/.agents/skills/e-code-review/reference/preflight-proposal.md +0 -75
- package/.agents/skills/e-code-review/reference/runner-selection.md +0 -40
- package/.agents/skills/e-code-review/scripts/config/e_code_review_effort_constants/__init__.py +0 -41
- package/.agents/skills/e-code-review/scripts/config/e_code_review_effort_constants/effort_constants.py +0 -40
- package/.agents/skills/e-code-review/scripts/e_code_review_scripts_constants/__init__.py +0 -1
- package/.agents/skills/e-code-review/scripts/e_code_review_scripts_constants/finding_pipeline_constants.py +0 -49
- package/.agents/skills/e-code-review/scripts/e_code_review_scripts_constants/grok_code_review_constants.py +0 -55
- package/.agents/skills/e-code-review/scripts/effort_defaults_evidence.json +0 -186
- package/.agents/skills/e-code-review/scripts/effort_evaluation.py +0 -362
- package/.agents/skills/e-code-review/scripts/finding_pipeline.py +0 -140
- package/.agents/skills/e-code-review/scripts/fixtures/demanding.json +0 -26
- package/.agents/skills/e-code-review/scripts/fixtures/easy.json +0 -14
- package/.agents/skills/e-code-review/scripts/fixtures/medium.json +0 -20
- package/.agents/skills/e-code-review/scripts/grok_code_review.py +0 -230
- package/.agents/skills/e-code-review/scripts/test_effort_evaluation.py +0 -180
- package/.agents/skills/e-code-review/scripts/test_finding_pipeline.py +0 -197
- package/.agents/skills/e-code-review/scripts/test_grok_code_review.py +0 -289
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Behavioral tests for the fix pull request test-proof check.
|
|
2
2
|
|
|
3
|
-
Every case drives a git repository under ``tmp_path`` and a pytest
|
|
4
|
-
the same shape the CI job runs.
|
|
3
|
+
Every case drives a git repository under ``tmp_path`` and a pytest or
|
|
4
|
+
``node --test`` subprocess, the same shape the CI job runs.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from pathlib import Path
|
|
@@ -27,6 +27,18 @@ PROOF_TEST_TEXT = (
|
|
|
27
27
|
ALWAYS_PASSING_TEST_TEXT = (
|
|
28
28
|
"def test_nothing_in_particular() -> None:\n assert True\n"
|
|
29
29
|
)
|
|
30
|
+
BUGGY_NODE_PRODUCTION_TEXT = "export function add(left, right) {\n return left - right;\n}\n"
|
|
31
|
+
FIXED_NODE_PRODUCTION_TEXT = "export function add(left, right) {\n return left + right;\n}\n"
|
|
32
|
+
NODE_PROOF_TEST_TEXT = (
|
|
33
|
+
"import { test } from 'node:test';\n"
|
|
34
|
+
"import { strict as assert } from 'node:assert';\n"
|
|
35
|
+
"import { add } from './calc.mjs';\n\n"
|
|
36
|
+
"test('add sums both operands', () => {\n assert.equal(add(1, 2), 3);\n});\n"
|
|
37
|
+
)
|
|
38
|
+
NODE_ALWAYS_PASSING_TEST_TEXT = (
|
|
39
|
+
"import { test } from 'node:test';\n\n"
|
|
40
|
+
"test('nothing in particular', () => {});\n"
|
|
41
|
+
)
|
|
30
42
|
|
|
31
43
|
|
|
32
44
|
def _commit_files(repository_root: Path, all_file_texts: dict[str, str]) -> None:
|
|
@@ -48,6 +60,12 @@ def _repository_at_buggy_base(tmp_path: Path) -> tuple[Path, str]:
|
|
|
48
60
|
return repository_root, _head_revision(repository_root)
|
|
49
61
|
|
|
50
62
|
|
|
63
|
+
def _repository_at_buggy_node_base(tmp_path: Path) -> tuple[Path, str]:
|
|
64
|
+
repository_root = repository_with_root_pytest_config(tmp_path)
|
|
65
|
+
_commit_files(repository_root, {"bin/calc.mjs": BUGGY_NODE_PRODUCTION_TEXT})
|
|
66
|
+
return repository_root, _head_revision(repository_root)
|
|
67
|
+
|
|
68
|
+
|
|
51
69
|
def _run_check(repository_root: Path, title: str, base_revision: str) -> int:
|
|
52
70
|
return fix_pr_test_proof.main(
|
|
53
71
|
[
|
|
@@ -82,7 +100,7 @@ def test_a_fix_with_no_changed_test_fails(
|
|
|
82
100
|
_commit_files(repository_root, {"pkg/calc.py": FIXED_PRODUCTION_TEXT})
|
|
83
101
|
|
|
84
102
|
assert _run_check(repository_root, "fix: sum operands", base_revision) == 1
|
|
85
|
-
assert "no Python test" in capsys.readouterr().err
|
|
103
|
+
assert "no Python or Node test" in capsys.readouterr().err
|
|
86
104
|
|
|
87
105
|
|
|
88
106
|
def test_a_fix_whose_test_fails_on_base_and_passes_on_head_passes(
|
|
@@ -124,3 +142,45 @@ def test_a_fix_whose_changed_test_fails_on_head_fails(
|
|
|
124
142
|
|
|
125
143
|
assert _run_check(repository_root, "fix: sum operands", base_revision) == 1
|
|
126
144
|
assert "fails on the head" in capsys.readouterr().err
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def test_a_node_fix_whose_test_fails_on_base_and_passes_on_head_passes(
|
|
148
|
+
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
149
|
+
) -> None:
|
|
150
|
+
repository_root, base_revision = _repository_at_buggy_node_base(tmp_path)
|
|
151
|
+
_commit_files(
|
|
152
|
+
repository_root,
|
|
153
|
+
{"bin/calc.mjs": FIXED_NODE_PRODUCTION_TEXT, "bin/calc.test.mjs": NODE_PROOF_TEST_TEXT},
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
assert _run_check(repository_root, "fix(bin): sum operands", base_revision) == 0
|
|
157
|
+
assert "fail on the base and pass on the head" in capsys.readouterr().out
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def test_a_node_fix_whose_changed_test_also_passes_on_base_fails(
|
|
161
|
+
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
162
|
+
) -> None:
|
|
163
|
+
repository_root, base_revision = _repository_at_buggy_node_base(tmp_path)
|
|
164
|
+
_commit_files(
|
|
165
|
+
repository_root,
|
|
166
|
+
{
|
|
167
|
+
"bin/calc.mjs": FIXED_NODE_PRODUCTION_TEXT,
|
|
168
|
+
"bin/calc.test.mjs": NODE_ALWAYS_PASSING_TEST_TEXT,
|
|
169
|
+
},
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
assert _run_check(repository_root, "fix(bin): sum operands", base_revision) == 1
|
|
173
|
+
assert "passes on the base" in capsys.readouterr().err
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def test_a_node_fix_whose_changed_test_fails_on_head_fails(
|
|
177
|
+
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
178
|
+
) -> None:
|
|
179
|
+
repository_root, base_revision = _repository_at_buggy_node_base(tmp_path)
|
|
180
|
+
_commit_files(
|
|
181
|
+
repository_root,
|
|
182
|
+
{"bin/calc.test.mjs": NODE_PROOF_TEST_TEXT, "bin/extra.mjs": "export const VALUE = 1;\n"},
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
assert _run_check(repository_root, "fix(bin): sum operands", base_revision) == 1
|
|
186
|
+
assert "fails on the head" in capsys.readouterr().err
|
|
@@ -1,13 +1,29 @@
|
|
|
1
|
-
import { lstatSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
1
|
+
import { lstatSync, mkdirSync, readFileSync, readlinkSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join, resolve } from 'node:path';
|
|
3
3
|
|
|
4
4
|
export const SKILL_LOAD_BLOCK_START = '<!-- claude-dev-env skill load: start -->';
|
|
5
5
|
export const SKILL_LOAD_BLOCK_END = '<!-- claude-dev-env skill load: end -->';
|
|
6
6
|
export const SKILL_LOAD_INSTRUCTION = 'Load the pstack:poteto-mode skill before your first tool call or reply: '
|
|
7
7
|
+ 'open its SKILL.md from your skills list and follow it. This binds every Codex agent, '
|
|
8
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 -->';
|
|
9
11
|
|
|
10
|
-
const
|
|
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
|
+
}
|
|
11
27
|
|
|
12
28
|
/**
|
|
13
29
|
* Return the Codex guidance text with the package's skill-load block in it.
|
|
@@ -17,29 +33,61 @@ const SKILL_LOAD_BLOCK = `${SKILL_LOAD_BLOCK_START}\n${SKILL_LOAD_INSTRUCTION}\n
|
|
|
17
33
|
* the text is replaced where it stands; otherwise the block goes first.
|
|
18
34
|
*/
|
|
19
35
|
export function withSkillLoadBlock(guidanceText) {
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
if (
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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);
|
|
28
64
|
}
|
|
29
65
|
|
|
30
66
|
/**
|
|
31
|
-
*
|
|
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.
|
|
32
75
|
*
|
|
33
|
-
* Returns the file path when the file changed, and null
|
|
34
|
-
* the current block.
|
|
76
|
+
* Returns the file path when the file changed, and null otherwise.
|
|
35
77
|
*/
|
|
36
|
-
export function
|
|
78
|
+
export function writeCodexAgentsGuidance(codexHome, packageGuidanceText, allPackageGuidancePaths) {
|
|
37
79
|
const agentsPath = join(codexHome, 'AGENTS.md');
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
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;
|
|
42
89
|
mkdirSync(codexHome, { recursive: true });
|
|
90
|
+
if (isPackageGuidanceLink) unlinkSync(agentsPath);
|
|
43
91
|
writeFileSync(agentsPath, updatedText, 'utf8');
|
|
44
92
|
return agentsPath;
|
|
45
93
|
}
|
|
@@ -1,22 +1,43 @@
|
|
|
1
1
|
import { test } from 'node:test';
|
|
2
2
|
import { strict as assert } from 'node:assert';
|
|
3
|
-
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { lstatSync, mkdtempSync, mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import {
|
|
7
|
+
PACKAGE_GUIDANCE_BLOCK_END,
|
|
8
|
+
PACKAGE_GUIDANCE_BLOCK_START,
|
|
7
9
|
SKILL_LOAD_BLOCK_END,
|
|
8
10
|
SKILL_LOAD_BLOCK_START,
|
|
9
11
|
SKILL_LOAD_INSTRUCTION,
|
|
10
12
|
withSkillLoadBlock,
|
|
11
|
-
|
|
13
|
+
writeCodexAgentsGuidance,
|
|
12
14
|
} from './codex-skill-load-block.mjs';
|
|
13
15
|
|
|
14
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';
|
|
15
18
|
|
|
16
|
-
function
|
|
19
|
+
function guidanceBlock(guidanceText) {
|
|
20
|
+
return `${PACKAGE_GUIDANCE_BLOCK_START}\n${guidanceText}${PACKAGE_GUIDANCE_BLOCK_END}\n`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function makeHomes(context) {
|
|
17
24
|
const root = mkdtempSync(join(tmpdir(), 'cde-skill-load-'));
|
|
18
25
|
context.after(() => rmSync(root, { recursive: true, force: true }));
|
|
19
|
-
|
|
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);
|
|
20
41
|
}
|
|
21
42
|
|
|
22
43
|
test('the instruction names the skill and binds spawned helpers', () => {
|
|
@@ -24,37 +45,37 @@ test('the instruction names the skill and binds spawned helpers', () => {
|
|
|
24
45
|
assert.match(SKILL_LOAD_INSTRUCTION, /spawn_agent/);
|
|
25
46
|
});
|
|
26
47
|
|
|
27
|
-
test('a missing Codex guidance file is created holding only the block', (context) => {
|
|
28
|
-
const
|
|
48
|
+
test('a missing Codex guidance file is created holding only the skill-load block', (context) => {
|
|
49
|
+
const homes = makeHomes(context);
|
|
29
50
|
|
|
30
|
-
const writtenPath =
|
|
51
|
+
const writtenPath = writeGuidance(homes);
|
|
31
52
|
|
|
32
|
-
assert.equal(writtenPath,
|
|
33
|
-
assert.equal(readFileSync(
|
|
53
|
+
assert.equal(writtenPath, homes.agentsPath);
|
|
54
|
+
assert.equal(readFileSync(homes.agentsPath, 'utf8'), EXPECTED_BLOCK);
|
|
34
55
|
});
|
|
35
56
|
|
|
36
|
-
test('existing guidance keeps every line and gains the block first', (context) => {
|
|
37
|
-
const
|
|
38
|
-
mkdirSync(codexHome);
|
|
39
|
-
writeFileSync(
|
|
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');
|
|
40
61
|
|
|
41
|
-
|
|
62
|
+
writeGuidance(homes);
|
|
42
63
|
|
|
43
64
|
assert.equal(
|
|
44
|
-
readFileSync(
|
|
65
|
+
readFileSync(homes.agentsPath, 'utf8'),
|
|
45
66
|
`${EXPECTED_BLOCK}\n# pstack model configuration\n\nbug-fix: gpt-6-astra\n`,
|
|
46
67
|
);
|
|
47
68
|
});
|
|
48
69
|
|
|
49
70
|
test('a repeat install leaves the file unchanged and reports no write', (context) => {
|
|
50
|
-
const
|
|
51
|
-
mkdirSync(codexHome);
|
|
52
|
-
writeFileSync(
|
|
53
|
-
|
|
54
|
-
const afterFirstInstall = readFileSync(
|
|
55
|
-
|
|
56
|
-
assert.equal(
|
|
57
|
-
assert.equal(readFileSync(
|
|
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);
|
|
58
79
|
});
|
|
59
80
|
|
|
60
81
|
test('an outdated block is replaced in place and the text around it is kept', () => {
|
|
@@ -66,7 +87,48 @@ test('an outdated block is replaced in place and the text around it is kept', ()
|
|
|
66
87
|
test('a start marker with no end marker gets a fresh block and keeps the stray text', () => {
|
|
67
88
|
const broken = `${SKILL_LOAD_BLOCK_START}\nHalf a block\n`;
|
|
68
89
|
|
|
69
|
-
|
|
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
|
+
});
|
|
70
122
|
|
|
71
|
-
|
|
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');
|
|
72
134
|
});
|
|
@@ -39,7 +39,7 @@ const REFERENCE_DIRECTORY_NAME = 'reference';
|
|
|
39
39
|
const SHIPPED_AGENT_FILE_NAME = 'AGENTS.md';
|
|
40
40
|
const RETIRED_AGENT_FILE_NAMES = ['clean-coder.md', 'code-quality-agent.md', 'pr-description-writer.md'];
|
|
41
41
|
const PERSONAL_SKILL_NAME = 'my-notes';
|
|
42
|
-
const
|
|
42
|
+
const REVIEW_PROCEDURE_FILE_NAME = 'high.md';
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
45
|
* @param {string} homeDirectory
|
|
@@ -70,22 +70,22 @@ function runInstaller(homeDirectory, extraArguments, environmentOverrides = {})
|
|
|
70
70
|
/**
|
|
71
71
|
* @param {{ skillsInstallDirectory: string }} installationPaths
|
|
72
72
|
*/
|
|
73
|
-
function
|
|
73
|
+
function assertReviewProcedureInstallation(installationPaths) {
|
|
74
74
|
const { skillsInstallDirectory } = installationPaths;
|
|
75
|
-
const
|
|
75
|
+
const allProcedurePathSegments = [
|
|
76
76
|
E_CODE_REVIEW_SKILL_NAME,
|
|
77
77
|
REFERENCE_DIRECTORY_NAME,
|
|
78
|
-
|
|
78
|
+
REVIEW_PROCEDURE_FILE_NAME,
|
|
79
79
|
];
|
|
80
|
-
const
|
|
80
|
+
const installedProcedurePath = join(skillsInstallDirectory, ...allProcedurePathSegments);
|
|
81
81
|
const sourceSkillsDirectory = resolvePackageManagedDirectory(
|
|
82
82
|
PACKAGE_DIRECTORY,
|
|
83
83
|
MANAGED_SKILLS_DIRECTORY_NAME,
|
|
84
84
|
);
|
|
85
|
-
const
|
|
85
|
+
const sourceProcedurePath = join(sourceSkillsDirectory, ...allProcedurePathSegments);
|
|
86
86
|
|
|
87
|
-
assert.equal(existsSync(
|
|
88
|
-
assert.equal(readFileSync(
|
|
87
|
+
assert.equal(existsSync(installedProcedurePath), true);
|
|
88
|
+
assert.equal(readFileSync(installedProcedurePath, 'utf8'), readFileSync(sourceProcedurePath, 'utf8'));
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
|
|
@@ -179,7 +179,7 @@ test('a full install writes skills and agents under .agents and points .claude a
|
|
|
179
179
|
);
|
|
180
180
|
assert.equal(realpathSync(lookupSkillFile), realpathSync(canonicalSkillFile));
|
|
181
181
|
assert.equal(realpathSync(lookupEli5SkillFile), realpathSync(canonicalEli5SkillFile));
|
|
182
|
-
|
|
182
|
+
assertReviewProcedureInstallation({ skillsInstallDirectory });
|
|
183
183
|
const projectedAdvisorPaths = [
|
|
184
184
|
join(skillsInstallDirectory, TEAM_ADVISOR_SKILL_NAME, 'SKILL.md'),
|
|
185
185
|
join(claudeHome, 'docs', 'references', 'advisor-tool.md'),
|
package/bin/install.mjs
CHANGED
|
@@ -45,7 +45,7 @@ import {
|
|
|
45
45
|
shouldInstallPstackPlugin,
|
|
46
46
|
} from './install-pstack-plugin.mjs';
|
|
47
47
|
import { seedCodexPstackModels } from './seed-codex-pstack-models.mjs';
|
|
48
|
-
import {
|
|
48
|
+
import { writeCodexAgentsGuidance } from './codex-skill-load-block.mjs';
|
|
49
49
|
import {
|
|
50
50
|
continuityHostConfigurationPaths,
|
|
51
51
|
removeContinuityHooks,
|
|
@@ -2788,8 +2788,10 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
|
|
|
2788
2788
|
allUserOwnedPreferencePaths.add(eachPath);
|
|
2789
2789
|
}
|
|
2790
2790
|
}
|
|
2791
|
-
const skillLoadGuidancePath =
|
|
2791
|
+
const skillLoadGuidancePath = writeCodexAgentsGuidance(
|
|
2792
2792
|
INSTALL_ROOT_RESOLUTION.codexHomeDirectory,
|
|
2793
|
+
readFileSync(join(PACKAGE_ROOT, 'AGENTS.md'), 'utf8'),
|
|
2794
|
+
[join(CLAUDE_HOME, 'AGENTS.md'), join(AGENTS_HOME, 'AGENTS.md')],
|
|
2793
2795
|
);
|
|
2794
2796
|
if (skillLoadGuidancePath) {
|
|
2795
2797
|
console.log(` \u2713 ${skillLoadGuidancePath} (Codex skill-load line)`);
|
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';
|
|
@@ -3099,6 +3100,25 @@ test('a repeat install preserves Codex pstack configuration', t => {
|
|
|
3099
3100
|
assert.equal(readFileSync(join(codexHome, 'pstack-models.md'), 'utf8'), 'session hook: off\n');
|
|
3100
3101
|
});
|
|
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));
|
|
3120
|
+
});
|
|
3121
|
+
|
|
3102
3122
|
test('an absent host command is reported and leaves the other host installed', t => {
|
|
3103
3123
|
const sandbox = pstackPluginSandbox(t);
|
|
3104
3124
|
|
package/commands/sr-loop.md
CHANGED
|
@@ -54,7 +54,9 @@ stack's design, intent, and changes.
|
|
|
54
54
|
## Phase B — loop /code-review low --fix
|
|
55
55
|
|
|
56
56
|
1. Invoke the `code-review` skill with arguments `low --fix` on the same
|
|
57
|
-
target. Let it report findings and apply its fixes.
|
|
57
|
+
target. Let it report findings and apply its fixes. When the host has no
|
|
58
|
+
`code-review` skill, invoke the `e-code-review` skill with `low --fix`
|
|
59
|
+
instead. It runs the same review and fix steps.
|
|
58
60
|
2. After any pass that changed files: test, commit, push as in Phase A.
|
|
59
61
|
3. Repeat until a pass reports zero findings, or until a pass reports only
|
|
60
62
|
nits — then fix, test, commit, push, and stop with no confirming pass.
|
|
@@ -64,7 +64,7 @@ def repository_allowlisted_values(repository_root: Path) -> frozenset[str]:
|
|
|
64
64
|
The exact literal values the repository's commits and writes may carry
|
|
65
65
|
past the PII scan, or an empty set when the repository has none.
|
|
66
66
|
"""
|
|
67
|
-
origin_slug =
|
|
67
|
+
origin_slug = repository_origin_slug(repository_root)
|
|
68
68
|
if origin_slug is None:
|
|
69
69
|
return frozenset()
|
|
70
70
|
return pii_allowlisted_values_by_repository().get(origin_slug, frozenset())
|
|
@@ -171,7 +171,7 @@ def _owner_repo_slug_from_origin_url(origin_url: str) -> str | None:
|
|
|
171
171
|
return _owner_repo_slug_from_path(repository_path)
|
|
172
172
|
|
|
173
173
|
|
|
174
|
-
def
|
|
174
|
+
def repository_origin_slug(repository_root: Path) -> str | None:
|
|
175
175
|
"""Return the lowercased owner/repo slug of the origin remote, or None.
|
|
176
176
|
|
|
177
177
|
Args:
|
|
@@ -211,7 +211,7 @@ def _is_repository_exempt_from_pii_scan(repository_root: Path) -> bool:
|
|
|
211
211
|
Returns:
|
|
212
212
|
True when the repository's origin slug is in the exempt set.
|
|
213
213
|
"""
|
|
214
|
-
origin_slug =
|
|
214
|
+
origin_slug = repository_origin_slug(repository_root)
|
|
215
215
|
if origin_slug is None:
|
|
216
216
|
return False
|
|
217
217
|
return origin_slug in pii_exempt_repository_slugs()
|
|
@@ -19,6 +19,7 @@ from pii_prevention_blocker_parts.repository_exemption import (
|
|
|
19
19
|
_is_repository_exempt_from_pii_scan,
|
|
20
20
|
_owner_repo_slug_from_origin_url,
|
|
21
21
|
repository_allowlisted_values,
|
|
22
|
+
repository_origin_slug,
|
|
22
23
|
)
|
|
23
24
|
|
|
24
25
|
_ALLOWED_VALUE = "owner.fixture" + "@" + "acme-corp" + ".example" + ".io"
|
|
@@ -103,3 +104,9 @@ def test_repository_without_origin_is_never_exempt(tmp_path: Path) -> None:
|
|
|
103
104
|
|
|
104
105
|
def test_owner_repo_slug_strips_a_trailing_slash_origin() -> None:
|
|
105
106
|
assert _owner_repo_slug_from_origin_url("https://github.com/Owner/Repo/") == "owner/repo"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_repository_origin_slug_reads_the_origin_remote(tmp_path: Path) -> None:
|
|
110
|
+
repository_root = tmp_path / "repo"
|
|
111
|
+
_init_repo_with_github_origin(repository_root, "SlugOwner/slug-repo")
|
|
112
|
+
assert repository_origin_slug(repository_root) == "slugowner/slug-repo"
|
package/package.json
CHANGED
|
@@ -32,7 +32,7 @@ correction arrives a second time, per
|
|
|
32
32
|
## Where it is enforced
|
|
33
33
|
|
|
34
34
|
The staged policy lint's `uncalled-new-file` rule reads each code file a change
|
|
35
|
-
adds under `scripts/`, `hooks/`, or `
|
|
35
|
+
adds under `scripts/`, `hooks/`, `bin/`, `ci/`, or `tools/`. It reports the file when its name
|
|
36
36
|
appears only in its own tests, `CHANGELOG.md`, `README.md`, and
|
|
37
37
|
`bin/ever-shipped-skills.mjs`. CI runs that lint against the merge base, so a
|
|
38
38
|
file with no caller turns the pull request red.
|
package/rules/testing.md
CHANGED
|
@@ -29,4 +29,4 @@ Red, green, refactor is the default loop for a bug fix and for new behavior. The
|
|
|
29
29
|
|
|
30
30
|
## A Fix Carries Its Proof Test
|
|
31
31
|
|
|
32
|
-
The `Fix test proof` job in `.github/workflows/pr-check.yml` runs `_shared/pr-loop/scripts/fix_pr_test_proof.py` on every pull request whose title starts with `fix`. A fix that changes production code must change at least one Python test that fails on the base and passes on the head. A fix that changes only docs or CI config passes.
|
|
32
|
+
The `Fix test proof` job in `.github/workflows/pr-check.yml` runs `_shared/pr-loop/scripts/fix_pr_test_proof.py` on every pull request whose title starts with `fix`. A fix that changes production code must change at least one Python or Node test that fails on the base and passes on the head. Node tests are files ending in `.test.mjs`, `.test.js`, or `.test.cjs`, and the job runs them with `node --test`. A fix that changes only docs or CI config passes. A fix proven by a PowerShell test needs a Python or Node test beside it.
|
|
@@ -179,7 +179,7 @@ UNCALLED_NEW_FILE_MESSAGE = (
|
|
|
179
179
|
" workflow, skill, command or import in this change, or drop it."
|
|
180
180
|
)
|
|
181
181
|
ALL_CALLED_CODE_SUFFIXES = frozenset({".py", ".mjs", ".js", ".sh", ".ps1"})
|
|
182
|
-
ALL_CALLED_CODE_SEGMENTS = ("/scripts/", "/hooks/", "/bin/")
|
|
182
|
+
ALL_CALLED_CODE_SEGMENTS = ("/scripts/", "/hooks/", "/bin/", "/ci/", "/tools/")
|
|
183
183
|
ALL_CALLER_SEARCH_SKIPPED_DIRECTORIES = frozenset(
|
|
184
184
|
{".git", "node_modules", "__pycache__", ".venv"}
|
|
185
185
|
)
|
|
@@ -13,6 +13,7 @@ from policy_lint.config import constants as policy_constants
|
|
|
13
13
|
from private_terms import private_term_line_numbers
|
|
14
14
|
|
|
15
15
|
from repository_checks.config import constants as repository_constants
|
|
16
|
+
from repository_checks.hook_modules import load_hooks_module
|
|
16
17
|
from repository_checks.models import RepositoryFinding
|
|
17
18
|
|
|
18
19
|
|
|
@@ -25,6 +26,15 @@ def _read_text_or_none(absolute_path: Path) -> str | None:
|
|
|
25
26
|
return None
|
|
26
27
|
|
|
27
28
|
|
|
29
|
+
def _is_owned_by_private_organization(repository_root: Path) -> bool:
|
|
30
|
+
exemption = load_hooks_module(repository_constants.REPOSITORY_EXEMPTION_MODULE_NAME)
|
|
31
|
+
origin_slug = exemption.repository_origin_slug(repository_root)
|
|
32
|
+
if origin_slug is None:
|
|
33
|
+
return False
|
|
34
|
+
owner_name = origin_slug.split(policy_constants.PATH_SEPARATOR)[0]
|
|
35
|
+
return bool(private_term_line_numbers(owner_name, ALL_PRIVATE_TERM_DIGESTS))
|
|
36
|
+
|
|
37
|
+
|
|
28
38
|
def collect_tracked_private_term_findings(
|
|
29
39
|
repository_root: Path, all_tracked_paths: Sequence[str]
|
|
30
40
|
) -> list[RepositoryFinding]:
|
|
@@ -35,8 +45,12 @@ def collect_tracked_private_term_findings(
|
|
|
35
45
|
all_tracked_paths: Repository-relative tracked paths.
|
|
36
46
|
|
|
37
47
|
Returns:
|
|
38
|
-
Findings that carry the path and line number, never the name.
|
|
48
|
+
Findings that carry the path and line number, never the name. A
|
|
49
|
+
repository whose github.com origin owner is itself a private
|
|
50
|
+
organization returns none, since that organization may name itself.
|
|
39
51
|
"""
|
|
52
|
+
if _is_owned_by_private_organization(repository_root):
|
|
53
|
+
return []
|
|
40
54
|
all_findings: list[RepositoryFinding] = []
|
|
41
55
|
for each_relative_path in all_tracked_paths:
|
|
42
56
|
if (content := _read_text_or_none(repository_root / each_relative_path)) is None:
|
|
@@ -124,3 +124,17 @@ def test_should_run_on_staged_and_base_selections() -> None:
|
|
|
124
124
|
assert uncalled_rule.selections == frozenset(
|
|
125
125
|
{SelectionKind.STAGED, SelectionKind.BASE}
|
|
126
126
|
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def test_should_report_new_ci_and_tools_helpers_only_tests_name(
|
|
130
|
+
tmp_path: Path,
|
|
131
|
+
) -> None:
|
|
132
|
+
ci_helper = ".github/ci/lonely_router.py"
|
|
133
|
+
tools_helper = "tools/lonely_archiver.py"
|
|
134
|
+
_write(tmp_path, ci_helper, "print('hi')\n")
|
|
135
|
+
_write(tmp_path, tools_helper, "print('hi')\n")
|
|
136
|
+
_write(tmp_path, ".github/ci/test_lonely_router.py", "import lonely_router\n")
|
|
137
|
+
|
|
138
|
+
all_messages = _messages(tmp_path, (_added(ci_helper), _added(tools_helper)))
|
|
139
|
+
|
|
140
|
+
assert len(all_messages) == 2
|