constraintloop 0.3.0__tar.gz → 0.3.1__tar.gz
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.
- {constraintloop-0.3.0 → constraintloop-0.3.1}/CHANGELOG.md +5 -0
- {constraintloop-0.3.0 → constraintloop-0.3.1}/PKG-INFO +1 -1
- {constraintloop-0.3.0 → constraintloop-0.3.1}/docs/faq.md +16 -0
- {constraintloop-0.3.0 → constraintloop-0.3.1}/pyproject.toml +1 -1
- {constraintloop-0.3.0 → constraintloop-0.3.1}/src/constraintloop/__init__.py +1 -1
- {constraintloop-0.3.0 → constraintloop-0.3.1}/src/constraintloop/config.py +11 -1
- {constraintloop-0.3.0 → constraintloop-0.3.1}/src/constraintloop/hooks.py +9 -0
- {constraintloop-0.3.0 → constraintloop-0.3.1}/tests/test_cli.py +17 -0
- {constraintloop-0.3.0 → constraintloop-0.3.1}/tests/test_hooks.py +16 -1
- {constraintloop-0.3.0 → constraintloop-0.3.1}/tests/test_models.py +34 -0
- {constraintloop-0.3.0 → constraintloop-0.3.1}/.gitignore +0 -0
- {constraintloop-0.3.0 → constraintloop-0.3.1}/CONTRIBUTING.md +0 -0
- {constraintloop-0.3.0 → constraintloop-0.3.1}/GOVERNANCE.md +0 -0
- {constraintloop-0.3.0 → constraintloop-0.3.1}/LICENSE +0 -0
- {constraintloop-0.3.0 → constraintloop-0.3.1}/README.md +0 -0
- {constraintloop-0.3.0 → constraintloop-0.3.1}/RELEASE.md +0 -0
- {constraintloop-0.3.0 → constraintloop-0.3.1}/SECURITY.md +0 -0
- {constraintloop-0.3.0 → constraintloop-0.3.1}/SUPPORT.md +0 -0
- {constraintloop-0.3.0 → constraintloop-0.3.1}/docs/configuration.md +0 -0
- {constraintloop-0.3.0 → constraintloop-0.3.1}/docs/convergence-loops.md +0 -0
- {constraintloop-0.3.0 → constraintloop-0.3.1}/docs/native-cli-evaluators.md +0 -0
- {constraintloop-0.3.0 → constraintloop-0.3.1}/docs/openai-evaluation.md +0 -0
- {constraintloop-0.3.0 → constraintloop-0.3.1}/docs/provider-privacy.md +0 -0
- {constraintloop-0.3.0 → constraintloop-0.3.1}/docs/recipes.md +0 -0
- {constraintloop-0.3.0 → constraintloop-0.3.1}/docs/release-readiness.md +0 -0
- {constraintloop-0.3.0 → constraintloop-0.3.1}/docs/threat-model.md +0 -0
- {constraintloop-0.3.0 → constraintloop-0.3.1}/schema/constraintloop.schema.json +0 -0
- {constraintloop-0.3.0 → constraintloop-0.3.1}/scripts/check_anthropic_sdk.py +0 -0
- {constraintloop-0.3.0 → constraintloop-0.3.1}/scripts/check_coverage.py +0 -0
- {constraintloop-0.3.0 → constraintloop-0.3.1}/scripts/check_openai_sdk.py +0 -0
- {constraintloop-0.3.0 → constraintloop-0.3.1}/scripts/check_sdist_contents.py +0 -0
- {constraintloop-0.3.0 → constraintloop-0.3.1}/scripts/generate_schema.py +0 -0
- {constraintloop-0.3.0 → constraintloop-0.3.1}/scripts/openai_eval_canary.py +0 -0
- {constraintloop-0.3.0 → constraintloop-0.3.1}/scripts/wheel_failure_smoke.py +0 -0
- {constraintloop-0.3.0 → constraintloop-0.3.1}/src/constraintloop/__main__.py +0 -0
- {constraintloop-0.3.0 → constraintloop-0.3.1}/src/constraintloop/cli.py +0 -0
- {constraintloop-0.3.0 → constraintloop-0.3.1}/src/constraintloop/diagnostics.py +0 -0
- {constraintloop-0.3.0 → constraintloop-0.3.1}/src/constraintloop/digest.py +0 -0
- {constraintloop-0.3.0 → constraintloop-0.3.1}/src/constraintloop/engine.py +0 -0
- {constraintloop-0.3.0 → constraintloop-0.3.1}/src/constraintloop/environment.py +0 -0
- {constraintloop-0.3.0 → constraintloop-0.3.1}/src/constraintloop/eval_corpus.py +0 -0
- {constraintloop-0.3.0 → constraintloop-0.3.1}/src/constraintloop/evaluators.py +0 -0
- {constraintloop-0.3.0 → constraintloop-0.3.1}/src/constraintloop/hygiene.py +0 -0
- {constraintloop-0.3.0 → constraintloop-0.3.1}/src/constraintloop/loops.py +0 -0
- {constraintloop-0.3.0 → constraintloop-0.3.1}/src/constraintloop/models.py +0 -0
- {constraintloop-0.3.0 → constraintloop-0.3.1}/src/constraintloop/native_cli_evaluator.py +0 -0
- {constraintloop-0.3.0 → constraintloop-0.3.1}/src/constraintloop/py.typed +0 -0
- {constraintloop-0.3.0 → constraintloop-0.3.1}/src/constraintloop/runners.py +0 -0
- {constraintloop-0.3.0 → constraintloop-0.3.1}/src/constraintloop/scaffold.py +0 -0
- {constraintloop-0.3.0 → constraintloop-0.3.1}/src/constraintloop/setup_hooks.py +0 -0
- {constraintloop-0.3.0 → constraintloop-0.3.1}/src/constraintloop/state.py +0 -0
- {constraintloop-0.3.0 → constraintloop-0.3.1}/tests/__init__.py +0 -0
- {constraintloop-0.3.0 → constraintloop-0.3.1}/tests/failure_lab.py +0 -0
- {constraintloop-0.3.0 → constraintloop-0.3.1}/tests/fixtures/openai_eval_corpus_v1.yml +0 -0
- {constraintloop-0.3.0 → constraintloop-0.3.1}/tests/test_cli_commands.py +0 -0
- {constraintloop-0.3.0 → constraintloop-0.3.1}/tests/test_diagnostics.py +0 -0
- {constraintloop-0.3.0 → constraintloop-0.3.1}/tests/test_digest.py +0 -0
- {constraintloop-0.3.0 → constraintloop-0.3.1}/tests/test_engine.py +0 -0
- {constraintloop-0.3.0 → constraintloop-0.3.1}/tests/test_environment.py +0 -0
- {constraintloop-0.3.0 → constraintloop-0.3.1}/tests/test_eval_corpus.py +0 -0
- {constraintloop-0.3.0 → constraintloop-0.3.1}/tests/test_evaluators.py +0 -0
- {constraintloop-0.3.0 → constraintloop-0.3.1}/tests/test_failure_lab.py +0 -0
- {constraintloop-0.3.0 → constraintloop-0.3.1}/tests/test_loops.py +0 -0
- {constraintloop-0.3.0 → constraintloop-0.3.1}/tests/test_native_cli_evaluator.py +0 -0
- {constraintloop-0.3.0 → constraintloop-0.3.1}/tests/test_release_metadata.py +0 -0
- {constraintloop-0.3.0 → constraintloop-0.3.1}/tests/test_runners.py +0 -0
- {constraintloop-0.3.0 → constraintloop-0.3.1}/tests/test_security.py +0 -0
- {constraintloop-0.3.0 → constraintloop-0.3.1}/tests/test_state.py +0 -0
|
@@ -5,6 +5,11 @@ Versioning, with the usual initial-development flexibility for `0.y.z`.
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.3.1] - 2026-08-31
|
|
9
|
+
|
|
10
|
+
- Prevent recursive Claude Stop-hook invocations from repeatedly blocking completion.
|
|
11
|
+
- Add actionable version-skew guidance when a hook runtime rejects unknown contract keys.
|
|
12
|
+
|
|
8
13
|
## [0.3.0] - 2026-08-18
|
|
9
14
|
|
|
10
15
|
- Add native monotonic ratchet constraints with committed JSON baselines,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: constraintloop
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Evidence-based completion gates for AI coding agents
|
|
5
5
|
Project-URL: Homepage, https://github.com/mauhpr/constraintloop
|
|
6
6
|
Project-URL: Documentation, https://github.com/mauhpr/constraintloop/tree/main/docs
|
|
@@ -149,6 +149,22 @@ Generated commands include the resolved project argument. If the error remains,
|
|
|
149
149
|
inspect the relevant `.claude`, `.codex`, or `.gemini` settings file for a stale
|
|
150
150
|
manually copied command.
|
|
151
151
|
|
|
152
|
+
### Why does a hook reject documented contract keys as extra inputs?
|
|
153
|
+
|
|
154
|
+
The hook is running an older ConstraintLoop executable than the contract expects.
|
|
155
|
+
Upgrade that installation, then regenerate the hook commands so they resolve to the
|
|
156
|
+
same release:
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
constraintloop --version
|
|
160
|
+
constraintloop setup --adapter all --project .
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Current releases include their version and this recovery action when strict schema
|
|
164
|
+
validation encounters unknown keys. Recursive Claude Stop-hook calls are allowed to
|
|
165
|
+
finish after delivering the error once, so an invalid or version-skewed contract does
|
|
166
|
+
not create a repeated Stop-hook loop.
|
|
167
|
+
|
|
152
168
|
## Loops
|
|
153
169
|
|
|
154
170
|
### Can I configure an unlimited repair loop?
|
|
@@ -10,6 +10,7 @@ from typing import Any
|
|
|
10
10
|
import yaml
|
|
11
11
|
from pydantic import ValidationError
|
|
12
12
|
|
|
13
|
+
from constraintloop import __version__
|
|
13
14
|
from constraintloop.models import Contract
|
|
14
15
|
|
|
15
16
|
CONFIG_NAMES = ("constraintloop.yml", "constraintloop.yaml")
|
|
@@ -69,7 +70,16 @@ def load_contract(project_root: Path, *, include_local: bool = True) -> tuple[Co
|
|
|
69
70
|
except ContractError:
|
|
70
71
|
raise
|
|
71
72
|
except (OSError, yaml.YAMLError, ValidationError) as exc:
|
|
72
|
-
|
|
73
|
+
hint = ""
|
|
74
|
+
if isinstance(exc, ValidationError) and any(
|
|
75
|
+
error["type"] == "extra_forbidden" for error in exc.errors()
|
|
76
|
+
):
|
|
77
|
+
hint = (
|
|
78
|
+
f"\nConstraintLoop {__version__} does not recognize one or more keys. "
|
|
79
|
+
"Check for typos; if the contract uses features from a newer release, upgrade "
|
|
80
|
+
"the hook executable and rerun `constraintloop setup --adapter all --project .`."
|
|
81
|
+
)
|
|
82
|
+
raise ContractError(f"Invalid contract {path}: {exc}{hint}") from exc
|
|
73
83
|
|
|
74
84
|
|
|
75
85
|
def _merge_mappings(base: dict[str, Any], overlay: dict[str, Any]) -> dict[str, Any]:
|
|
@@ -21,6 +21,15 @@ def handle_hook(
|
|
|
21
21
|
event: str,
|
|
22
22
|
payload: dict[str, Any],
|
|
23
23
|
) -> dict[str, Any]:
|
|
24
|
+
# Claude re-runs Stop hooks after a hook has already blocked completion and
|
|
25
|
+
# marks that recursive invocation explicitly. Blocking again traps the
|
|
26
|
+
# agent in a hook loop, so the recursive call must succeed without running
|
|
27
|
+
# the contract a second time.
|
|
28
|
+
if event == "stop" and (
|
|
29
|
+
payload.get("stop_hook_active") is True or payload.get("stopHookActive") is True
|
|
30
|
+
):
|
|
31
|
+
return {}
|
|
32
|
+
|
|
24
33
|
session_id = str(
|
|
25
34
|
payload.get("session_id")
|
|
26
35
|
or payload.get("sessionId")
|
|
@@ -6,6 +6,7 @@ import pytest
|
|
|
6
6
|
import yaml
|
|
7
7
|
from click.testing import CliRunner
|
|
8
8
|
|
|
9
|
+
from constraintloop import __version__
|
|
9
10
|
from constraintloop.cli import main
|
|
10
11
|
from constraintloop.config import ContractError, load_contract
|
|
11
12
|
|
|
@@ -127,6 +128,22 @@ def test_contract_rejects_ambiguous_and_non_mapping_overlays(tmp_path: Path) ->
|
|
|
127
128
|
load_contract(tmp_path)
|
|
128
129
|
|
|
129
130
|
|
|
131
|
+
def test_unknown_contract_keys_report_runtime_version_and_upgrade_action(tmp_path: Path) -> None:
|
|
132
|
+
payload = {
|
|
133
|
+
"settings": {"future_setting": True},
|
|
134
|
+
"constraints": {"check": {"kind": "command", "command": ["true"]}},
|
|
135
|
+
}
|
|
136
|
+
(tmp_path / "constraintloop.yml").write_text(yaml.safe_dump(payload), encoding="utf-8")
|
|
137
|
+
|
|
138
|
+
with pytest.raises(ContractError) as raised:
|
|
139
|
+
load_contract(tmp_path)
|
|
140
|
+
|
|
141
|
+
message = str(raised.value)
|
|
142
|
+
assert "future_setting" in message
|
|
143
|
+
assert f"ConstraintLoop {__version__} does not recognize one or more keys" in message
|
|
144
|
+
assert "constraintloop setup --adapter all --project ." in message
|
|
145
|
+
|
|
146
|
+
|
|
130
147
|
@pytest.mark.parametrize(
|
|
131
148
|
("overlay", "field"),
|
|
132
149
|
[
|
|
@@ -7,6 +7,7 @@ from pathlib import Path
|
|
|
7
7
|
import pytest
|
|
8
8
|
import yaml
|
|
9
9
|
|
|
10
|
+
from constraintloop import __version__
|
|
10
11
|
from constraintloop.hooks import handle_hook
|
|
11
12
|
from constraintloop.setup_hooks import install_hooks, uninstall_hooks
|
|
12
13
|
from constraintloop.state import create_advisory_acknowledgment, load_latest_result, load_session
|
|
@@ -41,6 +42,20 @@ def test_stop_blocks_then_requires_human(tmp_path: Path, monkeypatch) -> None:
|
|
|
41
42
|
assert "ask a human" in second["stopReason"]
|
|
42
43
|
|
|
43
44
|
|
|
45
|
+
@pytest.mark.parametrize("active_field", ["stop_hook_active", "stopHookActive"])
|
|
46
|
+
def test_recursive_stop_hook_succeeds_without_loading_contract(
|
|
47
|
+
tmp_path: Path, active_field: str
|
|
48
|
+
) -> None:
|
|
49
|
+
response = handle_hook(
|
|
50
|
+
tmp_path,
|
|
51
|
+
"claude",
|
|
52
|
+
"stop",
|
|
53
|
+
{active_field: True},
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
assert response == {}
|
|
57
|
+
|
|
58
|
+
|
|
44
59
|
def test_pre_tool_protects_contract(tmp_path: Path) -> None:
|
|
45
60
|
response = handle_hook(
|
|
46
61
|
tmp_path,
|
|
@@ -125,7 +140,7 @@ def test_setup_pins_uvx_and_supports_explicit_hook_executable(tmp_path: Path, mo
|
|
|
125
140
|
command = json.loads(path.read_text(encoding="utf-8"))["hooks"]["Stop"][0]["hooks"][0][
|
|
126
141
|
"command"
|
|
127
142
|
]
|
|
128
|
-
assert command.startswith("uvx --from constraintloop==
|
|
143
|
+
assert command.startswith(f"uvx --from constraintloop=={__version__} constraintloop hook")
|
|
129
144
|
|
|
130
145
|
path = install_hooks(tmp_path, "codex", hook_executable="pipx run constraintloop")
|
|
131
146
|
command = json.loads(path.read_text(encoding="utf-8"))["hooks"]["Stop"][0]["hooks"][0][
|
|
@@ -6,6 +6,40 @@ from pydantic import ValidationError
|
|
|
6
6
|
from constraintloop.models import Contract
|
|
7
7
|
|
|
8
8
|
|
|
9
|
+
def test_contract_accepts_progress_interval_and_command_retry_policies() -> None:
|
|
10
|
+
contract = Contract.model_validate(
|
|
11
|
+
{
|
|
12
|
+
"settings": {"progress_interval_seconds": 10},
|
|
13
|
+
"constraints": {
|
|
14
|
+
"alembic_heads": {
|
|
15
|
+
"kind": "command",
|
|
16
|
+
"command": ["check-alembic-heads"],
|
|
17
|
+
"timeout_seconds": 300,
|
|
18
|
+
"retry": {
|
|
19
|
+
"max_attempts": 2,
|
|
20
|
+
"exit_codes": [1],
|
|
21
|
+
"total_timeout_seconds": 600,
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
"async_postgres_tests": {
|
|
25
|
+
"kind": "command",
|
|
26
|
+
"command": ["pytest", "-m", "async_postgres"],
|
|
27
|
+
"timeout_seconds": 900,
|
|
28
|
+
"retry": {
|
|
29
|
+
"max_attempts": 2,
|
|
30
|
+
"retry_timeouts": True,
|
|
31
|
+
"total_timeout_seconds": 1800,
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
assert contract.settings.progress_interval_seconds == 10
|
|
39
|
+
assert contract.constraints["alembic_heads"].retry is not None
|
|
40
|
+
assert contract.constraints["async_postgres_tests"].retry is not None
|
|
41
|
+
|
|
42
|
+
|
|
9
43
|
def test_required_rubric_demands_majority_quorum() -> None:
|
|
10
44
|
with pytest.raises(ValidationError, match="at least two runs"):
|
|
11
45
|
Contract.model_validate(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|