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.
Files changed (47) hide show
  1. package/.agents/skills/e-code-review/SKILL.md +24 -71
  2. package/.agents/skills/e-code-review/reference/fix.md +7 -78
  3. package/.agents/skills/e-code-review/reference/high.md +129 -0
  4. package/.agents/skills/e-code-review/reference/low.md +20 -51
  5. package/.agents/skills/e-code-review/reference/max.md +169 -0
  6. package/.agents/skills/e-code-review/reference/medium.md +7 -191
  7. package/.agents/skills/e-code-review/reference/xhigh.md +47 -91
  8. package/_shared/pr-loop/scripts/fix_pr_test_proof.py +113 -12
  9. package/_shared/pr-loop/scripts/pr_loop_shared_constants/fix_pr_test_proof_constants.py +5 -1
  10. package/_shared/pr-loop/scripts/test_fix_pr_test_proof.py +63 -3
  11. package/bin/codex-skill-load-block.mjs +67 -19
  12. package/bin/codex-skill-load-block.test.mjs +87 -25
  13. package/bin/install.agents-home.test.mjs +9 -9
  14. package/bin/install.mjs +4 -2
  15. package/bin/install.test.mjs +20 -0
  16. package/commands/sr-loop.md +3 -1
  17. package/hooks/blocking/pii_prevention_blocker_parts/repository_exemption.py +3 -3
  18. package/hooks/blocking/tests/test_repository_exemption.py +7 -0
  19. package/hooks/hooks_constants/code_rules_path_utils_constants.py +0 -1
  20. package/package.json +1 -1
  21. package/rules/build-needs-a-user.md +1 -1
  22. package/rules/testing.md +1 -1
  23. package/scripts/policy_lint/config/constants.py +1 -1
  24. package/scripts/repository_checks/tracked_private_terms.py +15 -1
  25. package/scripts/tests/test_policy_lint_rules_callers.py +14 -0
  26. package/scripts/tests/test_tracked_private_terms.py +35 -0
  27. package/.agents/skills/e-code-review/preflight-proposal.contract.test.mjs +0 -20
  28. package/.agents/skills/e-code-review/reference/effort-evaluation.md +0 -35
  29. package/.agents/skills/e-code-review/reference/loop.md +0 -261
  30. package/.agents/skills/e-code-review/reference/mode-contract.test.mjs +0 -67
  31. package/.agents/skills/e-code-review/reference/preflight-proposal.md +0 -75
  32. package/.agents/skills/e-code-review/reference/runner-selection.md +0 -40
  33. package/.agents/skills/e-code-review/scripts/config/e_code_review_effort_constants/__init__.py +0 -41
  34. package/.agents/skills/e-code-review/scripts/config/e_code_review_effort_constants/effort_constants.py +0 -40
  35. package/.agents/skills/e-code-review/scripts/e_code_review_scripts_constants/__init__.py +0 -1
  36. package/.agents/skills/e-code-review/scripts/e_code_review_scripts_constants/finding_pipeline_constants.py +0 -49
  37. package/.agents/skills/e-code-review/scripts/e_code_review_scripts_constants/grok_code_review_constants.py +0 -55
  38. package/.agents/skills/e-code-review/scripts/effort_defaults_evidence.json +0 -186
  39. package/.agents/skills/e-code-review/scripts/effort_evaluation.py +0 -362
  40. package/.agents/skills/e-code-review/scripts/finding_pipeline.py +0 -140
  41. package/.agents/skills/e-code-review/scripts/fixtures/demanding.json +0 -26
  42. package/.agents/skills/e-code-review/scripts/fixtures/easy.json +0 -14
  43. package/.agents/skills/e-code-review/scripts/fixtures/medium.json +0 -20
  44. package/.agents/skills/e-code-review/scripts/grok_code_review.py +0 -230
  45. package/.agents/skills/e-code-review/scripts/test_effort_evaluation.py +0 -180
  46. package/.agents/skills/e-code-review/scripts/test_finding_pipeline.py +0 -197
  47. 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 subprocess,
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 SKILL_LOAD_BLOCK = `${SKILL_LOAD_BLOCK_START}\n${SKILL_LOAD_INSTRUCTION}\n${SKILL_LOAD_BLOCK_END}\n`;
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 startIndex = guidanceText.indexOf(SKILL_LOAD_BLOCK_START);
21
- const endIndex = startIndex === -1 ? -1 : guidanceText.indexOf(SKILL_LOAD_BLOCK_END, startIndex);
22
- if (endIndex === -1) {
23
- return guidanceText ? `${SKILL_LOAD_BLOCK}\n${guidanceText}` : SKILL_LOAD_BLOCK;
24
- }
25
- let afterBlockIndex = endIndex + SKILL_LOAD_BLOCK_END.length;
26
- if (guidanceText[afterBlockIndex] === '\n') afterBlockIndex += 1;
27
- return guidanceText.slice(0, startIndex) + SKILL_LOAD_BLOCK + guidanceText.slice(afterBlockIndex);
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
- * Write the skill-load block into CODEX_HOME/AGENTS.md.
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 when it already held
34
- * the current block.
76
+ * Returns the file path when the file changed, and null otherwise.
35
77
  */
36
- export function writeCodexSkillLoadBlock(codexHome) {
78
+ export function writeCodexAgentsGuidance(codexHome, packageGuidanceText, allPackageGuidancePaths) {
37
79
  const agentsPath = join(codexHome, 'AGENTS.md');
38
- const hasGuidance = lstatSync(agentsPath, { throwIfNoEntry: false }) !== undefined;
39
- const currentText = hasGuidance ? readFileSync(agentsPath, 'utf8') : '';
40
- const updatedText = withSkillLoadBlock(currentText);
41
- if (updatedText === currentText) return null;
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
- writeCodexSkillLoadBlock,
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 makeCodexHome(context) {
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
- return join(root, '.codex');
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 codexHome = makeCodexHome(context);
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 = writeCodexSkillLoadBlock(codexHome);
51
+ const writtenPath = writeGuidance(homes);
31
52
 
32
- assert.equal(writtenPath, join(codexHome, 'AGENTS.md'));
33
- assert.equal(readFileSync(writtenPath, 'utf8'), EXPECTED_BLOCK);
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 codexHome = makeCodexHome(context);
38
- mkdirSync(codexHome);
39
- writeFileSync(join(codexHome, 'AGENTS.md'), '# pstack model configuration\n\nbug-fix: gpt-6-astra\n');
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
- writeCodexSkillLoadBlock(codexHome);
62
+ writeGuidance(homes);
42
63
 
43
64
  assert.equal(
44
- readFileSync(join(codexHome, 'AGENTS.md'), 'utf8'),
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 codexHome = makeCodexHome(context);
51
- mkdirSync(codexHome);
52
- writeFileSync(join(codexHome, 'AGENTS.md'), 'Custom agents\n');
53
- writeCodexSkillLoadBlock(codexHome);
54
- const afterFirstInstall = readFileSync(join(codexHome, 'AGENTS.md'), 'utf8');
55
-
56
- assert.equal(writeCodexSkillLoadBlock(codexHome), null);
57
- assert.equal(readFileSync(join(codexHome, 'AGENTS.md'), 'utf8'), afterFirstInstall);
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
- const repaired = withSkillLoadBlock(broken);
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
- assert.equal(repaired, `${EXPECTED_BLOCK}\n${broken}`);
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 PREFLIGHT_PROPOSAL_FILE_NAME = 'preflight-proposal.md';
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 assertProposalContractInstallation(installationPaths) {
73
+ function assertReviewProcedureInstallation(installationPaths) {
74
74
  const { skillsInstallDirectory } = installationPaths;
75
- const allContractPathSegments = [
75
+ const allProcedurePathSegments = [
76
76
  E_CODE_REVIEW_SKILL_NAME,
77
77
  REFERENCE_DIRECTORY_NAME,
78
- PREFLIGHT_PROPOSAL_FILE_NAME,
78
+ REVIEW_PROCEDURE_FILE_NAME,
79
79
  ];
80
- const installedContractPath = join(skillsInstallDirectory, ...allContractPathSegments);
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 sourceContractPath = join(sourceSkillsDirectory, ...allContractPathSegments);
85
+ const sourceProcedurePath = join(sourceSkillsDirectory, ...allProcedurePathSegments);
86
86
 
87
- assert.equal(existsSync(installedContractPath), true);
88
- assert.equal(readFileSync(installedContractPath, 'utf8'), readFileSync(sourceContractPath, 'utf8'));
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
- assertProposalContractInstallation({ skillsInstallDirectory });
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 { writeCodexSkillLoadBlock } from './codex-skill-load-block.mjs';
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 = writeCodexSkillLoadBlock(
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)`);
@@ -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
 
@@ -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 = _repository_origin_slug(repository_root)
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 _repository_origin_slug(repository_root: Path) -> str | None:
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 = _repository_origin_slug(repository_root)
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"
@@ -23,7 +23,6 @@ ALL_CONFIG_DIRECTORY_NAMES = frozenset(
23
23
  "usage_pause_constants",
24
24
  "prototype_scripts_constants",
25
25
  "codex_review_scripts_constants",
26
- "e_code_review_scripts_constants",
27
26
  "dev_env_scripts_constants",
28
27
  "fresh_branch_scripts_constants",
29
28
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "8.11.0",
3
+ "version": "8.12.0",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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 `bin/`. It reports the file when its name
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. Version one reads Python tests only, so a fix proven by a JavaScript or PowerShell test needs a Python test beside it.
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