claude-dev-env 2.25.0 → 2.27.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/agents/clean-coder.md +95 -33
- package/.agents/agents/code-quality-agent.md +85 -24
- package/.agents/agents/pr-description-writer.md +2 -2
- package/.agents/agents/test_agent_frontmatter.py +626 -0
- package/.agents/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +47 -1
- package/.agents/skills/pr-cleanup/SKILL.md +1 -0
- package/.agents/skills/pr-plain-language-cleanup/SKILL.md +93 -0
- package/.agents/skills/pr-title-description/SKILL.md +92 -0
- package/.agents/skills/source-command-sr-loop/SKILL.md +12 -3
- package/bin/ever-shipped-skills.mjs +2 -0
- package/bin/install.agents-home.test.mjs +199 -8
- package/bin/install.mjs +15 -8
- package/bin/install.test.mjs +82 -2
- package/commands/sr-loop.md +10 -1
- package/docs/codex-compatibility.md +19 -0
- package/hooks/blocking/luna_fast_mode_gate.py +160 -0
- package/hooks/blocking/test_luna_fast_mode_gate.py +211 -0
- package/hooks/hooks.json +10 -5
- package/hooks/hooks_constants/AGENTS.md +1 -1
- package/hooks/hooks_constants/luna_fast_mode_gate_constants.py +45 -0
- package/hooks/hooks_constants/mypy_integration_constants.py +14 -3
- package/hooks/hooks_constants/session_start_injector.py +11 -4
- package/hooks/hooks_constants/test_session_start_injector.py +4 -2
- package/hooks/session/AGENTS.md +2 -2
- package/hooks/session/issue_tracker_session_starter.py +3 -2
- package/hooks/session/orchestrator_auto_starter.py +3 -2
- package/hooks/session/task_list_loop_starter.py +10 -1
- package/hooks/session/test_issue_tracker_session_starter.py +3 -1
- package/hooks/session/test_orchestrator_auto_starter.py +5 -3
- package/hooks/session/test_task_list_loop_starter.py +10 -8
- package/hooks/session/test_untracked_repo_detector.py +7 -5
- package/hooks/session/test_working_style_prompt.py +5 -3
- package/hooks/session/untracked_repo_detector.py +10 -1
- package/hooks/session/working_style_prompt.py +10 -1
- package/hooks/validators/AGENTS.md +2 -1
- package/hooks/validators/conftest.py +21 -4
- package/hooks/validators/mypy_integration.py +77 -16
- package/hooks/validators/run_all_validators.py +2 -35
- package/hooks/validators/system_temporary_roots.py +90 -0
- package/hooks/validators/test_directory_exemption_constants.py +1 -1
- package/hooks/validators/test_mypy_integration.py +169 -0
- package/hooks/validators/test_system_temporary_roots.py +93 -0
- package/package.json +1 -1
- package/scripts/codex_compat_materializer.py +4 -2
- package/scripts/tests/test_codex_compat_materializer.py +2 -2
|
@@ -386,6 +386,28 @@ def test_main_headless_flavor_emits_outcome_path_on_stdout(
|
|
|
386
386
|
assert "add_comment_to_pending_review" not in captured.out
|
|
387
387
|
|
|
388
388
|
|
|
389
|
+
def test_code_quality_agent_requires_scoped_intake_and_evidence_gaps() -> None:
|
|
390
|
+
agent_text = _CODE_QUALITY_AGENT_PATH.read_text(encoding="utf-8")
|
|
391
|
+
|
|
392
|
+
required_phrases = (
|
|
393
|
+
"read each existing `AGENTS.md` and `CLAUDE.md`",
|
|
394
|
+
"scoped `AGENTS.md` files as the canonical",
|
|
395
|
+
"`docs/CODE_RULES.md` is a compact projection",
|
|
396
|
+
"`hooks/blocking/code_rules_enforcer.py` is hand-maintained write-time coverage",
|
|
397
|
+
"full diff",
|
|
398
|
+
"resolved base or merge-base",
|
|
399
|
+
"complete changed-file list",
|
|
400
|
+
"PR description",
|
|
401
|
+
"evidence gap",
|
|
402
|
+
"Never infer omitted content",
|
|
403
|
+
)
|
|
404
|
+
for each_phrase in required_phrases:
|
|
405
|
+
assert each_phrase in agent_text
|
|
406
|
+
|
|
407
|
+
assert "## Review intake and policy sources" in agent_text
|
|
408
|
+
assert "Open questions" in agent_text
|
|
409
|
+
|
|
410
|
+
|
|
389
411
|
_PACKAGE_ROOT = _PACKAGE_ROOT_FOR_RUBRICS
|
|
390
412
|
_CODE_QUALITY_AGENT_PATH = _PACKAGE_ROOT / ".agents" / "agents" / "code-quality-agent.md"
|
|
391
413
|
_AUDIT_CONTRACT_PATH = _PACKAGE_ROOT / "_shared" / "pr-loop" / "audit-contract.md"
|
|
@@ -395,7 +417,11 @@ _CATEGORY_Q_LABEL = (
|
|
|
395
417
|
"(terminology, PR-description claims, message-vs-guard)"
|
|
396
418
|
)
|
|
397
419
|
_CATEGORY_Q_RUBRIC_REFERENCE = (
|
|
398
|
-
"
|
|
420
|
+
"`<managed-root>/audit-rubrics/category_rubrics/"
|
|
421
|
+
"category-q-cross-surface-claims.md`"
|
|
422
|
+
" (source fallback: "
|
|
423
|
+
"`packages/claude-dev-env/audit-rubrics/category_rubrics/"
|
|
424
|
+
"category-q-cross-surface-claims.md`)"
|
|
399
425
|
)
|
|
400
426
|
_A_THROUGH_Q_PATTERN = re.compile(r"A[\u2013-]Q")
|
|
401
427
|
_SEVENTEEN_CATEGORIES_PATTERN = re.compile(r"seventeen categor", re.IGNORECASE)
|
|
@@ -417,6 +443,26 @@ def test_code_quality_agent_default_scope_is_a_through_q() -> None:
|
|
|
417
443
|
assert "| Q |" in agent_text
|
|
418
444
|
|
|
419
445
|
|
|
446
|
+
def test_code_quality_agent_contract_is_caller_neutral() -> None:
|
|
447
|
+
agent_text = _CODE_QUALITY_AGENT_PATH.read_text(encoding="utf-8")
|
|
448
|
+
stale_terms = (
|
|
449
|
+
"PR #394",
|
|
450
|
+
"PR #397",
|
|
451
|
+
"May 2026",
|
|
452
|
+
"r3210166636",
|
|
453
|
+
"/bugteam",
|
|
454
|
+
"/pr-converge",
|
|
455
|
+
"/autoconverge",
|
|
456
|
+
"opus",
|
|
457
|
+
"sonnet",
|
|
458
|
+
"model:",
|
|
459
|
+
)
|
|
460
|
+
for stale_term in stale_terms:
|
|
461
|
+
assert stale_term not in agent_text
|
|
462
|
+
assert "The caller provides the diff, audit scope, ID prefix, and output format." in agent_text
|
|
463
|
+
assert "Do not assume a model, caller name, or persistence path." in agent_text
|
|
464
|
+
|
|
465
|
+
|
|
420
466
|
def test_audit_contract_category_schema_includes_q() -> None:
|
|
421
467
|
contract_text = _AUDIT_CONTRACT_PATH.read_text(encoding="utf-8")
|
|
422
468
|
assert '"category": "A | B | C | D | E | F | G | H | I | J | K | L | M | N | O | P | Q"' in (
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pr-plain-language-cleanup
|
|
3
|
+
disable-model-invocation: true
|
|
4
|
+
description: Review every line of every changed PR file and trim words, names, docs, comments, and code while keeping real behavior.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# PR plain-language cleanup
|
|
8
|
+
|
|
9
|
+
## Balance compression and readability
|
|
10
|
+
|
|
11
|
+
Shorten changed prose and make it easier to scan. Both matter.
|
|
12
|
+
|
|
13
|
+
- **Word count:** record before and after counts and the delta. Count changed
|
|
14
|
+
prose only. Exclude code, frontmatter, and unchanged lines.
|
|
15
|
+
- **Readability:** check sentence length, order, headings, jargon, and dense
|
|
16
|
+
blocks. Edit only when the result is easier to read.
|
|
17
|
+
- **Diff churn:** count changed lines. Remove whitespace-only movement and
|
|
18
|
+
reflow-only churn.
|
|
19
|
+
- **Meaning:** preserve behavior, APIs, data, test intent, conditions, warnings,
|
|
20
|
+
error detail, context, and public names.
|
|
21
|
+
|
|
22
|
+
Reflow is allowed only when it materially improves scanability after a real trim.
|
|
23
|
+
Report the four results separately.
|
|
24
|
+
|
|
25
|
+
Review and trim this pull request:
|
|
26
|
+
|
|
27
|
+
<PR LINK>
|
|
28
|
+
|
|
29
|
+
If no link is given, find the clear PR in the current context. If none is clear, ask for one.
|
|
30
|
+
|
|
31
|
+
Read the full PR, full diff, every changed file, changed tests and docs, and all repo rules that apply.
|
|
32
|
+
|
|
33
|
+
Find the changed files. Send one capable agent to each file. If there are more files than agents, use small batches.
|
|
34
|
+
|
|
35
|
+
Each agent owns one file. Read every line. Review code, tests, names, comments, docstrings, strings, notes, Markdown, and examples.
|
|
36
|
+
|
|
37
|
+
Trim filler, repeats, vague words, needless code, needless names, dead notes, jargon, and steps that add no value. Use few words and small words.
|
|
38
|
+
|
|
39
|
+
Keep real behavior, checks, data, error detail, context, tests, and public names unless a safe full rename is clear. Do not change behavior for style. Do not shorten a name when it loses meaning.
|
|
40
|
+
|
|
41
|
+
Follow the repo rules:
|
|
42
|
+
|
|
43
|
+
- Name modules for their capability.
|
|
44
|
+
- Name functions for their action.
|
|
45
|
+
- Name variables for the value they hold.
|
|
46
|
+
- Name classes for what they are.
|
|
47
|
+
- Use capability names for shared code.
|
|
48
|
+
- Keep workflow words on workflow code.
|
|
49
|
+
- Keep comments and docs true to the code.
|
|
50
|
+
- Use real data and real behavior in tests.
|
|
51
|
+
|
|
52
|
+
## Hard limits
|
|
53
|
+
|
|
54
|
+
This is a non-material, text-only trim.
|
|
55
|
+
|
|
56
|
+
DO NOT:
|
|
57
|
+
|
|
58
|
+
- run `e-simplify`, `e-code-review`, or `source-command-sr-loop`;
|
|
59
|
+
- run `pr-cleanup`, `pr-refinement`, `pr-small-cl`, placement audits, extraction audits, or capability-name audits;
|
|
60
|
+
- run review loops, advisor gates, commit gates, promotion gates, or any other extra cleanup;
|
|
61
|
+
- hunt bugs, change behavior, change APIs, change data, add features, or split the PR;
|
|
62
|
+
- turn a text trim into a code fix.
|
|
63
|
+
|
|
64
|
+
Run only the smallest checks needed for the files changed. Keep the repo rules. If a material issue appears, report it and stop.
|
|
65
|
+
|
|
66
|
+
If a change needs another file, report it to the main agent. The main agent owns cross-file changes.
|
|
67
|
+
|
|
68
|
+
After the file agents finish, apply safe edits, fix cross-file names and links, search for old names and wording, and read every changed file and the full diff again.
|
|
69
|
+
|
|
70
|
+
Run only the smallest relevant check for the files changed. Fix failures. Check the final diff against the repo rules.
|
|
71
|
+
|
|
72
|
+
Do not invent behavior, tests, or benefits. Stop and ask only when a change could alter behavior, break a public API, or exceed the PR scope.
|
|
73
|
+
|
|
74
|
+
Commit and push the changes to the PR branch.
|
|
75
|
+
|
|
76
|
+
Return:
|
|
77
|
+
|
|
78
|
+
## What changed
|
|
79
|
+
|
|
80
|
+
Short list of the main trims.
|
|
81
|
+
|
|
82
|
+
## Files checked
|
|
83
|
+
|
|
84
|
+
Every file reviewed.
|
|
85
|
+
|
|
86
|
+
## Verification
|
|
87
|
+
|
|
88
|
+
Checks run and results.
|
|
89
|
+
|
|
90
|
+
## Remaining items
|
|
91
|
+
|
|
92
|
+
Only items that need a user choice.
|
|
93
|
+
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pr-title-description
|
|
3
|
+
description: Review one or more pull requests and write plain titles and descriptions from the full diff.
|
|
4
|
+
disable-model-invocation: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Accept one or more pull request links as arguments. Apply the prompt below to each link.
|
|
8
|
+
|
|
9
|
+
With no argument, use the current task, browser, worktree, or GitHub context to find the pull request or pull requests. If the context does not show a clear pull request, ask for a pull request link.
|
|
10
|
+
|
|
11
|
+
Review this pull request: <PR link>
|
|
12
|
+
|
|
13
|
+
Read the PR and its full diff. Do not use the branch name, commit message, labels, current title, or a shallow summary.
|
|
14
|
+
|
|
15
|
+
Find the main thing this adds for a user, caller, or operator.
|
|
16
|
+
|
|
17
|
+
Write like the reader is smart but knows nothing about this code. Pretend they asked you to explain it like they are five and feel stupid. If a word needs another sentence to explain it, replace the word — or turn it into a tiny story.
|
|
18
|
+
|
|
19
|
+
Your job is not a dry API blurb. Your job is an illustrative before/after a person can picture.
|
|
20
|
+
|
|
21
|
+
Gold voice for titles and “What this adds”:
|
|
22
|
+
|
|
23
|
+
The theme package has a phone icon somewhere. We need to find it.
|
|
24
|
+
|
|
25
|
+
Before: yell “anyone know how to find icons?” and wait for a hand.
|
|
26
|
+
|
|
27
|
+
After: call the finder by its name tag: “icon finder, go.”
|
|
28
|
+
|
|
29
|
+
That’s all “operation id” is — the finder’s name tag.
|
|
30
|
+
|
|
31
|
+
Rules for that voice:
|
|
32
|
+
|
|
33
|
+
Start from a concrete scene (what’s in someone’s hand, what they’re hunting for).
|
|
34
|
+
Prefer Before / After when the change is “how you ask” or “how you look it up.”
|
|
35
|
+
Use picture words: hand it, open, find, check, save, skip, stop, call by name, name tag.
|
|
36
|
+
Prefer spoken results: “found it,” “missing,” “duplicated,” “couldn’t.”
|
|
37
|
+
When the diff uses an abstract word (operation id, registry, envelope, adapter, digest, schema), do one of two things:
|
|
38
|
+
Drop it and say what the person does with their hands, or
|
|
39
|
+
Keep it once, then gloss it in the same breath with a name-tag / mailbox / checklist metaphor — like the “finder’s name tag” line above.
|
|
40
|
+
Never leave jargon unexplained. Never write as if the reader already knows the jargon.
|
|
41
|
+
Add a tiny concrete example in parentheses when a name is abstract (like “the phone icon”).
|
|
42
|
+
Few words. Small words. Clear on first read.
|
|
43
|
+
Return:
|
|
44
|
+
|
|
45
|
+
Recommended title
|
|
46
|
+
|
|
47
|
+
Few words. Small words. Clear on first read.
|
|
48
|
+
Say what you hand it, what it does, and what you get back — or the Before/After of how you ask.
|
|
49
|
+
Prefer picture words and spoken results.
|
|
50
|
+
Good title: “Call the icon finder by its name tag instead of asking around”
|
|
51
|
+
Good title: “Given a theme STP and an asset name, report whether that asset was found, missing, or duplicated”
|
|
52
|
+
Bad title: anything that only says operation id / registry / typed envelope / adapter / digest / schema with no picture.
|
|
53
|
+
Use “Add” only if it helps. Do not force it.
|
|
54
|
+
No vague words like “improve,” “enhance,” or “update.”
|
|
55
|
+
Return one title only.
|
|
56
|
+
Why this title fits
|
|
57
|
+
|
|
58
|
+
1–2 short sentences in the same voice.
|
|
59
|
+
If needed, one Before / After beat, then what you get back.
|
|
60
|
+
If this section is needed to decode the title, rewrite the title.
|
|
61
|
+
Evidence
|
|
62
|
+
|
|
63
|
+
Key files or functions that support the title.
|
|
64
|
+
If you cannot read the PR or full diff, say so and do not propose a title.
|
|
65
|
+
|
|
66
|
+
Then write a short, paste-ready PR description.
|
|
67
|
+
|
|
68
|
+
Use this structure:
|
|
69
|
+
|
|
70
|
+
What this adds
|
|
71
|
+
1–2 short paragraphs in that illustrative voice. Lead with a scene when you can (“The theme package has a phone icon somewhere…”). If the change is a new way to ask, use Before / After. Say what you give it, what it does with its hands, and what you get back. If an abstract machine-word must appear, gloss it immediately (“That’s all X is — …”). Describe the job people can see. Do not describe inner machinery. Mention a check only if it is central to the feature. If there are many changes, keep the main one. Mention others only if they explain the main one.
|
|
72
|
+
|
|
73
|
+
Why
|
|
74
|
+
One short paragraph. Same plain level. Say who needs this and what goes wrong without it (the “yell and wait for a hand” problem). If this is needed to decode the feature, rewrite “What this adds.”
|
|
75
|
+
|
|
76
|
+
Verification
|
|
77
|
+
List only proof from the PR, CI, or your own checks.
|
|
78
|
+
|
|
79
|
+
Reported in the PR
|
|
80
|
+
|
|
81
|
+
…
|
|
82
|
+
Checked by you / CI here
|
|
83
|
+
|
|
84
|
+
…
|
|
85
|
+
Rules:
|
|
86
|
+
|
|
87
|
+
Few words. Small words. Clear on first read. ELI5 I-am-stupid mark.
|
|
88
|
+
Do not invent behavior, tests, or benefits.
|
|
89
|
+
Strip branch names, hashes, draft notes, agent notes, and merge notes.
|
|
90
|
+
No vague words like “improve,” “enhance,” or “update.”
|
|
91
|
+
Illustrative first. Abstract labels only if glossed in the same breath.
|
|
92
|
+
Use the recommended title and paste-ready description to run `gh pr edit <PR link> --title ... --body-file ...`, then run `gh pr view <PR link> --json title,body` to confirm the saved title and body. Report the saved result.
|
|
@@ -13,6 +13,14 @@ Run the converging cleanup loop on the target the user names (a PR URL,
|
|
|
13
13
|
a branch, or blank for the current branch's diff). Each phase invokes an
|
|
14
14
|
existing skill and repeats it until a pass returns zero new findings.
|
|
15
15
|
|
|
16
|
+
### Nit-only terminal
|
|
17
|
+
|
|
18
|
+
When a pass finds only nits, apply them, run scoped checks, commit, and push.
|
|
19
|
+
Treat that phase as clean. Do not run another simplify or review pass without
|
|
20
|
+
the user's request. If later work creates an uncommitted out-of-scope diff,
|
|
21
|
+
stop and report it. Do not inspect, repair, commit, or adopt that diff without
|
|
22
|
+
new user instruction.
|
|
23
|
+
|
|
16
24
|
### Effort
|
|
17
25
|
|
|
18
26
|
Set every model call to `max` effort. If that model has no `max` effort, use
|
|
@@ -34,7 +42,8 @@ stack's design, intent, and changes.
|
|
|
34
42
|
code, not the full repo suite), commit once (commit body via
|
|
35
43
|
`git commit -F <file>`, 10-minute timeout — the pre-commit gate runs its
|
|
36
44
|
own tests), and push to the PR head branch. The PR stays draft.
|
|
37
|
-
3. Repeat the invocation
|
|
45
|
+
3. Repeat the invocation only after substantive findings or a user request.
|
|
46
|
+
Skips are sticky: carry every adjudicated skip
|
|
38
47
|
forward into the next pass as "already adjudicated — do not re-report: ..."
|
|
39
48
|
context, or the loop never converges.
|
|
40
49
|
4. Phase A converges when a full pass returns zero new findings. When the
|
|
@@ -51,8 +60,8 @@ stack's design, intent, and changes.
|
|
|
51
60
|
when the headless run fails: invoke the `e-code-review` skill with
|
|
52
61
|
`low --fix` — same review-and-fix contract, no invocation restriction.
|
|
53
62
|
2. After any pass that changed files: test, commit, push as in Phase A.
|
|
54
|
-
3. Repeat until a pass reports zero findings.
|
|
55
|
-
|
|
63
|
+
3. Repeat until a pass reports zero findings. A nit-only pass ends this phase
|
|
64
|
+
after its fixes are pushed.
|
|
56
65
|
|
|
57
66
|
### Finish
|
|
58
67
|
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
MANAGED_SKILLS_DIRECTORY_NAME,
|
|
26
26
|
} from './install-constants.mjs';
|
|
27
27
|
import { isDirectoryPointerTo } from './publish-directory-pointer.mjs';
|
|
28
|
+
import { resolvePackageManagedDirectory } from './resolve-package-managed-directory.mjs';
|
|
28
29
|
|
|
29
30
|
const THIS_DIRECTORY = dirname(fileURLToPath(import.meta.url));
|
|
30
31
|
const INSTALLER_PATH = join(THIS_DIRECTORY, 'install.mjs');
|
|
@@ -32,6 +33,41 @@ const PACKAGE_DIRECTORY = dirname(THIS_DIRECTORY);
|
|
|
32
33
|
const SHIPPED_SKILL_NAME = 'privacy-hygiene';
|
|
33
34
|
const ELI5_SKILL_NAME = 'eli5';
|
|
34
35
|
const SHIPPED_AGENT_FILE_NAME = 'clean-coder.md';
|
|
36
|
+
const SHIPPED_AGENT_FILE_NAMES = [
|
|
37
|
+
SHIPPED_AGENT_FILE_NAME,
|
|
38
|
+
'code-quality-agent.md',
|
|
39
|
+
'pr-description-writer.md',
|
|
40
|
+
];
|
|
41
|
+
const CLEAN_CODER_POLICY_REFERENCES = [
|
|
42
|
+
[
|
|
43
|
+
'<managed-root>/docs/CODE_RULES.md',
|
|
44
|
+
'packages/claude-dev-env/docs/CODE_RULES.md',
|
|
45
|
+
],
|
|
46
|
+
[
|
|
47
|
+
'<managed-root>/hooks/blocking/code_rules_enforcer.py',
|
|
48
|
+
'packages/claude-dev-env/hooks/blocking/code_rules_enforcer.py',
|
|
49
|
+
],
|
|
50
|
+
[
|
|
51
|
+
'<managed-root>/rules/code-standards.md',
|
|
52
|
+
'packages/claude-dev-env/rules/code-standards.md',
|
|
53
|
+
],
|
|
54
|
+
[
|
|
55
|
+
'<managed-root>/rules/file-global-constants.md',
|
|
56
|
+
'packages/claude-dev-env/rules/file-global-constants.md',
|
|
57
|
+
],
|
|
58
|
+
[
|
|
59
|
+
'<managed-root>/rules/windows-filesystem-safe.md',
|
|
60
|
+
'packages/claude-dev-env/rules/windows-filesystem-safe.md',
|
|
61
|
+
],
|
|
62
|
+
[
|
|
63
|
+
'<managed-root>/rules/gh-cli-conventions.md',
|
|
64
|
+
'packages/claude-dev-env/rules/gh-cli-conventions.md',
|
|
65
|
+
],
|
|
66
|
+
[
|
|
67
|
+
'<managed-root>/rules/plain-illustrative-docstrings.md',
|
|
68
|
+
'packages/claude-dev-env/rules/plain-illustrative-docstrings.md',
|
|
69
|
+
],
|
|
70
|
+
];
|
|
35
71
|
const PERSONAL_SKILL_NAME = 'my-notes';
|
|
36
72
|
const SHARED_DIRECTORY_NAME = '_shared';
|
|
37
73
|
const PR_LOOP_DIRECTORY_NAME = 'pr-loop';
|
|
@@ -41,19 +77,25 @@ const PREFLIGHT_PROPOSAL_POINTER = '@~/.claude/_shared/pr-loop/preflight-proposa
|
|
|
41
77
|
/**
|
|
42
78
|
* @param {string} homeDirectory
|
|
43
79
|
* @param {string[]} extraArguments
|
|
80
|
+
* @param {Record<string, string | undefined>} [environmentOverrides]
|
|
44
81
|
* @returns {string}
|
|
45
82
|
*/
|
|
46
|
-
function runInstaller(homeDirectory, extraArguments) {
|
|
83
|
+
function runInstaller(homeDirectory, extraArguments, environmentOverrides = {}) {
|
|
84
|
+
const installerEnvironment = {
|
|
85
|
+
...process.env,
|
|
86
|
+
HOME: homeDirectory,
|
|
87
|
+
USERPROFILE: homeDirectory,
|
|
88
|
+
GIT_CONFIG_GLOBAL: join(homeDirectory, '.gitconfig'),
|
|
89
|
+
CODEX_HOME: join(homeDirectory, '.codex'),
|
|
90
|
+
...environmentOverrides,
|
|
91
|
+
};
|
|
92
|
+
for (const [eachName, eachValue] of Object.entries(environmentOverrides)) {
|
|
93
|
+
if (eachValue === undefined) delete installerEnvironment[eachName];
|
|
94
|
+
}
|
|
47
95
|
return execFileSync('node', [INSTALLER_PATH, ...extraArguments], {
|
|
48
96
|
cwd: PACKAGE_DIRECTORY,
|
|
49
97
|
encoding: 'utf8',
|
|
50
|
-
env:
|
|
51
|
-
...process.env,
|
|
52
|
-
HOME: homeDirectory,
|
|
53
|
-
USERPROFILE: homeDirectory,
|
|
54
|
-
GIT_CONFIG_GLOBAL: join(homeDirectory, '.gitconfig'),
|
|
55
|
-
CODEX_HOME: join(homeDirectory, '.codex'),
|
|
56
|
-
},
|
|
98
|
+
env: installerEnvironment,
|
|
57
99
|
});
|
|
58
100
|
}
|
|
59
101
|
|
|
@@ -75,6 +117,33 @@ function assertProposalContractInstallation(installationPaths) {
|
|
|
75
117
|
assert.equal(readFileSync(installedContractPath, 'utf8'), readFileSync(sourceContractPath, 'utf8'));
|
|
76
118
|
}
|
|
77
119
|
|
|
120
|
+
/**
|
|
121
|
+
* @param {string} agentFilePath
|
|
122
|
+
* @param {string} layoutName
|
|
123
|
+
* @param {string} managedRoot
|
|
124
|
+
* @returns {string[]}
|
|
125
|
+
*/
|
|
126
|
+
function cleanCoderPolicyReferenceProblems(agentFilePath, layoutName, managedRoot) {
|
|
127
|
+
const agentBody = readFileSync(agentFilePath, 'utf8');
|
|
128
|
+
const missingReferences = [];
|
|
129
|
+
for (const [installedReference, sourceReference] of CLEAN_CODER_POLICY_REFERENCES) {
|
|
130
|
+
const eachReference = layoutName === 'source'
|
|
131
|
+
? sourceReference
|
|
132
|
+
: installedReference;
|
|
133
|
+
const eachTargetPath = sourceReference.replace('packages/claude-dev-env/', '');
|
|
134
|
+
if (!agentBody.includes(eachReference)) {
|
|
135
|
+
missingReferences.push(eachReference + ' is absent');
|
|
136
|
+
}
|
|
137
|
+
const resolvedPath = layoutName === 'source'
|
|
138
|
+
? join(PACKAGE_DIRECTORY, eachTargetPath)
|
|
139
|
+
: join(managedRoot, eachTargetPath);
|
|
140
|
+
if (!existsSync(resolvedPath)) {
|
|
141
|
+
missingReferences.push(layoutName + ': ' + resolvedPath);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return missingReferences.map((reference) => layoutName + ': ' + reference);
|
|
145
|
+
}
|
|
146
|
+
|
|
78
147
|
test('CONTENT_DIRECTORIES omits agents because that tree installs to the agents home', () => {
|
|
79
148
|
assert.equal(CONTENT_DIRECTORIES.includes(MANAGED_AGENTS_DIRECTORY_NAME), false);
|
|
80
149
|
});
|
|
@@ -133,6 +202,18 @@ test('a full install writes skills and agents under .agents and points .claude a
|
|
|
133
202
|
readFileSync(lookupAgentFile, 'utf8'),
|
|
134
203
|
readFileSync(canonicalAgentFile, 'utf8'),
|
|
135
204
|
);
|
|
205
|
+
const sourceAgentFile = join(
|
|
206
|
+
resolvePackageManagedDirectory(
|
|
207
|
+
PACKAGE_DIRECTORY,
|
|
208
|
+
MANAGED_AGENTS_DIRECTORY_NAME,
|
|
209
|
+
),
|
|
210
|
+
SHIPPED_AGENT_FILE_NAME,
|
|
211
|
+
);
|
|
212
|
+
const brokenPolicyReferences = [
|
|
213
|
+
...cleanCoderPolicyReferenceProblems(sourceAgentFile, 'source', claudeHome),
|
|
214
|
+
...cleanCoderPolicyReferenceProblems(canonicalAgentFile, 'installed', claudeHome),
|
|
215
|
+
];
|
|
216
|
+
assert.deepEqual(brokenPolicyReferences, [], 'Clean Coder has broken policy references');
|
|
136
217
|
assert.equal(realpathSync(lookupAgentFile), realpathSync(canonicalAgentFile));
|
|
137
218
|
assert.equal(
|
|
138
219
|
lstatSync(skillsInstallDirectory).isSymbolicLink(),
|
|
@@ -149,6 +230,116 @@ test('a full install writes skills and agents under .agents and points .claude a
|
|
|
149
230
|
}
|
|
150
231
|
});
|
|
151
232
|
|
|
233
|
+
test('real installs place the Clean Coder in each active agents home', () => {
|
|
234
|
+
const runRoot = mkdtempSync(join(tmpdir(), 'cdev-active-roots-'));
|
|
235
|
+
const homeDirectory = join(runRoot, 'home');
|
|
236
|
+
const configRoot = join(runRoot, 'config-profile');
|
|
237
|
+
const explicitRoot = join(runRoot, 'explicit-target');
|
|
238
|
+
const inheritedProfilesRoot = join(runRoot, 'inherited-profiles');
|
|
239
|
+
const inheritedProfileMarker = join(inheritedProfilesRoot, 'untouched.txt');
|
|
240
|
+
mkdirSync(homeDirectory, { recursive: true });
|
|
241
|
+
mkdirSync(inheritedProfilesRoot, { recursive: true });
|
|
242
|
+
writeFileSync(inheritedProfileMarker, 'leave this profile root alone\n');
|
|
243
|
+
try {
|
|
244
|
+
const installCases = [
|
|
245
|
+
{
|
|
246
|
+
name: 'default',
|
|
247
|
+
arguments: ['--only', 'core'],
|
|
248
|
+
environment: { CLAUDE_CONFIG_DIR: undefined },
|
|
249
|
+
managedRoot: join(homeDirectory, '.claude'),
|
|
250
|
+
agentsHome: join(homeDirectory, '.agents'),
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
name: 'CLAUDE_CONFIG_DIR',
|
|
254
|
+
arguments: ['--only', 'core'],
|
|
255
|
+
environment: { CLAUDE_CONFIG_DIR: configRoot },
|
|
256
|
+
managedRoot: configRoot,
|
|
257
|
+
agentsHome: `${configRoot}.agents`,
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
name: 'named profile',
|
|
261
|
+
arguments: ['--profile', 'editor', '--only', 'core'],
|
|
262
|
+
environment: {
|
|
263
|
+
CLAUDE_CONFIG_DIR: undefined,
|
|
264
|
+
LLM_SETTINGS_PROFILES_ROOT: undefined,
|
|
265
|
+
},
|
|
266
|
+
managedRoot: join(homeDirectory, '.claude-profiles', 'editor'),
|
|
267
|
+
agentsHome: join(homeDirectory, '.claude-profiles', 'editor.agents'),
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
name: 'explicit target',
|
|
271
|
+
arguments: ['--target', explicitRoot, '--only', 'core'],
|
|
272
|
+
environment: { CLAUDE_CONFIG_DIR: configRoot },
|
|
273
|
+
managedRoot: explicitRoot,
|
|
274
|
+
agentsHome: `${explicitRoot}.agents`,
|
|
275
|
+
},
|
|
276
|
+
];
|
|
277
|
+
|
|
278
|
+
for (const eachInstallCase of installCases) {
|
|
279
|
+
runInstaller(
|
|
280
|
+
homeDirectory,
|
|
281
|
+
eachInstallCase.arguments,
|
|
282
|
+
{
|
|
283
|
+
LLM_SETTINGS_PROFILES_ROOT: inheritedProfilesRoot,
|
|
284
|
+
...eachInstallCase.environment,
|
|
285
|
+
},
|
|
286
|
+
);
|
|
287
|
+
for (const eachAgentFileName of SHIPPED_AGENT_FILE_NAMES) {
|
|
288
|
+
const installedAgentPath = join(
|
|
289
|
+
eachInstallCase.agentsHome,
|
|
290
|
+
MANAGED_AGENTS_DIRECTORY_NAME,
|
|
291
|
+
eachAgentFileName,
|
|
292
|
+
);
|
|
293
|
+
assert.equal(
|
|
294
|
+
existsSync(installedAgentPath),
|
|
295
|
+
true,
|
|
296
|
+
`${eachInstallCase.name}: agent is under the active agents home`,
|
|
297
|
+
);
|
|
298
|
+
const installedAgentText = readFileSync(installedAgentPath, 'utf8');
|
|
299
|
+
assert.match(installedAgentText, /active managed root/i);
|
|
300
|
+
assert.match(installedAgentText, /active agents home/i);
|
|
301
|
+
assert.match(installedAgentText, /<managed-root>\//);
|
|
302
|
+
assert.match(installedAgentText, /<agents-home>\//);
|
|
303
|
+
}
|
|
304
|
+
const installedCleanCoderPath = join(
|
|
305
|
+
eachInstallCase.agentsHome,
|
|
306
|
+
MANAGED_AGENTS_DIRECTORY_NAME,
|
|
307
|
+
SHIPPED_AGENT_FILE_NAME,
|
|
308
|
+
);
|
|
309
|
+
const policyReferenceProblems = cleanCoderPolicyReferenceProblems(
|
|
310
|
+
installedCleanCoderPath,
|
|
311
|
+
'installed',
|
|
312
|
+
eachInstallCase.managedRoot,
|
|
313
|
+
);
|
|
314
|
+
assert.deepEqual(
|
|
315
|
+
policyReferenceProblems,
|
|
316
|
+
[],
|
|
317
|
+
`${eachInstallCase.name}: Clean Coder has broken policy references`,
|
|
318
|
+
);
|
|
319
|
+
assert.equal(
|
|
320
|
+
isDirectoryPointerTo(
|
|
321
|
+
join(eachInstallCase.managedRoot, MANAGED_AGENTS_DIRECTORY_NAME),
|
|
322
|
+
join(eachInstallCase.agentsHome, MANAGED_AGENTS_DIRECTORY_NAME),
|
|
323
|
+
),
|
|
324
|
+
true,
|
|
325
|
+
`${eachInstallCase.name}: lookup path points to the active agents home`,
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
assert.equal(
|
|
329
|
+
readFileSync(inheritedProfileMarker, 'utf8'),
|
|
330
|
+
'leave this profile root alone\n',
|
|
331
|
+
'an inherited profile root stays untouched',
|
|
332
|
+
);
|
|
333
|
+
assert.equal(
|
|
334
|
+
existsSync(join(inheritedProfilesRoot, 'editor')),
|
|
335
|
+
false,
|
|
336
|
+
'the named profile does not use the inherited profile root',
|
|
337
|
+
);
|
|
338
|
+
} finally {
|
|
339
|
+
rmSync(runRoot, { recursive: true, force: true });
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
|
|
152
343
|
test('a real skills directory from an older install lands in .agents and stays readable through the pointer', () => {
|
|
153
344
|
const homeDirectory = mkdtempSync(join(tmpdir(), 'cdev-agents-relocate-'));
|
|
154
345
|
const claudeHome = join(homeDirectory, '.claude');
|
package/bin/install.mjs
CHANGED
|
@@ -1126,13 +1126,13 @@ function backupHubBeforeOverwrite(destPath, incomingPath, backupName) {
|
|
|
1126
1126
|
}
|
|
1127
1127
|
|
|
1128
1128
|
/**
|
|
1129
|
-
*
|
|
1129
|
+
* Hook script paths the installer manages even though hooks.json
|
|
1130
1130
|
* carries no standalone entry for them. Most were folded into the PreToolUse
|
|
1131
|
-
* dispatcher in Stage 1;
|
|
1132
|
-
*
|
|
1131
|
+
* dispatcher in Stage 1; retired hooks have no current registration.
|
|
1132
|
+
* Each path stays in this set so a reinstall from an older settings shape
|
|
1133
1133
|
* prunes its standalone entry — a folded hook would otherwise double-run
|
|
1134
|
-
* alongside the dispatcher, and
|
|
1135
|
-
*
|
|
1134
|
+
* alongside the dispatcher, and a retired hook's entry would continue to run
|
|
1135
|
+
* from the user's settings.json.
|
|
1136
1136
|
*/
|
|
1137
1137
|
export const FOLDED_HOOK_RELATIVE_PATHS = new Set([
|
|
1138
1138
|
'blocking/write_existing_file_blocker.py',
|
|
@@ -1150,6 +1150,7 @@ export const FOLDED_HOOK_RELATIVE_PATHS = new Set([
|
|
|
1150
1150
|
'blocking/pytest_testpaths_orphan_blocker.py',
|
|
1151
1151
|
'blocking/open_questions_in_plans_blocker.py',
|
|
1152
1152
|
'blocking/md_to_html_blocker.py',
|
|
1153
|
+
'session/untracked_repo_detector.py',
|
|
1153
1154
|
]);
|
|
1154
1155
|
|
|
1155
1156
|
/**
|
|
@@ -1168,14 +1169,19 @@ export const POST_FOLDED_HOOK_RELATIVE_PATHS = new Set([
|
|
|
1168
1169
|
'workflow/md_to_html_companion.py',
|
|
1169
1170
|
]);
|
|
1170
1171
|
|
|
1172
|
+
/** Hook scripts that still ship but no longer run as registered hooks. */
|
|
1173
|
+
export const RETIRED_HOOK_REGISTRATION_RELATIVE_PATHS = new Set([
|
|
1174
|
+
'session/untracked_repo_detector.py',
|
|
1175
|
+
]);
|
|
1176
|
+
|
|
1171
1177
|
/**
|
|
1172
1178
|
* Builds the set of hook script paths this installer manages, each relative to
|
|
1173
1179
|
* the hooks directory (e.g. 'blocking/code_rules_enforcer.py'), parsed from the
|
|
1174
1180
|
* `${CLAUDE_PLUGIN_ROOT}/hooks/<path>` references in hooks.json. Inline
|
|
1175
1181
|
* `python3 -c` commands reference the hooks directory without a script tail and
|
|
1176
1182
|
* contribute nothing. Also includes every path from FOLDED_HOOK_RELATIVE_PATHS
|
|
1177
|
-
* and
|
|
1178
|
-
*
|
|
1183
|
+
* and the retained path sets so a reinstall from an older settings shape prunes
|
|
1184
|
+
* folded and retired entries.
|
|
1179
1185
|
*
|
|
1180
1186
|
* @param {{hooks: object}} hooksConfig Parsed hooks.json.
|
|
1181
1187
|
* @returns {Set<string>} Forward-slash relative script paths under hooks/.
|
|
@@ -1184,6 +1190,7 @@ export function managedHookScriptRelativePaths(hooksConfig) {
|
|
|
1184
1190
|
const relativePaths = new Set([
|
|
1185
1191
|
...FOLDED_HOOK_RELATIVE_PATHS,
|
|
1186
1192
|
...POST_FOLDED_HOOK_RELATIVE_PATHS,
|
|
1193
|
+
...RETIRED_HOOK_REGISTRATION_RELATIVE_PATHS,
|
|
1187
1194
|
]);
|
|
1188
1195
|
const scriptReferencePattern = /\$\{CLAUDE_PLUGIN_ROOT\}\/hooks\/(\S+?\.py)/g;
|
|
1189
1196
|
for (const matcherGroups of Object.values(hooksConfig.hooks)) {
|
|
@@ -1408,7 +1415,7 @@ export function mergeHooksIntoSettings(settings, hooksConfig, pluginRootDir, pyt
|
|
|
1408
1415
|
/\$\{CLAUDE_PLUGIN_ROOT\}/g,
|
|
1409
1416
|
() => pluginRootForward,
|
|
1410
1417
|
);
|
|
1411
|
-
command = command.replace(/^python3
|
|
1418
|
+
command = command.replace(/^python3?(?=\s)/, () => pythonCommand);
|
|
1412
1419
|
return { ...hook, command };
|
|
1413
1420
|
});
|
|
1414
1421
|
const existingIndex = settings.hooks[eventType].findIndex(
|