claude-dev-env 8.1.0 → 8.3.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/poteto-agent.md +6 -0
- package/.agents/agents/test_agent_frontmatter.py +9 -9
- package/.agents/agents/test_pr_description_writer_style.py +1 -1
- package/.agents/agents-archived/ARCHIVE-MANIFEST.md +15 -0
- package/.agents/{agents → agents-archived}/clean-coder.md +4 -4
- package/.agents/{agents → agents-archived}/code-quality-agent.md +5 -5
- package/.agents/{agents → agents-archived}/plan-packet-validator.md +2 -2
- package/.agents/{agents → agents-archived}/session-advisor.md +2 -2
- package/.agents/skills/_shared/end-of-run-gotcha-recommendations.md +2 -2
- package/.agents/skills/cert-classification-rule/SKILL.md +3 -3
- package/.agents/skills/e-code-review/SKILL.md +2 -2
- package/.agents/skills/e-code-review/reference/fix.md +1 -1
- package/.agents/skills/e-code-review/reference/loop.md +1 -1
- package/.agents/skills/e-code-review/reference/medium.md +4 -4
- package/.agents/skills/e-code-review/reference/xhigh.md +5 -5
- package/.agents/skills/everything-search/SKILL.md +3 -3
- package/.agents/skills/grok-spawn/SKILL.md +9 -6
- package/.agents/skills/grok-spawn/reference/flag-profiles.md +12 -8
- package/.agents/skills/issue-tracker/SKILL.md +3 -3
- package/.agents/skills/orchestrator/SKILL.md +19 -18
- package/.agents/skills/orchestrator/reference/consult-the-orchestrator.md +1 -1
- package/.agents/skills/orchestrator/reference/executor-consult-block.md +1 -1
- package/.agents/skills/orchestrator-refresh/SKILL.md +4 -2
- package/.agents/skills/plugin-eval-standalone-skill/SKILL.md +96 -0
- package/.agents/skills/plugin-eval-standalone-skill/reference/graders-and-commands.md +108 -0
- package/.agents/skills/privacy-hygiene/SKILL.md +5 -6
- package/.agents/skills/privacy-hygiene/reference/sweep-procedure.md +5 -6
- package/.agents/skills/pull-request/SKILL.md +14 -5
- package/.agents/skills/pull-request/reference/publication-tasks.md +1 -1
- package/.agents/skills/repairing-hook-boundaries/SKILL.md +1 -1
- package/.agents/skills/syncing-submodules/SKILL.md +1 -1
- package/.agents/skills/team-advisor/SKILL.md +11 -9
- package/.agents/skills/team-advisor/reference/advisor-docs-review.md +28 -1
- package/.agents/skills/usage-pause/SKILL.md +3 -3
- package/_shared/advisor/advisor-protocol.md +7 -3
- package/_shared/advisor/reference/advisor-block.md +8 -1
- package/audit-rubrics/category_rubrics/category-f-silent-failures.md +2 -2
- package/audit-rubrics/category_rubrics/category-i-concurrency.md +1 -1
- package/audit-rubrics/category_rubrics/category-m-producer-consumer-cardinality.md +1 -1
- package/audit-rubrics/category_rubrics/category-n-test-name-scenario-verifier.md +5 -5
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +5 -5
- package/audit-rubrics/category_rubrics/category-p-name-vs-behavior-contract.md +3 -3
- package/audit-rubrics/category_rubrics/category-q-cross-surface-claims.md +1 -1
- package/audit-rubrics/prompts/category-a-api-contracts.md +2 -2
- package/audit-rubrics/prompts/category-b-selector-engine-compat.md +3 -3
- package/audit-rubrics/prompts/category-c-resource-cleanup.md +3 -3
- package/audit-rubrics/prompts/category-d-scoping-and-ordering.md +1 -1
- package/audit-rubrics/prompts/category-e-dead-code.md +3 -3
- package/audit-rubrics/prompts/category-f-silent-failures.md +12 -12
- package/audit-rubrics/prompts/category-g-bounds-and-overflow.md +2 -2
- package/audit-rubrics/prompts/category-h-security-boundaries.md +4 -4
- package/audit-rubrics/prompts/category-i-concurrency.md +6 -6
- package/audit-rubrics/prompts/category-j-code-rules-compliance.md +5 -5
- package/audit-rubrics/prompts/category-m-producer-consumer-cardinality.md +1 -1
- package/audit-rubrics/prompts/category-n-test-name-scenario-verifier.md +7 -7
- package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +2 -2
- package/audit-rubrics/prompts/category-p-name-vs-behavior-contract.md +3 -3
- package/audit-rubrics/prompts/category-q-cross-surface-claims.md +2 -2
- package/bin/ever-shipped-skills.mjs +1 -0
- package/bin/ever-shipped-skills.test.mjs +4 -0
- package/bin/install.agents-home.test.mjs +44 -94
- package/bin/install.mjs +31 -24
- package/bin/install.test.mjs +21 -0
- package/bin/resolve-package-managed-directory.test.mjs +1 -1
- package/bin/subagent-model-policy.test.mjs +15 -8
- package/docs/BDD_SCENARIO_QUALITY.md +2 -2
- package/docs/CODE_RULES.md +7 -7
- package/docs/TEST_QUALITY.md +1 -1
- package/docs/nas-ssh-invocation.md +1 -1
- package/docs/references/advisor-tool.md +2 -2
- package/docs/references/prose-style-enforcement.md +1 -1
- package/docs/references/team-advisor-skill.md +2 -2
- package/docs/references/weak-executor-advisor.md +1 -1
- package/docs/subagent-model-routing.md +6 -5
- package/hooks/blocking/tdd_enforcer.py +2 -3
- package/hooks/blocking/test_bash_post_call_dispatcher.py +7 -24
- package/hooks/blocking/test_code_rules_enforcer_magic_allowlist.py +1 -1
- package/hooks/hooks_constants/bash_post_call_dispatcher_constants.py +0 -3
- package/hooks/hooks_constants/hosted_hook_runner.py +1 -1
- package/hooks/hooks_constants/msys_path_conversion_advisor_constants.py +3 -35
- package/hooks/hooks_constants/test_bash_post_call_dispatcher_constants.py +2 -10
- package/hooks/routing/subagent_model_routing.mjs +15 -0
- package/hooks/routing/subagent_model_routing.test.mjs +58 -25
- package/package.json +1 -1
- package/rules/archiving-agent-config.md +5 -8
- package/rules/git-workflow.md +3 -3
- package/rules/long-horizon-autonomy.md +1 -1
- package/rules/shell-invocation.md +0 -2
- package/rules/subagent-model-policy.json +5 -13
- package/rules/testing.md +0 -2
- package/scripts/active_capability_references.py +74 -19
- package/scripts/banned_prose_words.py +159 -0
- package/scripts/dev_env_scripts_constants/active_capability_constants.py +12 -10
- package/scripts/dev_env_scripts_constants/banned_prose_word_constants.py +95 -0
- package/scripts/dev_env_scripts_constants/grok_worker_constants.py +3 -3
- package/scripts/grok_headless_runner.py +19 -1
- package/scripts/grok_worker_preflight.py +67 -1
- package/scripts/policy_lint/adapter_retired_hook_prose.py +17 -7
- package/scripts/policy_lint/adapters.py +6 -6
- package/scripts/policy_lint/config/constants.py +8 -0
- package/scripts/policy_lint/registry.py +1 -1
- package/scripts/resolve_advisor_model_route.mjs +2 -2
- package/scripts/subagent_model_policy.mjs +4 -1
- package/scripts/sync_to_cursor/rules.py +1 -1
- package/scripts/test_active_capability_references.py +100 -33
- package/scripts/test_grok_headless_runner.py +35 -1
- package/scripts/test_grok_worker_preflight.py +73 -0
- package/scripts/tests/resolve_advisor_model_route.test.mjs +3 -3
- package/scripts/tests/test_banned_prose_words.py +103 -0
- package/scripts/tests/test_engine.py +1 -1
- package/scripts/tests/test_policy_lint_rules_retired_hook_prose.py +46 -0
- package/scripts/tests/test_rules.py +17 -6
- package/system-prompts/software-engineer.xml +7 -7
- package/hooks/advisory/msys_path_conversion_advisor.py +0 -200
- package/hooks/advisory/test_msys_path_conversion_advisor.py +0 -223
- package/hooks/hooks_constants/test_failure_recorder_constants.py +0 -34
- package/hooks/observability/test_failure_recorder.py +0 -221
- package/hooks/observability/test_test_failure_recorder.py +0 -173
- /package/.agents/{agents → agents-archived}/git-commit-crafter.md +0 -0
- /package/.agents/{agents → agents-archived}/issue-tracker.md +0 -0
- /package/.agents/{agents → agents-archived}/pr-description-writer.md +0 -0
- /package/.agents/{agents → agents-archived}/skill-writer-agent.md +0 -0
|
@@ -9,7 +9,7 @@ _SCRIPTS_DIR = Path(__file__).resolve().parent.parent
|
|
|
9
9
|
if str(_SCRIPTS_DIR) not in sys.path:
|
|
10
10
|
sys.path.insert(0, str(_SCRIPTS_DIR))
|
|
11
11
|
|
|
12
|
-
from sync_to_cursor.rules import build_mappings
|
|
12
|
+
from sync_to_cursor.rules import build_mappings, merge_code_standards
|
|
13
13
|
|
|
14
14
|
_PACKAGE_ROOT = _SCRIPTS_DIR.parent
|
|
15
15
|
_SKIPPED_RULE_FILE_NAMES = frozenset({"CLAUDE.md", "AGENTS.md"})
|
|
@@ -94,8 +94,8 @@ def _comment_policy_surfaces(package_root: Path) -> tuple[Path, ...]:
|
|
|
94
94
|
package_root / "AGENTS.md",
|
|
95
95
|
package_root / "docs" / "CODE_RULES.md",
|
|
96
96
|
package_root / "system-prompts" / "software-engineer.xml",
|
|
97
|
-
package_root / ".agents" / "agents" / "clean-coder.md",
|
|
98
|
-
package_root / ".agents" / "agents" / "code-quality-agent.md",
|
|
97
|
+
package_root / ".agents" / "agents-archived" / "clean-coder.md",
|
|
98
|
+
package_root / ".agents" / "agents-archived" / "code-quality-agent.md",
|
|
99
99
|
package_root / "audit-rubrics" / "category_rubrics" / "category-j-code-rules-compliance.md",
|
|
100
100
|
package_root / "audit-rubrics" / "prompts" / "category-j-code-rules-compliance.md",
|
|
101
101
|
package_root / "audit-rubrics" / "category_rubrics" / "category-l-behavior-equivalence.md",
|
|
@@ -119,7 +119,7 @@ def _worker_policy_surfaces(package_root: Path) -> tuple[Path, ...]:
|
|
|
119
119
|
package_root / "AGENTS.md",
|
|
120
120
|
package_root / "docs" / "CODE_RULES.md",
|
|
121
121
|
package_root / "system-prompts" / "software-engineer.xml",
|
|
122
|
-
package_root / ".agents" / "agents" / "clean-coder.md",
|
|
122
|
+
package_root / ".agents" / "agents-archived" / "clean-coder.md",
|
|
123
123
|
package_root / ".agents" / "skills" / "grok-spawn" / "reference" / "worker-briefs.md",
|
|
124
124
|
package_root.parent.parent / ".github" / "copilot-instructions.md",
|
|
125
125
|
)
|
|
@@ -185,14 +185,14 @@ def _category_l_policy_texts() -> tuple[str, str, str]:
|
|
|
185
185
|
|
|
186
186
|
|
|
187
187
|
def _comment_policy_summary_texts() -> tuple[str, str, str]:
|
|
188
|
-
clean_coder = _PACKAGE_ROOT / ".agents" / "agents" / "clean-coder.md"
|
|
188
|
+
clean_coder = _PACKAGE_ROOT / ".agents" / "agents-archived" / "clean-coder.md"
|
|
189
189
|
category_j = (
|
|
190
190
|
_PACKAGE_ROOT
|
|
191
191
|
/ "audit-rubrics"
|
|
192
192
|
/ "category_rubrics"
|
|
193
193
|
/ "category-j-code-rules-compliance.md"
|
|
194
194
|
)
|
|
195
|
-
code_quality = _PACKAGE_ROOT / ".agents" / "agents" / "code-quality-agent.md"
|
|
195
|
+
code_quality = _PACKAGE_ROOT / ".agents" / "agents-archived" / "code-quality-agent.md"
|
|
196
196
|
return tuple(
|
|
197
197
|
each_path.read_text(encoding="utf-8").lower()
|
|
198
198
|
for each_path in (clean_coder, category_j, code_quality)
|
|
@@ -225,3 +225,14 @@ def test_comment_policy_removes_directive_justification_guidance() -> None:
|
|
|
225
225
|
assert "without clear justification" not in react_patterns
|
|
226
226
|
assert "changed `# noqa` directives are removed or resolved" in category_e_prompt
|
|
227
227
|
assert "every `# noqa` on an import line must be justified" not in category_e_prompt
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def test_merge_code_standards_resolves_every_heading_it_names() -> None:
|
|
231
|
+
merged_text = merge_code_standards(
|
|
232
|
+
(
|
|
233
|
+
_PACKAGE_ROOT / "rules" / "code-standards.md",
|
|
234
|
+
_PACKAGE_ROOT / "docs" / "CODE_RULES.md",
|
|
235
|
+
)
|
|
236
|
+
)
|
|
237
|
+
assert "## Reference (full text:" in merged_text
|
|
238
|
+
assert "## COMMENT PRESERVATION" in merged_text
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
inferring are replaced by honest acknowledgment. Second, every recommendation, claim, or piece of advice cites a
|
|
73
73
|
specific source: a file in the current project, an external URL found via web search, a
|
|
74
74
|
named expert or paper, or official documentation. Third, when working from documents, extract
|
|
75
|
-
the
|
|
75
|
+
the text first before analyzing — ground responses in direct quotes, and reference
|
|
76
76
|
the quote when making a point.
|
|
77
77
|
|
|
78
78
|
Creative thinking, brainstorming, and novel ideas are exempt from citation requirements;
|
|
@@ -85,7 +85,7 @@
|
|
|
85
85
|
error message, verify the assumption that produced it, and apply a targeted fix. Commit to
|
|
86
86
|
a viable approach and see it through; switch approaches only when new information directly
|
|
87
87
|
contradicts the original reasoning. Escalate to the user only after investigation is
|
|
88
|
-
complete and a
|
|
88
|
+
complete and a decision point requires their input.
|
|
89
89
|
|
|
90
90
|
Treat OWASP top 10 vulnerability classes — command injection, XSS, SQL injection, and
|
|
91
91
|
related categories — as first-class design criteria. Build code that is inherently resistant
|
|
@@ -115,7 +115,7 @@
|
|
|
115
115
|
Types — complete coverage required:
|
|
116
116
|
- Every parameter and return value carries an explicit type hint
|
|
117
117
|
- Replace Any with the most specific type available
|
|
118
|
-
- Avoid `# type: ignore`; remove it and use a typed boundary or
|
|
118
|
+
- Avoid `# type: ignore`; remove it and use a typed boundary or an explicit type (AGENTS Types)
|
|
119
119
|
|
|
120
120
|
Configuration — one source of truth:
|
|
121
121
|
- Every constant lives in config/ only
|
|
@@ -156,7 +156,7 @@
|
|
|
156
156
|
desired behaviour. Surface assumptions early so they can be confirmed or corrected.
|
|
157
157
|
|
|
158
158
|
Illustrate phase. Explore goals, constraints, and concrete examples with the user. Use
|
|
159
|
-
|
|
159
|
+
scenarios — "given X, when Y happens, the system should Z" — to ground the conversation
|
|
160
160
|
in observable outcomes.
|
|
161
161
|
|
|
162
162
|
Formulate phase. Express behaviour as developer-facing executable specifications using the
|
|
@@ -193,7 +193,7 @@
|
|
|
193
193
|
|
|
194
194
|
Right-sized engineering: functions over classes, concrete over abstract, YAGNI on API
|
|
195
195
|
surface. Abstract base classes for single implementations, dependency injection frameworks,
|
|
196
|
-
CQRS, microservices, and multiple inheritance hierarchies belong only when a
|
|
196
|
+
CQRS, microservices, and multiple inheritance hierarchies belong only when a second
|
|
197
197
|
use case has arrived.
|
|
198
198
|
</scope_discipline>
|
|
199
199
|
|
|
@@ -224,7 +224,7 @@
|
|
|
224
224
|
When multiple tool calls are mutually independent, make all calls in a single response.
|
|
225
225
|
Sequence calls only when a later call requires an earlier call's result. Reading three files:
|
|
226
226
|
call all three Read operations at once. Running independent searches: launch all Grep
|
|
227
|
-
and Glob calls simultaneously. Use
|
|
227
|
+
and Glob calls simultaneously. Use concrete parameter values only. When uncertain whether calls are
|
|
228
228
|
independent, run them sequentially.
|
|
229
229
|
</agent_workflow>
|
|
230
230
|
|
|
@@ -363,7 +363,7 @@
|
|
|
363
363
|
Prefer working in memory over creating scratchpad files. Use variables and tool results
|
|
364
364
|
over writing intermediate data to disk.
|
|
365
365
|
|
|
366
|
-
When a temporary file is
|
|
366
|
+
When a temporary file is needed — a helper script, a test fixture, a debug output
|
|
367
367
|
— track it for cleanup. After each task completes, remove every temporary file, script, or
|
|
368
368
|
helper file created during the task. Leave the working directory cleaner than it was found.
|
|
369
369
|
|
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""PostToolUse context advisory: Git Bash rewrote a ``<rev>:<path>`` argument.
|
|
3
|
-
|
|
4
|
-
This hook never blocks. It watches every Bash call finish and, when git failed
|
|
5
|
-
with either its ambiguous-argument error or its invalid-object-name error and
|
|
6
|
-
the quoted argument carries the marks of MSYS path conversion, adds one loud
|
|
7
|
-
note to the agent's context::
|
|
8
|
-
|
|
9
|
-
=== MSYS PATH CONVERSION (context reminder, never a block) ===
|
|
10
|
-
Git saw: origin\\main;.claude\\skills\\x\\test_run_evals.py
|
|
11
|
-
Git Bash rewrote a <rev>:<path> argument before git saw it, turning the
|
|
12
|
-
colon into a semicolon and the slashes into backslashes.
|
|
13
|
-
Fix: export MSYS_NO_PATHCONV=1 MSYS2_ARG_CONV_EXCL='*'
|
|
14
|
-
Put that export and the git command in the same command.
|
|
15
|
-
|
|
16
|
-
Why a reminder and not a gate: the rewriting happens inside the shell, so the
|
|
17
|
-
command text the agent wrote looks correct and the error names an argument the
|
|
18
|
-
agent never typed. The note arrives at the moment the failure lands, which is
|
|
19
|
-
the only moment the two names line up.
|
|
20
|
-
|
|
21
|
-
Six quiet branches emit nothing: a non-Bash tool, a zero-exit call, a failure
|
|
22
|
-
carrying neither marker, a quoted argument whose closing quote never arrives, a
|
|
23
|
-
quoted argument free of both mangling marks, and a command that already names
|
|
24
|
-
either workaround variable.
|
|
25
|
-
|
|
26
|
-
The verdict rests on the failure text alone and never parses the command, so
|
|
27
|
-
two shapes get the note without deserving it: an argument the agent typed with
|
|
28
|
-
a semicolon or a backslash of its own, and a failing call whose stdout quotes
|
|
29
|
-
one of the two git messages. Both are rare, and the note costs a reader one
|
|
30
|
-
paragraph, so the detector keeps its single read of the failure text.
|
|
31
|
-
|
|
32
|
-
Hosted by ``blocking/bash_post_call_dispatcher.py``, which forwards the
|
|
33
|
-
``hookSpecificOutput.additionalContext`` this hook prints.
|
|
34
|
-
"""
|
|
35
|
-
|
|
36
|
-
from __future__ import annotations
|
|
37
|
-
|
|
38
|
-
import sys
|
|
39
|
-
from pathlib import Path
|
|
40
|
-
|
|
41
|
-
try:
|
|
42
|
-
_hooks_root_directory = str(Path(__file__).resolve().parent.parent)
|
|
43
|
-
if _hooks_root_directory not in sys.path:
|
|
44
|
-
sys.path.insert(0, _hooks_root_directory)
|
|
45
|
-
from hooks_constants.bash_post_call_dispatcher_constants import (
|
|
46
|
-
EXIT_CODE_ERROR_PREFIX,
|
|
47
|
-
)
|
|
48
|
-
from hooks_constants.bash_pre_tool_use_dispatcher_constants import (
|
|
49
|
-
ALL_BASH_ONLY_TOOL_NAMES,
|
|
50
|
-
)
|
|
51
|
-
from hooks_constants.msys_path_conversion_advisor_constants import (
|
|
52
|
-
ADVISORY_HEADER,
|
|
53
|
-
ADVISORY_LINE_SEPARATOR,
|
|
54
|
-
ALL_MANGLED_ARGUMENT_MARKERS,
|
|
55
|
-
ALL_MSYS_MANGLING_CHARACTERS,
|
|
56
|
-
ALL_MSYS_PATH_CONVERSION_VARIABLE_NAMES,
|
|
57
|
-
FIX_LINE,
|
|
58
|
-
MANGLED_ARGUMENT_CLOSING_QUOTE,
|
|
59
|
-
MANGLED_ARGUMENT_LINE_TEMPLATE,
|
|
60
|
-
MANGLING_EXPLANATION_LINE,
|
|
61
|
-
SAME_COMMAND_LINE,
|
|
62
|
-
)
|
|
63
|
-
from hooks_constants.post_tool_use_context import (
|
|
64
|
-
write_post_tool_use_context_to_stdout,
|
|
65
|
-
)
|
|
66
|
-
from hooks_constants.pre_tool_use_stdin import read_hook_input_dictionary_from_stdin
|
|
67
|
-
except ImportError as import_error:
|
|
68
|
-
raise ImportError(
|
|
69
|
-
"msys_path_conversion_advisor is unable to import its dependencies; "
|
|
70
|
-
"ensure the hooks directory is importable."
|
|
71
|
-
) from import_error
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def text_after_first_mangled_argument_marker(response_text: str) -> str | None:
|
|
75
|
-
"""Return what follows the first marker present in a git failure, else None.
|
|
76
|
-
|
|
77
|
-
Git names a mangled ``<rev>:<path>`` argument under two messages, one for a
|
|
78
|
-
path after the colon and one for an absolute path after the colon. The
|
|
79
|
-
markers are walked in order and the first one present wins.
|
|
80
|
-
|
|
81
|
-
Args:
|
|
82
|
-
response_text: The harness response text for the failed call.
|
|
83
|
-
|
|
84
|
-
Returns:
|
|
85
|
-
The text following the opening quote of the first marker present, or
|
|
86
|
-
None when the response carries no marker.
|
|
87
|
-
"""
|
|
88
|
-
for each_marker in ALL_MANGLED_ARGUMENT_MARKERS:
|
|
89
|
-
_, marker_text, text_after_marker = response_text.partition(each_marker)
|
|
90
|
-
if marker_text:
|
|
91
|
-
return text_after_marker
|
|
92
|
-
return None
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def argument_carries_msys_mangling_marks(argument_text: str) -> bool:
|
|
96
|
-
"""Return whether an argument carries a mark left behind by path conversion.
|
|
97
|
-
|
|
98
|
-
Path conversion leaves a backslash or a semicolon in the argument, and that
|
|
99
|
-
is what separates this failure from a revision that is simply absent.
|
|
100
|
-
|
|
101
|
-
Args:
|
|
102
|
-
argument_text: The quoted argument git named in its failure.
|
|
103
|
-
|
|
104
|
-
Returns:
|
|
105
|
-
True when one of those marks is present, else False.
|
|
106
|
-
"""
|
|
107
|
-
return any(each_character in argument_text for each_character in ALL_MSYS_MANGLING_CHARACTERS)
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
def command_names_a_path_conversion_variable(command_text: str) -> bool:
|
|
111
|
-
"""Return whether the command already names a variable that stops conversion.
|
|
112
|
-
|
|
113
|
-
The advisory names two exports, and either one on its own suppresses the
|
|
114
|
-
rewriting, so a command carrying either has nothing left to learn.
|
|
115
|
-
|
|
116
|
-
Args:
|
|
117
|
-
command_text: The Bash command text the agent ran.
|
|
118
|
-
|
|
119
|
-
Returns:
|
|
120
|
-
True when one of those variable names is present, else False.
|
|
121
|
-
"""
|
|
122
|
-
return any(
|
|
123
|
-
each_variable_name in command_text
|
|
124
|
-
for each_variable_name in ALL_MSYS_PATH_CONVERSION_VARIABLE_NAMES
|
|
125
|
-
)
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
def mangled_revision_path_argument(command_text: str, tool_response: object) -> str | None:
|
|
129
|
-
"""Return the argument git reported, when MSYS mangled it, else None.
|
|
130
|
-
|
|
131
|
-
Four conditions all hold before an argument is returned. The harness
|
|
132
|
-
reported a non-zero exit status, which arrives as a string carrying the
|
|
133
|
-
exit-code prefix. Git printed either its ambiguous-argument error or its
|
|
134
|
-
invalid-object-name error, and the quoted argument after it closes. That
|
|
135
|
-
argument carries a path-conversion mark, and the command names neither of
|
|
136
|
-
the two variables the workaround exports.
|
|
137
|
-
|
|
138
|
-
Args:
|
|
139
|
-
command_text: The Bash command text the agent ran.
|
|
140
|
-
tool_response: The harness response for the finished call.
|
|
141
|
-
|
|
142
|
-
Returns:
|
|
143
|
-
The quoted argument word for word, or None to stay quiet.
|
|
144
|
-
"""
|
|
145
|
-
if not isinstance(tool_response, str) or not tool_response.startswith(EXIT_CODE_ERROR_PREFIX):
|
|
146
|
-
return None
|
|
147
|
-
text_after_marker = text_after_first_mangled_argument_marker(tool_response)
|
|
148
|
-
if text_after_marker is None:
|
|
149
|
-
return None
|
|
150
|
-
argument_text, closing_quote, _ = text_after_marker.partition(MANGLED_ARGUMENT_CLOSING_QUOTE)
|
|
151
|
-
if not closing_quote:
|
|
152
|
-
return None
|
|
153
|
-
if not argument_carries_msys_mangling_marks(argument_text):
|
|
154
|
-
return None
|
|
155
|
-
if command_names_a_path_conversion_variable(command_text):
|
|
156
|
-
return None
|
|
157
|
-
return argument_text
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
def build_advisory_context(mangled_argument: str) -> str:
|
|
161
|
-
"""Build the advisory text naming the mangled argument and the two exports.
|
|
162
|
-
|
|
163
|
-
Args:
|
|
164
|
-
mangled_argument: The argument git reported, word for word.
|
|
165
|
-
|
|
166
|
-
Returns:
|
|
167
|
-
The multi-line note the dispatcher forwards as additionalContext.
|
|
168
|
-
"""
|
|
169
|
-
all_lines = [
|
|
170
|
-
ADVISORY_HEADER,
|
|
171
|
-
MANGLED_ARGUMENT_LINE_TEMPLATE.format(argument=mangled_argument),
|
|
172
|
-
MANGLING_EXPLANATION_LINE,
|
|
173
|
-
FIX_LINE,
|
|
174
|
-
SAME_COMMAND_LINE,
|
|
175
|
-
]
|
|
176
|
-
return ADVISORY_LINE_SEPARATOR.join(all_lines)
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
def main() -> None:
|
|
180
|
-
"""Add the MSYS path-conversion note after a git call the shell mangled.
|
|
181
|
-
|
|
182
|
-
Reads the PostToolUse payload from stdin. Every quiet branch returns with
|
|
183
|
-
no output, so this hook can never alter or block a tool call.
|
|
184
|
-
"""
|
|
185
|
-
hook_payload = read_hook_input_dictionary_from_stdin()
|
|
186
|
-
if hook_payload is None or hook_payload.get("tool_name") not in ALL_BASH_ONLY_TOOL_NAMES:
|
|
187
|
-
return
|
|
188
|
-
tool_input = hook_payload.get("tool_input")
|
|
189
|
-
command_text = tool_input.get("command", "") if isinstance(tool_input, dict) else ""
|
|
190
|
-
if not isinstance(command_text, str):
|
|
191
|
-
return
|
|
192
|
-
mangled_argument = mangled_revision_path_argument(
|
|
193
|
-
command_text, hook_payload.get("tool_response")
|
|
194
|
-
)
|
|
195
|
-
if mangled_argument is not None:
|
|
196
|
-
write_post_tool_use_context_to_stdout(build_advisory_context(mangled_argument))
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
if __name__ == "__main__":
|
|
200
|
-
main()
|
|
@@ -1,223 +0,0 @@
|
|
|
1
|
-
"""Behavior tests for the MSYS path-conversion advisory hook.
|
|
2
|
-
|
|
3
|
-
The hook is a PostToolUse observer on Bash. It never blocks. These tests pin
|
|
4
|
-
which failed git call wakes it, which lookalike failures stay quiet, and what
|
|
5
|
-
the advisory text says.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from __future__ import annotations
|
|
9
|
-
|
|
10
|
-
import io
|
|
11
|
-
import json
|
|
12
|
-
import sys
|
|
13
|
-
from pathlib import Path
|
|
14
|
-
|
|
15
|
-
import pytest
|
|
16
|
-
|
|
17
|
-
try:
|
|
18
|
-
advisory_directory = str(Path(__file__).resolve().parent)
|
|
19
|
-
if advisory_directory not in sys.path:
|
|
20
|
-
sys.path.insert(0, advisory_directory)
|
|
21
|
-
import msys_path_conversion_advisor
|
|
22
|
-
except ImportError as import_error:
|
|
23
|
-
raise ImportError(
|
|
24
|
-
"test_msys_path_conversion_advisor: cannot import its sibling modules; "
|
|
25
|
-
"ensure the advisory directory is importable."
|
|
26
|
-
) from import_error
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
_MANGLED_ARGUMENT = r"origin\main;.claude\skills\x\test_run_evals.py"
|
|
30
|
-
_MANGLED_FAILURE_RESPONSE = (
|
|
31
|
-
"Error: Exit code 128\n"
|
|
32
|
-
r"fatal: ambiguous argument 'origin\main;.claude\skills\x\test_run_evals.py': "
|
|
33
|
-
"unknown revision or path not in the working tree."
|
|
34
|
-
)
|
|
35
|
-
_MISSING_REVISION_RESPONSE = (
|
|
36
|
-
"Error: Exit code 128\n"
|
|
37
|
-
"fatal: ambiguous argument 'origin/nope:packages/app/main.py': "
|
|
38
|
-
"unknown revision or path not in the working tree."
|
|
39
|
-
)
|
|
40
|
-
_MANGLED_OBJECT_NAME_RESPONSE = (
|
|
41
|
-
"Error: Exit code 128\n" + r"fatal: invalid object name 'origin\main;C'."
|
|
42
|
-
)
|
|
43
|
-
_MISSING_OBJECT_NAME_RESPONSE = (
|
|
44
|
-
"Error: Exit code 128\nfatal: invalid object name 'nosuchbranch'."
|
|
45
|
-
)
|
|
46
|
-
_SHOW_COMMAND = "git show origin/main:.claude/skills/x/test_run_evals.py"
|
|
47
|
-
_ABSOLUTE_PATH_SHOW_COMMAND = "git show origin/main:/nope"
|
|
48
|
-
_MISSING_OBJECT_SHOW_COMMAND = "git show nosuchbranch:AGENTS.md"
|
|
49
|
-
_EXPORT_LINE = "export MSYS_NO_PATHCONV=1 MSYS2_ARG_CONV_EXCL='*'"
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def _payload(command: str, tool_response: object) -> str:
|
|
53
|
-
return json.dumps(
|
|
54
|
-
{
|
|
55
|
-
"session_id": "msys-session",
|
|
56
|
-
"cwd": "C:/repo",
|
|
57
|
-
"tool_name": "Bash",
|
|
58
|
-
"tool_input": {"command": command},
|
|
59
|
-
"tool_response": tool_response,
|
|
60
|
-
}
|
|
61
|
-
)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def _run_main(
|
|
65
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
66
|
-
capsys: pytest.CaptureFixture[str],
|
|
67
|
-
payload_text: str,
|
|
68
|
-
) -> str:
|
|
69
|
-
monkeypatch.setattr(sys, "stdin", io.StringIO(payload_text))
|
|
70
|
-
msys_path_conversion_advisor.main()
|
|
71
|
-
return capsys.readouterr().out
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def test_should_return_the_mangled_argument_word_for_word() -> None:
|
|
75
|
-
mangled_argument = msys_path_conversion_advisor.mangled_revision_path_argument(
|
|
76
|
-
_SHOW_COMMAND, _MANGLED_FAILURE_RESPONSE
|
|
77
|
-
)
|
|
78
|
-
|
|
79
|
-
assert mangled_argument == r"origin\main;.claude\skills\x\test_run_evals.py"
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
def test_should_return_the_mangled_argument_from_an_invalid_object_name_failure() -> None:
|
|
83
|
-
mangled_argument = msys_path_conversion_advisor.mangled_revision_path_argument(
|
|
84
|
-
_ABSOLUTE_PATH_SHOW_COMMAND, _MANGLED_OBJECT_NAME_RESPONSE
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
assert mangled_argument == r"origin\main;C"
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
def test_should_stay_quiet_when_the_object_name_failure_names_an_unmangled_argument() -> None:
|
|
91
|
-
assert (
|
|
92
|
-
msys_path_conversion_advisor.mangled_revision_path_argument(
|
|
93
|
-
_MISSING_OBJECT_SHOW_COMMAND, _MISSING_OBJECT_NAME_RESPONSE
|
|
94
|
-
)
|
|
95
|
-
is None
|
|
96
|
-
)
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
def test_should_stay_quiet_when_the_command_succeeded() -> None:
|
|
100
|
-
zero_exit_response = {"stdout": "", "stderr": ""}
|
|
101
|
-
|
|
102
|
-
assert (
|
|
103
|
-
msys_path_conversion_advisor.mangled_revision_path_argument(
|
|
104
|
-
_SHOW_COMMAND, zero_exit_response
|
|
105
|
-
)
|
|
106
|
-
is None
|
|
107
|
-
)
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
def test_should_stay_quiet_when_the_quoted_argument_is_unmangled() -> None:
|
|
111
|
-
assert (
|
|
112
|
-
msys_path_conversion_advisor.mangled_revision_path_argument(
|
|
113
|
-
"git show origin/nope:packages/app/main.py", _MISSING_REVISION_RESPONSE
|
|
114
|
-
)
|
|
115
|
-
is None
|
|
116
|
-
)
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
def test_should_stay_quiet_when_the_command_already_exports_the_workaround() -> None:
|
|
120
|
-
already_fixed_command = (
|
|
121
|
-
"export MSYS_NO_PATHCONV=1 MSYS2_ARG_CONV_EXCL='*' && "
|
|
122
|
-
"git show origin/main:.claude/skills/x/test_run_evals.py"
|
|
123
|
-
)
|
|
124
|
-
|
|
125
|
-
assert (
|
|
126
|
-
msys_path_conversion_advisor.mangled_revision_path_argument(
|
|
127
|
-
already_fixed_command, _MANGLED_FAILURE_RESPONSE
|
|
128
|
-
)
|
|
129
|
-
is None
|
|
130
|
-
)
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
def test_should_stay_quiet_when_the_command_exports_only_the_argument_exclusion() -> None:
|
|
134
|
-
argument_exclusion_only_command = (
|
|
135
|
-
"export MSYS2_ARG_CONV_EXCL='*' && "
|
|
136
|
-
"git show origin/main:.claude/skills/x/test_run_evals.py"
|
|
137
|
-
)
|
|
138
|
-
|
|
139
|
-
assert (
|
|
140
|
-
msys_path_conversion_advisor.mangled_revision_path_argument(
|
|
141
|
-
argument_exclusion_only_command, _MANGLED_FAILURE_RESPONSE
|
|
142
|
-
)
|
|
143
|
-
is None
|
|
144
|
-
)
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
def test_should_stay_quiet_without_the_ambiguous_argument_marker() -> None:
|
|
148
|
-
other_failure = "Error: Exit code 1\nfatal: not a git repository"
|
|
149
|
-
|
|
150
|
-
assert (
|
|
151
|
-
msys_path_conversion_advisor.mangled_revision_path_argument(_SHOW_COMMAND, other_failure)
|
|
152
|
-
is None
|
|
153
|
-
)
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
def test_should_stay_quiet_when_the_quoted_argument_never_closes() -> None:
|
|
157
|
-
unterminated_failure = (
|
|
158
|
-
"Error: Exit code 128\n" + r"fatal: ambiguous argument 'origin\main;.claude"
|
|
159
|
-
)
|
|
160
|
-
|
|
161
|
-
assert (
|
|
162
|
-
msys_path_conversion_advisor.mangled_revision_path_argument(
|
|
163
|
-
_SHOW_COMMAND, unterminated_failure
|
|
164
|
-
)
|
|
165
|
-
is None
|
|
166
|
-
)
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
def test_advisory_should_name_the_argument_and_the_export_line() -> None:
|
|
170
|
-
advisory_text = msys_path_conversion_advisor.build_advisory_context(_MANGLED_ARGUMENT)
|
|
171
|
-
|
|
172
|
-
assert advisory_text.startswith("=== MSYS PATH CONVERSION")
|
|
173
|
-
assert "never a block" in advisory_text
|
|
174
|
-
assert r"origin\main;.claude\skills\x\test_run_evals.py" in advisory_text
|
|
175
|
-
assert _EXPORT_LINE in advisory_text
|
|
176
|
-
assert "Git Bash rewrote a <rev>:<path> argument" in advisory_text
|
|
177
|
-
assert "same command" in advisory_text
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
def test_main_should_emit_additional_context_after_a_mangled_failure(
|
|
181
|
-
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
182
|
-
) -> None:
|
|
183
|
-
stdout_text = _run_main(monkeypatch, capsys, _payload(_SHOW_COMMAND, _MANGLED_FAILURE_RESPONSE))
|
|
184
|
-
|
|
185
|
-
payload = json.loads(stdout_text)
|
|
186
|
-
assert payload["hookSpecificOutput"]["hookEventName"] == "PostToolUse"
|
|
187
|
-
additional_context = str(payload["hookSpecificOutput"]["additionalContext"])
|
|
188
|
-
assert _EXPORT_LINE in additional_context
|
|
189
|
-
assert r"origin\main;.claude\skills\x\test_run_evals.py" in additional_context
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
def test_main_should_stay_quiet_for_a_non_bash_tool(
|
|
193
|
-
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
194
|
-
) -> None:
|
|
195
|
-
payload = json.loads(_payload(_SHOW_COMMAND, _MANGLED_FAILURE_RESPONSE))
|
|
196
|
-
payload["tool_name"] = "PowerShell"
|
|
197
|
-
|
|
198
|
-
assert _run_main(monkeypatch, capsys, json.dumps(payload)) == ""
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
def test_should_return_an_argument_whose_only_mangling_mark_is_the_semicolon() -> None:
|
|
202
|
-
semicolon_only_response = (
|
|
203
|
-
"Error: Exit code 128\nfatal: invalid object name 'HEAD;.gitignore'."
|
|
204
|
-
)
|
|
205
|
-
|
|
206
|
-
mangled_argument = msys_path_conversion_advisor.mangled_revision_path_argument(
|
|
207
|
-
"git show HEAD:.gitignore", semicolon_only_response
|
|
208
|
-
)
|
|
209
|
-
|
|
210
|
-
assert mangled_argument == "HEAD;.gitignore"
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
def test_should_stay_quiet_when_the_command_exports_only_the_path_conversion_switch() -> None:
|
|
214
|
-
path_conversion_only_command = (
|
|
215
|
-
"export MSYS_NO_PATHCONV=1 && git show origin/main:.claude/skills/x/test_run_evals.py"
|
|
216
|
-
)
|
|
217
|
-
|
|
218
|
-
assert (
|
|
219
|
-
msys_path_conversion_advisor.mangled_revision_path_argument(
|
|
220
|
-
path_conversion_only_command, _MANGLED_FAILURE_RESPONSE
|
|
221
|
-
)
|
|
222
|
-
is None
|
|
223
|
-
)
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
"""Constants for the failing-test-run PostToolUse recorder hook.
|
|
2
|
-
|
|
3
|
-
Holds the cheap substring check the hook runs before any shell parsing, the
|
|
4
|
-
chaining and redirection operator
|
|
5
|
-
tokens that keep the recognizer to one unchained command, the pytest program
|
|
6
|
-
and Python interpreter basenames, and the flag-token marker the path
|
|
7
|
-
extractor uses to skip an option rather than an existing test file argument.
|
|
8
|
-
|
|
9
|
-
The pytest-program and interpreter recognition here is deliberately its own small, stdlib-only
|
|
10
|
-
check rather than a reuse of ``piped_pytest_blocker_constants`` /
|
|
11
|
-
``pytest_invocation``: those solve the harder pipe-and-wrapper problem and
|
|
12
|
-
pull in a much larger transitive import for every Bash call regardless of
|
|
13
|
-
content, working against the point of the recognizer's own cheap prefilter.
|
|
14
|
-
"""
|
|
15
|
-
|
|
16
|
-
from __future__ import annotations
|
|
17
|
-
|
|
18
|
-
import re
|
|
19
|
-
|
|
20
|
-
BASH_TOOL_NAME = "Bash"
|
|
21
|
-
|
|
22
|
-
ALL_PYTEST_MENTION_SUBSTRINGS: tuple[str, ...] = ("pytest", "py.test")
|
|
23
|
-
OPTION_TOKEN_PREFIX: str = "-"
|
|
24
|
-
NODE_ID_FILE_PATH_SEPARATOR: str = "::"
|
|
25
|
-
ALL_CHAINING_OPERATOR_SUBSTRINGS: tuple[str, ...] = ("&&", "||", ";", "|", "\n")
|
|
26
|
-
ALL_REDIRECTION_TOKENS: frozenset[str] = frozenset(
|
|
27
|
-
{">", ">>", "<", "<<", "2>&1", "1>&2", "2>", "1>"}
|
|
28
|
-
)
|
|
29
|
-
ALL_PYTEST_PROGRAM_BASENAMES: frozenset[str] = frozenset({"pytest", "py.test"})
|
|
30
|
-
MODULE_RUN_FLAG: str = "-m"
|
|
31
|
-
PYTEST_MODULE_NAME: str = "pytest"
|
|
32
|
-
PYTHON_INTERPRETER_BASENAME_PATTERN: re.Pattern[str] = re.compile(
|
|
33
|
-
r"^(?:python|py|pypy)[0-9.]*(?:\.exe)?$"
|
|
34
|
-
)
|