claude-dev-env 2.4.0 → 2.5.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/CLAUDE.md +53 -49
- package/_shared/pr-loop/scripts/_claude_permissions_common.py +84 -0
- package/_shared/pr-loop/scripts/code_rules_gate.py +4 -2
- package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +306 -306
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/claude_permissions_constants.py +44 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/copilot_quota_constants.py +24 -24
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/stale_worktree_rule_sweep_constants.py +107 -107
- package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +290 -48
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +42 -2
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +36 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +100 -1
- package/_shared/pr-loop/scripts/tests/test_fix_hookspath.py +497 -497
- package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +311 -2
- package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep.py +301 -301
- package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep_constants.py +85 -85
- package/_shared/pr-loop/worker-spawn.md +1 -1
- package/agents/CLAUDE.md +2 -1
- package/agents/caveman.md +0 -1
- package/agents/clasp-deployment-orchestrator.md +0 -1
- package/agents/clean-coder.md +0 -1
- package/agents/code-advisor.md +0 -1
- package/agents/code-quality-agent.md +1 -2
- package/agents/code-verifier.md +0 -1
- package/agents/deep-research.md +0 -1
- package/agents/docs-agent.md +0 -1
- package/agents/git-commit-crafter.md +0 -1
- package/agents/issue-tracker.md +42 -0
- package/agents/plan-packet-validator.md +0 -1
- package/agents/pr-description-writer.md +0 -1
- package/agents/test_agent_frontmatter.py +67 -18
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +143 -141
- package/bin/CLAUDE.md +68 -5
- package/bin/ever-shipped-skills.mjs +1 -0
- package/bin/install-constants.mjs +88 -0
- package/bin/install.mjs +1138 -114
- package/bin/install.prune.test.mjs +869 -19
- package/bin/install.test.mjs +906 -2
- package/commands/implement.md +1 -1
- package/commands/right-size.md +1 -1
- package/docs/CLAUDE.md +1 -0
- package/docs/host-pool-health-monitor.md +102 -0
- package/docs/references/CLAUDE.md +4 -2
- package/docs/references/advisor-tool.md +13 -0
- package/docs/references/code-review-enforcement.md +10 -0
- package/docs/references/team-advisor-skill.md +14 -0
- package/hooks/blocking/CLAUDE.md +1 -0
- package/hooks/blocking/code_review_pr_create_gate.py +7 -3
- package/hooks/blocking/code_review_push_gate.py +9 -4
- package/hooks/blocking/code_review_stamp_directory_write_blocker.py +8 -0
- package/hooks/blocking/config/__init__.py +5 -5
- package/hooks/blocking/config/code_review_enforcement_constants.py +4 -1
- package/hooks/blocking/config/test_code_review_enforcement_constants.py +5 -0
- package/hooks/blocking/config/verified_commit_constants.py +160 -159
- package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +256 -0
- package/hooks/blocking/pre_tool_use_dispatcher.py +24 -24
- package/hooks/blocking/test_code_review_pr_create_gate.py +14 -0
- package/hooks/blocking/test_code_review_push_gate.py +16 -0
- package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +19 -0
- package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +231 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +10 -1
- package/hooks/blocking/test_verdict_directory_write_blocker.py +808 -808
- package/hooks/blocking/test_verification_verdict_store.py +54 -0
- package/hooks/blocking/test_verified_commit_gate.py +581 -581
- package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +131 -131
- package/hooks/blocking/verdict_directory_write_blocker.py +687 -687
- package/hooks/blocking/verification_verdict_store.py +1039 -1036
- package/hooks/blocking/verified_commit_message_accuracy_blocker.py +167 -167
- package/hooks/blocking/verifier_verdict_minter.py +280 -280
- package/hooks/git-hooks/test_pre_push.py +25 -0
- package/hooks/hooks.json +10 -0
- package/hooks/hooks_constants/CLAUDE.md +2 -1
- package/hooks/hooks_constants/enter_worktree_prefetch_constants.py +18 -18
- package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +48 -0
- package/hooks/hooks_constants/ruff_integration_constants.py +16 -0
- package/hooks/lifecycle/enter_worktree_origin_prefetch.py +163 -146
- package/hooks/lifecycle/test_enter_worktree_origin_prefetch.py +185 -178
- package/hooks/pyproject.toml +1 -0
- package/hooks/validators/CLAUDE.md +1 -0
- package/hooks/validators/config/__init__.py +0 -0
- package/hooks/validators/config/directory_exemption_constants.py +183 -0
- package/hooks/validators/config/test_directory_exemption_constants.py +21 -0
- package/hooks/validators/conftest.py +4 -0
- package/hooks/validators/ruff_integration.py +49 -5
- package/hooks/validators/run_all_validators.py +206 -9
- package/hooks/validators/test_directory_exemption_constants.py +185 -0
- package/hooks/validators/test_python_antipattern_checks.py +110 -5
- package/hooks/validators/test_ruff_integration.py +92 -1
- package/hooks/validators/test_run_all_validators.py +115 -68
- package/hooks/validators/test_run_all_validators_pretooluse.py +159 -1
- package/package.json +10 -2
- package/rules/CLAUDE.md +1 -0
- package/rules/docstring-prose-matches-implementation.md +45 -44
- package/rules/state-what-is.md +25 -0
- package/rules/verified-commit-gate-skip.md +1 -1
- package/scripts/CLAUDE.md +1 -0
- package/scripts/Capture-PoolHealth.ps1 +410 -0
- package/scripts/_code_review_test_support.py +404 -0
- package/scripts/claude_chain_runner.py +141 -1
- package/scripts/conftest.py +16 -1
- package/scripts/dev_env_scripts_constants/CLAUDE.md +1 -1
- package/scripts/dev_env_scripts_constants/claude_chain_constants.py +9 -0
- package/scripts/resolve_worker_spawn.py +626 -626
- package/scripts/spawn_grok_batch.py +672 -672
- package/scripts/test_claude_chain_runner.py +131 -0
- package/scripts/test_invoke_code_review_chain.py +70 -0
- package/scripts/test_invoke_code_review_cli.py +192 -0
- package/scripts/test_invoke_code_review_contract.py +256 -0
- package/scripts/test_invoke_code_review_git.py +123 -0
- package/scripts/test_invoke_code_review_mode.py +99 -0
- package/scripts/test_resolve_worker_spawn.py +1014 -1014
- package/skills/CLAUDE.md +2 -0
- package/skills/auditing-claude-config/SKILL.md +114 -114
- package/skills/autoconverge/SKILL.md +427 -427
- package/skills/autoconverge/reference/convergence.md +24 -3
- package/skills/autoconverge/workflow/CLAUDE.md +1 -0
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +3 -3
- package/skills/autoconverge/workflow/converge.contract.test.mjs +1263 -1263
- package/skills/autoconverge/workflow/converge.mjs +167 -0
- package/skills/autoconverge/workflow/converge.p2-advance.test.mjs +202 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a11d903476b803493.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a26213978adeef6fb.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a3def0d15ed9d9110.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a41f41b1b708ee3b7.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a758b880abecc3ff7.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a8897b89656b1bd16.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-abd463d744a1437bc.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ad19d027ae8ee1816.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +265 -265
- package/skills/closeout/SKILL.md +33 -50
- package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +8 -0
- package/skills/codex-review/scripts/run_codex_review.py +233 -1
- package/skills/codex-review/scripts/test_run_codex_review.py +189 -0
- package/skills/condensing-instructions/SKILL.md +81 -0
- package/skills/copilot-review/SKILL.md +119 -119
- package/skills/e-code-review/SKILL.md +52 -0
- package/skills/e-code-review/reference/fix.md +54 -0
- package/skills/e-code-review/reference/loop.md +43 -0
- package/skills/e-code-review/reference/low.md +57 -0
- package/skills/e-code-review/reference/medium.md +153 -0
- package/skills/e-code-review/reference/xhigh.md +182 -0
- package/skills/e-simplify/SKILL.md +97 -0
- package/skills/issue-tracker/SKILL.md +92 -0
- package/skills/issue-tracker/reference/epic-and-sub-issue-model.md +55 -0
- package/skills/issue-tracker/reference/handoff-schema.md +64 -0
- package/skills/issue-tracker/reference/operation-matrix.md +41 -0
- package/skills/orchestrator/SKILL.md +162 -21
- package/skills/orchestrator/scripts/status_gate.py +625 -0
- package/skills/orchestrator/scripts/status_gate_constants/__init__.py +1 -0
- package/skills/orchestrator/scripts/status_gate_constants/config/__init__.py +1 -0
- package/skills/orchestrator/scripts/status_gate_constants/config/constants.py +47 -0
- package/skills/orchestrator/scripts/test_status_gate.py +439 -0
- package/skills/orchestrator-refresh/SKILL.md +110 -35
- package/skills/plan-to-pr/SKILL.md +155 -0
- package/skills/plan-to-pr/reference/final-validation-tasks.md +15 -0
- package/skills/plan-to-pr/reference/model-routing.md +36 -0
- package/skills/plan-to-pr/reference/packet-contract.md +43 -0
- package/skills/plan-to-pr/reference/packet-schema.json +57 -0
- package/skills/plan-to-pr/reference/process-inventory.md +22 -0
- package/skills/plan-to-pr/reference/review-loop.md +33 -0
- package/skills/plan-to-pr/reference/run-record.schema.json +27 -0
- package/skills/plan-to-pr/reference/self-audit-tasks.md +15 -0
- package/skills/plan-to-pr/reference/task-seeds.md +14 -0
- package/skills/plan-to-pr/reference/task-ticket.md +38 -0
- package/skills/plan-to-pr/scripts/config/__init__.py +1 -0
- package/skills/plan-to-pr/scripts/config/constants.py +193 -0
- package/skills/plan-to-pr/scripts/create_packet.py +173 -0
- package/skills/plan-to-pr/scripts/test_create_packet.py +102 -0
- package/skills/plan-to-pr/scripts/test_validate_packet.py +256 -0
- package/skills/plan-to-pr/scripts/test_validate_protocol.py +135 -0
- package/skills/plan-to-pr/scripts/test_validate_run.py +158 -0
- package/skills/plan-to-pr/scripts/validate_packet.py +655 -0
- package/skills/plan-to-pr/scripts/validate_protocol.py +622 -0
- package/skills/plan-to-pr/scripts/validate_run.py +173 -0
- package/skills/plan-to-pr/test_skill_contract.py +207 -0
- package/skills/plan-to-pr/test_task_ticket_contract.py +151 -0
- package/skills/pr-converge/SKILL.md +472 -469
- package/skills/pr-converge/reference/examples.md +3 -3
- package/skills/pr-converge/reference/fix-protocol.md +1 -1
- package/skills/pr-converge/reference/ground-rules.md +7 -4
- package/skills/pr-converge/reference/multi-pr-orchestration.md +4 -1
- package/skills/pr-converge/reference/per-tick.md +5 -5
- package/skills/pr-converge/reference/progress-checklist.md +1 -1
- package/skills/pr-converge/scripts/check_convergence_gates.py +279 -279
- package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -507
- package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -84
- package/skills/pr-converge/test_step5_host_branch.py +1 -1
- package/skills/pr-fix-protocol/SKILL.md +1 -1
- package/skills/privacy-hygiene/SKILL.md +68 -68
- package/skills/prototype/workflows/promotion.md +1 -1
- package/skills/release-notes-html/SKILL.md +164 -0
- package/skills/task-build/CLAUDE.md +8 -7
- package/skills/task-build/SKILL.md +16 -8
- package/skills/task-build/reference/tool-routing.md +19 -0
- package/scripts/test_invoke_code_review.py +0 -966
- package/skills/closeout/reference/issue-body-templates.md +0 -108
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
"""Shared fixtures and seams for the ``/code-review`` invoker test suite.
|
|
2
|
+
|
|
3
|
+
Every ``test_invoke_code_review_*`` module imports its fixture values, its
|
|
4
|
+
chain-outcome builders, its runner shortcuts, and its seam installer from here
|
|
5
|
+
so the behavioral groups stay small and none of the shared support is copied
|
|
6
|
+
between them.
|
|
7
|
+
|
|
8
|
+
This module inserts the scripts directory on ``sys.path`` before its flat
|
|
9
|
+
import list, so the invoker and its constants package resolve by bare name
|
|
10
|
+
whether or not pytest's conftest has already registered the directory.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import subprocess
|
|
16
|
+
import sys
|
|
17
|
+
from collections.abc import Callable, Sequence
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
import pytest
|
|
22
|
+
|
|
23
|
+
_SCRIPTS_DIRECTORY = str(Path(__file__).resolve().parent)
|
|
24
|
+
if _SCRIPTS_DIRECTORY not in sys.path:
|
|
25
|
+
sys.path.insert(0, _SCRIPTS_DIRECTORY)
|
|
26
|
+
|
|
27
|
+
import claude_chain_runner as chain_runner # noqa: E402
|
|
28
|
+
import invoke_code_review as invoker # noqa: E402
|
|
29
|
+
from claude_chain_runner import ChainAttempt, ChainInvocationOutcome # noqa: E402
|
|
30
|
+
from dev_env_scripts_constants.code_review_constants import ( # noqa: E402
|
|
31
|
+
CLI_SESSION_MODEL_FLAG,
|
|
32
|
+
CODE_REVIEW_MODEL_ALIAS,
|
|
33
|
+
GIT_BINARY,
|
|
34
|
+
RECORD_STAMP_FLAG,
|
|
35
|
+
)
|
|
36
|
+
from dev_env_scripts_constants.grok_worker_constants import ( # noqa: E402
|
|
37
|
+
CLI_TIMEOUT_FLAG,
|
|
38
|
+
CWD_FLAG,
|
|
39
|
+
)
|
|
40
|
+
from dev_env_scripts_constants.timing import ( # noqa: E402
|
|
41
|
+
DEFAULT_CODE_REVIEW_TIMEOUT_SECONDS,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
HOST_PROFILE_CLAUDE = "Claude"
|
|
45
|
+
HOST_PROFILE_THIRD_PARTY = "ThirdParty"
|
|
46
|
+
|
|
47
|
+
FIXTURE_SERVED_COMMAND = "claude"
|
|
48
|
+
FIXTURE_CHAIN_RETURNCODE = 0
|
|
49
|
+
FIXTURE_FAILED_RETURNCODE = 1
|
|
50
|
+
FIXTURE_GIT_STATUS_FAILURE_RETURNCODE = 128
|
|
51
|
+
FIXTURE_CHAIN_STDOUT = '{"result":"review done"}'
|
|
52
|
+
FIXTURE_SESSION_OPUS = "opus"
|
|
53
|
+
FIXTURE_SESSION_OPUS_UPPER = "Opus"
|
|
54
|
+
FIXTURE_SESSION_SONNET = "sonnet"
|
|
55
|
+
FIXTURE_SESSION_HAIKU = "haiku"
|
|
56
|
+
FIXTURE_CHAIN_CONFIG_ERROR_MESSAGE = "chain config missing"
|
|
57
|
+
FIXTURE_HOST_PROFILE_ERROR_MESSAGE = "unknown host profile"
|
|
58
|
+
DIRTY_FILE_NAME = "review_fix.txt"
|
|
59
|
+
DIRTY_FILE_CONTENTS = "applied fix\n"
|
|
60
|
+
GIT_INIT_TIMEOUT_SECONDS = 30
|
|
61
|
+
REPOSITORY_USER_EMAIL = "reviewer@example.com"
|
|
62
|
+
REPOSITORY_USER_NAME = "Reviewer"
|
|
63
|
+
PINNED_INITIAL_BRANCH = "main"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _run_git(*git_arguments: str, working_directory: Path | None = None) -> None:
|
|
67
|
+
all_git_tokens = [GIT_BINARY]
|
|
68
|
+
if working_directory is not None:
|
|
69
|
+
all_git_tokens += ["-C", str(working_directory)]
|
|
70
|
+
all_git_tokens += list(git_arguments)
|
|
71
|
+
subprocess.run(
|
|
72
|
+
all_git_tokens,
|
|
73
|
+
check=True,
|
|
74
|
+
capture_output=True,
|
|
75
|
+
text=True,
|
|
76
|
+
timeout=GIT_INIT_TIMEOUT_SECONDS,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _initialize_repository(repository_directory: Path) -> None:
|
|
81
|
+
repository_directory.mkdir(parents=True, exist_ok=True)
|
|
82
|
+
_run_git(
|
|
83
|
+
"init",
|
|
84
|
+
"--initial-branch",
|
|
85
|
+
PINNED_INITIAL_BRANCH,
|
|
86
|
+
working_directory=repository_directory,
|
|
87
|
+
)
|
|
88
|
+
_run_git(
|
|
89
|
+
"config",
|
|
90
|
+
"user.email",
|
|
91
|
+
REPOSITORY_USER_EMAIL,
|
|
92
|
+
working_directory=repository_directory,
|
|
93
|
+
)
|
|
94
|
+
_run_git(
|
|
95
|
+
"config",
|
|
96
|
+
"user.name",
|
|
97
|
+
REPOSITORY_USER_NAME,
|
|
98
|
+
working_directory=repository_directory,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def write_dirty_tree(working_directory: Path) -> None:
|
|
103
|
+
dirty_file = working_directory / DIRTY_FILE_NAME
|
|
104
|
+
dirty_file.write_text(DIRTY_FILE_CONTENTS, encoding="utf-8")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def run_record_stamp_cli(working_directory: Path, *, effort: str) -> int:
|
|
108
|
+
return invoker.main(
|
|
109
|
+
[
|
|
110
|
+
CWD_FLAG,
|
|
111
|
+
str(working_directory),
|
|
112
|
+
CLI_SESSION_MODEL_FLAG,
|
|
113
|
+
CODE_REVIEW_MODEL_ALIAS,
|
|
114
|
+
RECORD_STAMP_FLAG,
|
|
115
|
+
effort,
|
|
116
|
+
]
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def claude_served(
|
|
121
|
+
*,
|
|
122
|
+
returncode: int = FIXTURE_CHAIN_RETURNCODE,
|
|
123
|
+
stdout: str = FIXTURE_CHAIN_STDOUT,
|
|
124
|
+
) -> ChainInvocationOutcome:
|
|
125
|
+
return ChainInvocationOutcome(
|
|
126
|
+
served_command=FIXTURE_SERVED_COMMAND,
|
|
127
|
+
returncode=returncode,
|
|
128
|
+
stdout=stdout,
|
|
129
|
+
stderr="",
|
|
130
|
+
attempts=(ChainAttempt(command=FIXTURE_SERVED_COMMAND, status="served"),),
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def claude_failed() -> ChainInvocationOutcome:
|
|
135
|
+
return ChainInvocationOutcome(
|
|
136
|
+
served_command=None,
|
|
137
|
+
returncode=FIXTURE_FAILED_RETURNCODE,
|
|
138
|
+
stdout="",
|
|
139
|
+
stderr="chain exhausted",
|
|
140
|
+
attempts=(
|
|
141
|
+
ChainAttempt(command=FIXTURE_SERVED_COMMAND, status="usage_limited"),
|
|
142
|
+
),
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def init_git_repository(repository_directory: Path) -> Path:
|
|
147
|
+
_initialize_repository(repository_directory)
|
|
148
|
+
(repository_directory / "README.md").write_text("baseline\n", encoding="utf-8")
|
|
149
|
+
_run_git("add", "README.md", working_directory=repository_directory)
|
|
150
|
+
_run_git("commit", "-m", "baseline", working_directory=repository_directory)
|
|
151
|
+
return repository_directory
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def run_review(
|
|
155
|
+
working_directory: Path, *, session_model: str
|
|
156
|
+
) -> invoker.CodeReviewOutcome:
|
|
157
|
+
return invoker.invoke_code_review(
|
|
158
|
+
working_directory=working_directory,
|
|
159
|
+
session_model=session_model,
|
|
160
|
+
timeout_seconds=DEFAULT_CODE_REVIEW_TIMEOUT_SECONDS,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def run_review_cli(working_directory: Path, *, session_model: str) -> int:
|
|
165
|
+
return invoker.main(
|
|
166
|
+
[
|
|
167
|
+
CWD_FLAG,
|
|
168
|
+
str(working_directory),
|
|
169
|
+
CLI_SESSION_MODEL_FLAG,
|
|
170
|
+
session_model,
|
|
171
|
+
CLI_TIMEOUT_FLAG,
|
|
172
|
+
str(DEFAULT_CODE_REVIEW_TIMEOUT_SECONDS),
|
|
173
|
+
]
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@dataclass
|
|
178
|
+
class SeamCallLog:
|
|
179
|
+
claude_calls: int = 0
|
|
180
|
+
claude_arguments: list[str] | None = None
|
|
181
|
+
host_profile_calls: int = 0
|
|
182
|
+
is_stdin_empty: bool = False
|
|
183
|
+
claude_working_directory: Path | None = None
|
|
184
|
+
all_observed_working_directories: list[Path] = field(default_factory=list)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@dataclass
|
|
188
|
+
class _SeamConfiguration:
|
|
189
|
+
host_profile: str
|
|
190
|
+
claude_outcome: ChainInvocationOutcome | BaseException | None
|
|
191
|
+
should_dirty_tree_on_chain: bool
|
|
192
|
+
working_directory: Path | None
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _record_dirty_tree(configuration: _SeamConfiguration) -> None:
|
|
196
|
+
if not configuration.should_dirty_tree_on_chain:
|
|
197
|
+
return
|
|
198
|
+
if configuration.working_directory is None:
|
|
199
|
+
return
|
|
200
|
+
write_dirty_tree(configuration.working_directory)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _detect_empty_stdin(maybe_stdin: object) -> bool | None:
|
|
204
|
+
if maybe_stdin is subprocess.DEVNULL:
|
|
205
|
+
return True
|
|
206
|
+
if maybe_stdin is None:
|
|
207
|
+
return None
|
|
208
|
+
maybe_read = getattr(maybe_stdin, "read", None)
|
|
209
|
+
if not callable(maybe_read):
|
|
210
|
+
return None
|
|
211
|
+
stdin_contents = maybe_read()
|
|
212
|
+
maybe_seek = getattr(maybe_stdin, "seek", None)
|
|
213
|
+
if callable(maybe_seek):
|
|
214
|
+
maybe_seek(0)
|
|
215
|
+
return stdin_contents == ""
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _build_host_profile_seam(
|
|
219
|
+
call_log: SeamCallLog, configuration: _SeamConfiguration
|
|
220
|
+
) -> Callable[..., str]:
|
|
221
|
+
def fake_host_profile(setting_by_name: object | None = None) -> str:
|
|
222
|
+
del setting_by_name
|
|
223
|
+
call_log.host_profile_calls += 1
|
|
224
|
+
return configuration.host_profile
|
|
225
|
+
|
|
226
|
+
return fake_host_profile
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _build_claude_seam(
|
|
230
|
+
call_log: SeamCallLog, configuration: _SeamConfiguration
|
|
231
|
+
) -> Callable[..., ChainInvocationOutcome]:
|
|
232
|
+
def fake_claude(
|
|
233
|
+
all_claude_arguments: list[str], *, timeout_seconds: int
|
|
234
|
+
) -> ChainInvocationOutcome:
|
|
235
|
+
call_log.claude_calls += 1
|
|
236
|
+
call_log.claude_arguments = list(all_claude_arguments)
|
|
237
|
+
chain_runner.chain_subprocess_runner(
|
|
238
|
+
["claude", *all_claude_arguments],
|
|
239
|
+
capture_output=True,
|
|
240
|
+
text=True,
|
|
241
|
+
timeout=timeout_seconds,
|
|
242
|
+
check=False,
|
|
243
|
+
)
|
|
244
|
+
_record_dirty_tree(configuration)
|
|
245
|
+
if isinstance(configuration.claude_outcome, BaseException):
|
|
246
|
+
raise configuration.claude_outcome
|
|
247
|
+
assert isinstance(configuration.claude_outcome, ChainInvocationOutcome)
|
|
248
|
+
return configuration.claude_outcome
|
|
249
|
+
|
|
250
|
+
return fake_claude
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _build_subprocess_seam(
|
|
254
|
+
call_log: SeamCallLog,
|
|
255
|
+
) -> Callable[..., subprocess.CompletedProcess[str]]:
|
|
256
|
+
def tracking_subprocess_runner(
|
|
257
|
+
all_invocation_tokens: Sequence[str],
|
|
258
|
+
*all_positionals: object,
|
|
259
|
+
**all_keywords: object,
|
|
260
|
+
) -> subprocess.CompletedProcess[str]:
|
|
261
|
+
del all_positionals
|
|
262
|
+
maybe_empty_stdin = _detect_empty_stdin(all_keywords.get("stdin"))
|
|
263
|
+
if maybe_empty_stdin is not None:
|
|
264
|
+
call_log.is_stdin_empty = maybe_empty_stdin
|
|
265
|
+
maybe_cwd = all_keywords.get("cwd")
|
|
266
|
+
if maybe_cwd is not None:
|
|
267
|
+
resolved_directory = Path(str(maybe_cwd))
|
|
268
|
+
call_log.claude_working_directory = resolved_directory
|
|
269
|
+
call_log.all_observed_working_directories.append(resolved_directory)
|
|
270
|
+
return subprocess.CompletedProcess(
|
|
271
|
+
args=list(all_invocation_tokens),
|
|
272
|
+
returncode=0,
|
|
273
|
+
stdout="{}",
|
|
274
|
+
stderr="",
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
return tracking_subprocess_runner
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _apply_seams(
|
|
281
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
282
|
+
call_log: SeamCallLog,
|
|
283
|
+
configuration: _SeamConfiguration,
|
|
284
|
+
) -> None:
|
|
285
|
+
monkeypatch.setattr(
|
|
286
|
+
invoker,
|
|
287
|
+
"review_host_profile_detector",
|
|
288
|
+
_build_host_profile_seam(call_log, configuration),
|
|
289
|
+
)
|
|
290
|
+
monkeypatch.setattr(
|
|
291
|
+
invoker,
|
|
292
|
+
"review_claude_runner",
|
|
293
|
+
_build_claude_seam(call_log, configuration),
|
|
294
|
+
)
|
|
295
|
+
monkeypatch.setattr(
|
|
296
|
+
chain_runner,
|
|
297
|
+
"chain_subprocess_runner",
|
|
298
|
+
_build_subprocess_seam(call_log),
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def install_seams(
|
|
303
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
304
|
+
*,
|
|
305
|
+
host_profile: str = HOST_PROFILE_CLAUDE,
|
|
306
|
+
claude_outcome: ChainInvocationOutcome | BaseException | None = None,
|
|
307
|
+
should_dirty_tree_on_chain: bool = False,
|
|
308
|
+
working_directory: Path | None = None,
|
|
309
|
+
) -> SeamCallLog:
|
|
310
|
+
call_log = SeamCallLog()
|
|
311
|
+
configuration = _SeamConfiguration(
|
|
312
|
+
host_profile=host_profile,
|
|
313
|
+
claude_outcome=claude_outcome,
|
|
314
|
+
should_dirty_tree_on_chain=should_dirty_tree_on_chain,
|
|
315
|
+
working_directory=working_directory,
|
|
316
|
+
)
|
|
317
|
+
_apply_seams(monkeypatch, call_log, configuration)
|
|
318
|
+
return call_log
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
EFFORT_LOW = "low"
|
|
322
|
+
REJECTED_ULTRA_EFFORT = "ultra"
|
|
323
|
+
SINGLE_PASS_CAP = 1
|
|
324
|
+
MISSING_STORE_FILE_NAME = "code_review_stamp_store_absent.py"
|
|
325
|
+
SURFACE_SOURCE = "def add(left: int, right: int) -> int:\n return left + right\n"
|
|
326
|
+
SURFACE_CHANGE_SOURCE = (
|
|
327
|
+
"def add(left: int, right: int) -> int:\n return left - right\n"
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _make_repo_with_change_surface(tmp_path: Path) -> Path:
|
|
332
|
+
origin_directory = tmp_path / "origin.git"
|
|
333
|
+
work_directory = tmp_path / "work"
|
|
334
|
+
_run_git(
|
|
335
|
+
"init",
|
|
336
|
+
"--bare",
|
|
337
|
+
"--initial-branch",
|
|
338
|
+
PINNED_INITIAL_BRANCH,
|
|
339
|
+
str(origin_directory),
|
|
340
|
+
)
|
|
341
|
+
_initialize_repository(work_directory)
|
|
342
|
+
(work_directory / "app.py").write_text(SURFACE_SOURCE, encoding="utf-8")
|
|
343
|
+
_run_git("add", "-A", working_directory=work_directory)
|
|
344
|
+
_run_git("commit", "-m", "base", working_directory=work_directory)
|
|
345
|
+
_run_git(
|
|
346
|
+
"remote",
|
|
347
|
+
"add",
|
|
348
|
+
"origin",
|
|
349
|
+
str(origin_directory),
|
|
350
|
+
working_directory=work_directory,
|
|
351
|
+
)
|
|
352
|
+
_run_git(
|
|
353
|
+
"push", "-u", "origin", PINNED_INITIAL_BRANCH, working_directory=work_directory
|
|
354
|
+
)
|
|
355
|
+
(work_directory / "app.py").write_text(SURFACE_CHANGE_SOURCE, encoding="utf-8")
|
|
356
|
+
return work_directory
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _isolate_home(monkeypatch: pytest.MonkeyPatch, fake_home: Path) -> None:
|
|
360
|
+
home_text = str(fake_home)
|
|
361
|
+
monkeypatch.setenv("HOME", home_text)
|
|
362
|
+
monkeypatch.setenv("USERPROFILE", home_text)
|
|
363
|
+
monkeypatch.delenv("HOMEDRIVE", raising=False)
|
|
364
|
+
monkeypatch.delenv("HOMEPATH", raising=False)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def prepared_surface_repo(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Path:
|
|
368
|
+
fake_home = tmp_path / "home"
|
|
369
|
+
fake_home.mkdir()
|
|
370
|
+
_isolate_home(monkeypatch, fake_home)
|
|
371
|
+
return _make_repo_with_change_surface(tmp_path)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _chain_clean_outcome() -> invoker.CodeReviewOutcome:
|
|
375
|
+
return invoker.CodeReviewOutcome(
|
|
376
|
+
mode=invoker.MODE_CHAIN,
|
|
377
|
+
served_command=FIXTURE_SERVED_COMMAND,
|
|
378
|
+
returncode=FIXTURE_CHAIN_RETURNCODE,
|
|
379
|
+
is_dirty_tree=True,
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def stable_clean_review(**_review_keywords: object) -> invoker.CodeReviewOutcome:
|
|
384
|
+
return _chain_clean_outcome()
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def surface_changing_review(
|
|
388
|
+
*, working_directory: Path, **_review_keywords: object
|
|
389
|
+
) -> invoker.CodeReviewOutcome:
|
|
390
|
+
write_dirty_tree(working_directory)
|
|
391
|
+
return _chain_clean_outcome()
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
class DriftingReview:
|
|
395
|
+
def __init__(self) -> None:
|
|
396
|
+
self.pass_count = 0
|
|
397
|
+
|
|
398
|
+
def __call__(
|
|
399
|
+
self, *, working_directory: Path, **_review_keywords: object
|
|
400
|
+
) -> invoker.CodeReviewOutcome:
|
|
401
|
+
self.pass_count += 1
|
|
402
|
+
drift_path = working_directory / f"fix_{self.pass_count}.txt"
|
|
403
|
+
drift_path.write_text(str(self.pass_count), encoding="utf-8")
|
|
404
|
+
return _chain_clean_outcome()
|
|
@@ -40,6 +40,7 @@ import io
|
|
|
40
40
|
import json
|
|
41
41
|
import subprocess
|
|
42
42
|
import sys
|
|
43
|
+
import tempfile
|
|
43
44
|
from collections.abc import Callable, Sequence
|
|
44
45
|
from dataclasses import dataclass
|
|
45
46
|
from pathlib import Path
|
|
@@ -58,6 +59,7 @@ from dev_env_scripts_constants.claude_chain_constants import (
|
|
|
58
59
|
ATTEMPT_STATUS_USAGE_LIMITED,
|
|
59
60
|
ATTEMPT_SUMMARY_ENTRY_TEMPLATE,
|
|
60
61
|
ATTEMPT_SUMMARY_JOIN_SEPARATOR,
|
|
62
|
+
CARRIAGE_RETURN,
|
|
61
63
|
CHAIN_CONFIG_ERROR_EXIT_CODE,
|
|
62
64
|
CHAIN_EXHAUSTED_EXIT_CODE,
|
|
63
65
|
CHAIN_EXHAUSTED_MESSAGE_TEMPLATE,
|
|
@@ -82,13 +84,151 @@ from dev_env_scripts_constants.claude_chain_constants import (
|
|
|
82
84
|
CONFIG_MISSING_MESSAGE_TEMPLATE,
|
|
83
85
|
CONFIG_NOT_OBJECT_REASON,
|
|
84
86
|
CONFIG_UNREADABLE_MESSAGE_TEMPLATE,
|
|
87
|
+
CRLF_NEWLINE,
|
|
85
88
|
DEFAULT_TIMEOUT_SECONDS,
|
|
86
89
|
EXAMPLE_CONFIG_FILENAME,
|
|
90
|
+
LINE_FEED,
|
|
87
91
|
NO_COMPLETED_PROCESS_RETURN_CODE,
|
|
88
92
|
UTF8_ENCODING,
|
|
89
93
|
)
|
|
90
94
|
|
|
91
95
|
|
|
96
|
+
def _decode_captured_stream(raw_bytes: bytes, encoding: str, errors: str) -> str:
|
|
97
|
+
"""Decode captured *raw_bytes* with ``text=True`` universal-newline semantics.
|
|
98
|
+
|
|
99
|
+
Spool capture writes binary temp files, so a bare ``.decode`` leaves CRLF and
|
|
100
|
+
bare CR intact. ``subprocess.run(..., text=True)`` normalized those to LF;
|
|
101
|
+
this helper restores that contract for Windows children that emit ``\\r\\n``.
|
|
102
|
+
"""
|
|
103
|
+
decoded_text = raw_bytes.decode(encoding, errors)
|
|
104
|
+
return decoded_text.replace(CRLF_NEWLINE, LINE_FEED).replace(
|
|
105
|
+
CARRIAGE_RETURN, LINE_FEED
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class _SpooledByteStream(Protocol):
|
|
110
|
+
"""Binary spool with seek/read — TemporaryFile wrappers and BufferedIO."""
|
|
111
|
+
|
|
112
|
+
def seek(self, target: int, whence: int = 0, /) -> int: ...
|
|
113
|
+
|
|
114
|
+
def read(self, size: int | None = -1, /) -> bytes: ...
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _decoded_spooled_streams(
|
|
118
|
+
stdout_file: _SpooledByteStream,
|
|
119
|
+
stderr_file: _SpooledByteStream,
|
|
120
|
+
encoding: str,
|
|
121
|
+
errors: str,
|
|
122
|
+
) -> tuple[str, str]:
|
|
123
|
+
"""Seek both spool files to the start and decode their full contents."""
|
|
124
|
+
stdout_file.seek(0)
|
|
125
|
+
stderr_file.seek(0)
|
|
126
|
+
return (
|
|
127
|
+
_decode_captured_stream(stdout_file.read(), encoding, errors),
|
|
128
|
+
_decode_captured_stream(stderr_file.read(), encoding, errors),
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _attach_partial_timeout_streams(
|
|
133
|
+
timeout_error: subprocess.TimeoutExpired,
|
|
134
|
+
stdout_file: _SpooledByteStream,
|
|
135
|
+
stderr_file: _SpooledByteStream,
|
|
136
|
+
encoding: str,
|
|
137
|
+
errors: str,
|
|
138
|
+
) -> None:
|
|
139
|
+
"""Decode partial spool contents onto *timeout_error* before re-raise."""
|
|
140
|
+
captured_stdout, captured_stderr = _decoded_spooled_streams(
|
|
141
|
+
stdout_file, stderr_file, encoding, errors
|
|
142
|
+
)
|
|
143
|
+
timeout_error.stdout = captured_stdout
|
|
144
|
+
timeout_error.stderr = captured_stderr
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# Capturing a large-output child through OS pipes (``capture_output=True``)
|
|
148
|
+
# deadlocks on Windows: the child buffers its whole response and flushes it at
|
|
149
|
+
# once, the pipe buffer fills before the parent drains it, and both sides block.
|
|
150
|
+
# Redirecting each stream to a temporary file removes the pipe, so the child
|
|
151
|
+
# writes freely; the files are then read back and decoded the way a pipe capture
|
|
152
|
+
# would. ``capture_output``, ``text``, and ``env`` are ignored in favor of file
|
|
153
|
+
# redirection and the parent environment; ``timeout``, ``check``, ``cwd``,
|
|
154
|
+
# ``stdin``, ``input``, ``encoding``, and ``errors`` are honored. On timeout,
|
|
155
|
+
# partial stdout/stderr are decoded from the temp files and attached to the
|
|
156
|
+
# raised ``TimeoutExpired``. When ``check=True`` and the child exits non-zero,
|
|
157
|
+
# ``CalledProcessError.stdout`` / ``.stderr`` stay unset (temp files are not
|
|
158
|
+
# attached to that raised error).
|
|
159
|
+
def _run_captured_subprocess(
|
|
160
|
+
all_invocation_tokens: list[str],
|
|
161
|
+
**all_subprocess_options: object,
|
|
162
|
+
) -> subprocess.CompletedProcess[str]:
|
|
163
|
+
"""Run *all_invocation_tokens*, spooling stdout and stderr to temp files."""
|
|
164
|
+
encoding = str(all_subprocess_options.get("encoding") or UTF8_ENCODING)
|
|
165
|
+
errors = str(all_subprocess_options.get("errors") or CODEC_ERROR_STRATEGY)
|
|
166
|
+
input_bytes = _captured_stdin_bytes(all_subprocess_options, encoding, errors)
|
|
167
|
+
working_directory = all_subprocess_options.get("cwd")
|
|
168
|
+
timeout_seconds = all_subprocess_options.get("timeout")
|
|
169
|
+
with (
|
|
170
|
+
tempfile.TemporaryFile() as stdout_file,
|
|
171
|
+
tempfile.TemporaryFile() as stderr_file,
|
|
172
|
+
):
|
|
173
|
+
try:
|
|
174
|
+
completion = subprocess.run(
|
|
175
|
+
all_invocation_tokens,
|
|
176
|
+
stdout=stdout_file,
|
|
177
|
+
stderr=stderr_file,
|
|
178
|
+
cwd=working_directory if isinstance(working_directory, str) else None,
|
|
179
|
+
check=bool(all_subprocess_options.get("check", False)),
|
|
180
|
+
timeout=(
|
|
181
|
+
float(timeout_seconds)
|
|
182
|
+
if isinstance(timeout_seconds, (int, float))
|
|
183
|
+
else None
|
|
184
|
+
),
|
|
185
|
+
input=input_bytes,
|
|
186
|
+
)
|
|
187
|
+
except subprocess.TimeoutExpired as timeout_error:
|
|
188
|
+
_attach_partial_timeout_streams(
|
|
189
|
+
timeout_error, stdout_file, stderr_file, encoding, errors
|
|
190
|
+
)
|
|
191
|
+
raise
|
|
192
|
+
captured_stdout, captured_stderr = _decoded_spooled_streams(
|
|
193
|
+
stdout_file, stderr_file, encoding, errors
|
|
194
|
+
)
|
|
195
|
+
return subprocess.CompletedProcess(
|
|
196
|
+
all_invocation_tokens,
|
|
197
|
+
completion.returncode,
|
|
198
|
+
captured_stdout,
|
|
199
|
+
captured_stderr,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _captured_stdin_bytes(
|
|
204
|
+
all_subprocess_options: dict[str, object], encoding: str, errors: str
|
|
205
|
+
) -> bytes | None:
|
|
206
|
+
"""Return the bytes to feed the child's stdin for a spooled run.
|
|
207
|
+
|
|
208
|
+
::
|
|
209
|
+
|
|
210
|
+
stdin=<open prompt file> -> the file's bytes
|
|
211
|
+
stdin=subprocess.DEVNULL -> b"" (an immediate EOF)
|
|
212
|
+
input="charter text" -> the encoded text
|
|
213
|
+
|
|
214
|
+
A wrapper hands the runner a ``stdin`` stream or a ``DEVNULL`` sentinel; the
|
|
215
|
+
spooled run reads from an ``input`` pipe rather than the caller's handle, so
|
|
216
|
+
the stream is read into bytes here to deliver the same stdin a direct pipe
|
|
217
|
+
would. When no ``stdin`` is given, the ``input`` text is encoded instead.
|
|
218
|
+
"""
|
|
219
|
+
stdin_source = all_subprocess_options.get("stdin")
|
|
220
|
+
if isinstance(stdin_source, io.TextIOBase):
|
|
221
|
+
return stdin_source.read().encode(encoding, errors)
|
|
222
|
+
if isinstance(stdin_source, (io.RawIOBase, io.BufferedIOBase)):
|
|
223
|
+
return stdin_source.read() or b""
|
|
224
|
+
if isinstance(stdin_source, int):
|
|
225
|
+
return b""
|
|
226
|
+
input_text = all_subprocess_options.get("input")
|
|
227
|
+
if input_text is None:
|
|
228
|
+
return None
|
|
229
|
+
return str(input_text).encode(encoding, errors)
|
|
230
|
+
|
|
231
|
+
|
|
92
232
|
class ChainConfigurationError(Exception):
|
|
93
233
|
"""Raised when the chain configuration is missing, unreadable, or malformed."""
|
|
94
234
|
|
|
@@ -138,7 +278,7 @@ class WeeklyUsageAccountReport(Protocol):
|
|
|
138
278
|
command: str
|
|
139
279
|
|
|
140
280
|
|
|
141
|
-
chain_subprocess_runner =
|
|
281
|
+
chain_subprocess_runner = _run_captured_subprocess
|
|
142
282
|
|
|
143
283
|
|
|
144
284
|
def _load_chain_usage_module() -> ModuleType:
|
package/scripts/conftest.py
CHANGED
|
@@ -1,4 +1,19 @@
|
|
|
1
|
-
"""Put the scripts directory on sys.path
|
|
1
|
+
"""Put the scripts directory on ``sys.path`` for every test in this folder.
|
|
2
|
+
|
|
3
|
+
The code-review invoker and its constants package live beside these tests
|
|
4
|
+
rather than on the default pytest path, so each test module can import them
|
|
5
|
+
by bare name once this directory is registered.
|
|
6
|
+
|
|
7
|
+
::
|
|
8
|
+
|
|
9
|
+
import invoke_code_review as invoker # resolves here
|
|
10
|
+
from dev_env_scripts_constants... # resolves here
|
|
11
|
+
|
|
12
|
+
pytest loads this file before collecting sibling test modules, so the path
|
|
13
|
+
is in place by the time any test module runs its top-level imports.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
2
17
|
|
|
3
18
|
import sys
|
|
4
19
|
from pathlib import Path
|
|
@@ -11,7 +11,7 @@ Named constants for scripts in `scripts/`. Follows the project convention that t
|
|
|
11
11
|
| `claude_chain_constants.py` | `claude_chain_runner.py` - the chain config filename and home subdirectory, the usage-limit signature text, the per-binary status labels, the default timeout, CLI flag and separator tokens, config JSON keys (including optional `credentials_path`), invalid-shape reason text, config-error and exhausted-chain message templates, and CLI exit codes; `invoke_code_review.py` and `resolve_worker_spawn.py` - the text-codec keyword names and the `collect_forwarded_text_codec` helper that forwards them to the subprocess runner for text-mode capture |
|
|
12
12
|
| `claude_chain_usage_constants.py` | `claude_chain_usage.py` - full weekly percent scale, usage-pause skill path segments, CLI config-path flag, JSON report keys, and probe error message templates |
|
|
13
13
|
| `grok_worker_constants.py` | `grok_worker_preflight.py`, `grok_headless_runner.py`, and `spawn_grok_batch.py` - the `grok` binary name and CLI flags, model and subcommand tokens, leader-socket and scratch-file name parts, auth and usage-limit signature lists, outcome classifications, fallthrough reasons, tool-profile names and prompt headers, timeouts and turn caps (including launch-failure return code and post-kill grace), ping-cache keys and TTL, batch-spec and summary JSON keys, the prompt-part and report-stream join separators, and the CLI launch-error stderr prefix |
|
|
14
|
-
| `code_review_constants.py` | `invoke_code_review.py` - the `/code-review
|
|
14
|
+
| `code_review_constants.py` | `invoke_code_review.py` - the `/code-review ultra --fix` prompt, opus model alias, permission-mode flag and value, result mode and JSON keys, session-model CLI flag, git dirty-check tokens, and in-session return markers |
|
|
15
15
|
| `__init__.py` | Empty package marker |
|
|
16
16
|
|
|
17
17
|
## Convention
|
|
@@ -15,6 +15,15 @@ UTF8_ENCODING: str = "utf-8"
|
|
|
15
15
|
CODEC_ERROR_STRATEGY: str = "replace"
|
|
16
16
|
"""Codec error handler that maps any unencodable or undecodable value to a marker."""
|
|
17
17
|
|
|
18
|
+
CRLF_NEWLINE: str = "\r\n"
|
|
19
|
+
"""Windows-style newline sequence normalized away when decoding spool captures."""
|
|
20
|
+
|
|
21
|
+
CARRIAGE_RETURN: str = "\r"
|
|
22
|
+
"""Bare carriage return normalized to LF when decoding spool captures."""
|
|
23
|
+
|
|
24
|
+
LINE_FEED: str = "\n"
|
|
25
|
+
"""Unix-style newline retained after universal-newline normalization."""
|
|
26
|
+
|
|
18
27
|
SUBPROCESS_ENCODING_KEYWORD: str = "encoding"
|
|
19
28
|
"""Keyword name for text encoding when forwarding chain subprocess runner kwargs."""
|
|
20
29
|
|