constraintloop 0.3.0__tar.gz → 0.4.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.4.1}/.gitignore +3 -0
- {constraintloop-0.3.0 → constraintloop-0.4.1}/CHANGELOG.md +28 -0
- {constraintloop-0.3.0 → constraintloop-0.4.1}/PKG-INFO +45 -15
- {constraintloop-0.3.0 → constraintloop-0.4.1}/README.md +44 -14
- {constraintloop-0.3.0 → constraintloop-0.4.1}/docs/configuration.md +11 -3
- {constraintloop-0.3.0 → constraintloop-0.4.1}/docs/faq.md +38 -3
- {constraintloop-0.3.0 → constraintloop-0.4.1}/docs/recipes.md +12 -4
- {constraintloop-0.3.0 → constraintloop-0.4.1}/pyproject.toml +1 -1
- {constraintloop-0.3.0 → constraintloop-0.4.1}/schema/constraintloop.schema.json +8 -0
- {constraintloop-0.3.0 → constraintloop-0.4.1}/scripts/check_coverage.py +2 -2
- {constraintloop-0.3.0 → constraintloop-0.4.1}/src/constraintloop/__init__.py +2 -2
- constraintloop-0.4.1/src/constraintloop/_process.py +63 -0
- {constraintloop-0.3.0 → constraintloop-0.4.1}/src/constraintloop/cli.py +32 -7
- {constraintloop-0.3.0 → constraintloop-0.4.1}/src/constraintloop/config.py +11 -1
- {constraintloop-0.3.0 → constraintloop-0.4.1}/src/constraintloop/engine.py +60 -3
- {constraintloop-0.3.0 → constraintloop-0.4.1}/src/constraintloop/evaluators.py +12 -9
- {constraintloop-0.3.0 → constraintloop-0.4.1}/src/constraintloop/hooks.py +52 -4
- {constraintloop-0.3.0 → constraintloop-0.4.1}/src/constraintloop/hygiene.py +4 -0
- {constraintloop-0.3.0 → constraintloop-0.4.1}/src/constraintloop/models.py +2 -0
- {constraintloop-0.3.0 → constraintloop-0.4.1}/src/constraintloop/native_cli_evaluator.py +3 -6
- {constraintloop-0.3.0 → constraintloop-0.4.1}/src/constraintloop/runners.py +12 -8
- {constraintloop-0.3.0 → constraintloop-0.4.1}/src/constraintloop/scaffold.py +1 -0
- {constraintloop-0.3.0 → constraintloop-0.4.1}/src/constraintloop/setup_hooks.py +126 -7
- {constraintloop-0.3.0 → constraintloop-0.4.1}/tests/test_cli.py +63 -1
- {constraintloop-0.3.0 → constraintloop-0.4.1}/tests/test_cli_commands.py +278 -0
- {constraintloop-0.3.0 → constraintloop-0.4.1}/tests/test_engine.py +35 -0
- {constraintloop-0.3.0 → constraintloop-0.4.1}/tests/test_environment.py +13 -0
- {constraintloop-0.3.0 → constraintloop-0.4.1}/tests/test_eval_corpus.py +33 -0
- {constraintloop-0.3.0 → constraintloop-0.4.1}/tests/test_hooks.py +201 -6
- {constraintloop-0.3.0 → constraintloop-0.4.1}/tests/test_models.py +91 -0
- {constraintloop-0.3.0 → constraintloop-0.4.1}/tests/test_native_cli_evaluator.py +109 -8
- constraintloop-0.4.1/tests/test_process.py +50 -0
- {constraintloop-0.3.0 → constraintloop-0.4.1}/tests/test_runners.py +38 -0
- {constraintloop-0.3.0 → constraintloop-0.4.1}/CONTRIBUTING.md +0 -0
- {constraintloop-0.3.0 → constraintloop-0.4.1}/GOVERNANCE.md +0 -0
- {constraintloop-0.3.0 → constraintloop-0.4.1}/LICENSE +0 -0
- {constraintloop-0.3.0 → constraintloop-0.4.1}/RELEASE.md +0 -0
- {constraintloop-0.3.0 → constraintloop-0.4.1}/SECURITY.md +0 -0
- {constraintloop-0.3.0 → constraintloop-0.4.1}/SUPPORT.md +0 -0
- {constraintloop-0.3.0 → constraintloop-0.4.1}/docs/convergence-loops.md +0 -0
- {constraintloop-0.3.0 → constraintloop-0.4.1}/docs/native-cli-evaluators.md +0 -0
- {constraintloop-0.3.0 → constraintloop-0.4.1}/docs/openai-evaluation.md +0 -0
- {constraintloop-0.3.0 → constraintloop-0.4.1}/docs/provider-privacy.md +0 -0
- {constraintloop-0.3.0 → constraintloop-0.4.1}/docs/release-readiness.md +0 -0
- {constraintloop-0.3.0 → constraintloop-0.4.1}/docs/threat-model.md +0 -0
- {constraintloop-0.3.0 → constraintloop-0.4.1}/scripts/check_anthropic_sdk.py +0 -0
- {constraintloop-0.3.0 → constraintloop-0.4.1}/scripts/check_openai_sdk.py +0 -0
- {constraintloop-0.3.0 → constraintloop-0.4.1}/scripts/check_sdist_contents.py +0 -0
- {constraintloop-0.3.0 → constraintloop-0.4.1}/scripts/generate_schema.py +0 -0
- {constraintloop-0.3.0 → constraintloop-0.4.1}/scripts/openai_eval_canary.py +0 -0
- {constraintloop-0.3.0 → constraintloop-0.4.1}/scripts/wheel_failure_smoke.py +0 -0
- {constraintloop-0.3.0 → constraintloop-0.4.1}/src/constraintloop/__main__.py +0 -0
- {constraintloop-0.3.0 → constraintloop-0.4.1}/src/constraintloop/diagnostics.py +0 -0
- {constraintloop-0.3.0 → constraintloop-0.4.1}/src/constraintloop/digest.py +0 -0
- {constraintloop-0.3.0 → constraintloop-0.4.1}/src/constraintloop/environment.py +0 -0
- {constraintloop-0.3.0 → constraintloop-0.4.1}/src/constraintloop/eval_corpus.py +0 -0
- {constraintloop-0.3.0 → constraintloop-0.4.1}/src/constraintloop/loops.py +0 -0
- {constraintloop-0.3.0 → constraintloop-0.4.1}/src/constraintloop/py.typed +0 -0
- {constraintloop-0.3.0 → constraintloop-0.4.1}/src/constraintloop/state.py +0 -0
- {constraintloop-0.3.0 → constraintloop-0.4.1}/tests/__init__.py +0 -0
- {constraintloop-0.3.0 → constraintloop-0.4.1}/tests/failure_lab.py +0 -0
- {constraintloop-0.3.0 → constraintloop-0.4.1}/tests/fixtures/openai_eval_corpus_v1.yml +0 -0
- {constraintloop-0.3.0 → constraintloop-0.4.1}/tests/test_diagnostics.py +0 -0
- {constraintloop-0.3.0 → constraintloop-0.4.1}/tests/test_digest.py +0 -0
- {constraintloop-0.3.0 → constraintloop-0.4.1}/tests/test_evaluators.py +0 -0
- {constraintloop-0.3.0 → constraintloop-0.4.1}/tests/test_failure_lab.py +0 -0
- {constraintloop-0.3.0 → constraintloop-0.4.1}/tests/test_loops.py +0 -0
- {constraintloop-0.3.0 → constraintloop-0.4.1}/tests/test_release_metadata.py +0 -0
- {constraintloop-0.3.0 → constraintloop-0.4.1}/tests/test_security.py +0 -0
- {constraintloop-0.3.0 → constraintloop-0.4.1}/tests/test_state.py +0 -0
|
@@ -5,6 +5,34 @@ Versioning, with the usual initial-development flexibility for `0.y.z`.
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.4.1] - 2026-09-04
|
|
9
|
+
|
|
10
|
+
- Raise enforced statement coverage from 90% to 95% and branch coverage from
|
|
11
|
+
80% to 90%.
|
|
12
|
+
- Add behavioral coverage for lifecycle CLI boundaries, hook configuration
|
|
13
|
+
failures, native evaluator entrypoints, timeout escalation, project
|
|
14
|
+
scaffolding, environment parsing, and contract validation.
|
|
15
|
+
|
|
16
|
+
## [0.4.0] - 2026-09-04
|
|
17
|
+
|
|
18
|
+
- Enforce finite command timeouts across complete POSIX process groups so
|
|
19
|
+
leaked Docker/TestContainers descendants cannot hold Stop gates open.
|
|
20
|
+
- Add an explicit `push` phase and opt-in managed Git pre-push hook for heavy
|
|
21
|
+
integration gates while keeping frequent Stop checks fast.
|
|
22
|
+
- Install Claude hooks in gitignored local settings, persist explicit uninstall
|
|
23
|
+
tombstones, and remove ConstraintLoop's own committed agent settings files.
|
|
24
|
+
- Defer lifecycle evaluation for subagents and turns paused on background or
|
|
25
|
+
scheduled work.
|
|
26
|
+
- Compact hook failure output to counts, failing checks, and high-signal error
|
|
27
|
+
lines while retaining full evidence for `constraintloop debug`.
|
|
28
|
+
- Run commands and command evaluators from a stable project context with the
|
|
29
|
+
project root on `PYTHONPATH`.
|
|
30
|
+
|
|
31
|
+
## [0.3.1] - 2026-08-31
|
|
32
|
+
|
|
33
|
+
- Prevent recursive Claude Stop-hook invocations from repeatedly blocking completion.
|
|
34
|
+
- Add actionable version-skew guidance when a hook runtime rejects unknown contract keys.
|
|
35
|
+
|
|
8
36
|
## [0.3.0] - 2026-08-18
|
|
9
37
|
|
|
10
38
|
- 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
|
+
Version: 0.4.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
|
|
@@ -123,6 +123,7 @@ uv tool install constraintloop
|
|
|
123
123
|
|
|
124
124
|
constraintloop init
|
|
125
125
|
constraintloop setup --adapter all
|
|
126
|
+
# Or add --pre-push above to wire heavyweight push gates locally.
|
|
126
127
|
constraintloop run
|
|
127
128
|
constraintloop ci
|
|
128
129
|
```
|
|
@@ -137,7 +138,7 @@ If you intentionally run setup through `uvx`, generated hooks pin the current
|
|
|
137
138
|
ConstraintLoop version. You can choose another persistent invocation with, for
|
|
138
139
|
example, `constraintloop setup --hook-executable "pipx run constraintloop"`.
|
|
139
140
|
|
|
140
|
-
The five commands above establish this flow:
|
|
141
|
+
The five core commands above establish this flow:
|
|
141
142
|
|
|
142
143
|
```mermaid
|
|
143
144
|
sequenceDiagram
|
|
@@ -147,7 +148,7 @@ sequenceDiagram
|
|
|
147
148
|
participant T as Project tools
|
|
148
149
|
U->>CL: init + review contract
|
|
149
150
|
U->>CL: setup hooks
|
|
150
|
-
A->>CL: run change/stop phase
|
|
151
|
+
A->>CL: run change/stop/push phase
|
|
151
152
|
CL->>T: execute ready constraints
|
|
152
153
|
T-->>CL: exit codes, metrics, artifacts
|
|
153
154
|
CL-->>A: pass, repair, wait, or escalate
|
|
@@ -160,18 +161,27 @@ sequenceDiagram
|
|
|
160
161
|
version: 1
|
|
161
162
|
settings:
|
|
162
163
|
max_auto_retries: 2
|
|
164
|
+
hook_output_limit: 4096
|
|
163
165
|
|
|
164
166
|
constraints:
|
|
165
167
|
tests:
|
|
166
168
|
kind: command
|
|
167
169
|
command: [python, -m, pytest, -q]
|
|
168
|
-
phases: [stop, ci]
|
|
170
|
+
phases: [stop, push, ci]
|
|
169
171
|
watch: ["src/**/*.py", "tests/**/*.py", pyproject.toml]
|
|
170
172
|
retry:
|
|
171
173
|
max_attempts: 3
|
|
172
174
|
exit_codes: [1]
|
|
173
175
|
delay_seconds: 2
|
|
174
176
|
|
|
177
|
+
integration_tests:
|
|
178
|
+
kind: command
|
|
179
|
+
command: [python, -m, pytest, -q, -m, integration]
|
|
180
|
+
phases: [push, ci]
|
|
181
|
+
watch: ["src/**/*.py", "tests/**/*.py", pyproject.toml]
|
|
182
|
+
needs: [tests]
|
|
183
|
+
timeout_seconds: 900
|
|
184
|
+
|
|
175
185
|
coverage:
|
|
176
186
|
kind: metric
|
|
177
187
|
command: [python, -m, pytest, --cov, "--cov-report=json:coverage.json"]
|
|
@@ -266,16 +276,22 @@ advisory until their false-positive and false-negative rates are measured.
|
|
|
266
276
|
| --- | --- | --- |
|
|
267
277
|
| `change` | After a file-changing tool action | Fast syntax, formatting, or diff checks |
|
|
268
278
|
| `stop` | When the agent attempts to finish | Tests, build checks, and advisory review |
|
|
279
|
+
| `push` | Explicit local run or opt-in Git pre-push hook | Full integration and platform suites |
|
|
269
280
|
| `ci` | Protected hosted workflow | Authoritative uncached and waiver-free verification |
|
|
270
281
|
|
|
271
282
|
1. `SessionStart` tells the coding agent which required gates exist.
|
|
272
283
|
2. The prompt hook records the user's goal as review evidence.
|
|
273
284
|
3. Before tool execution, agent attempts to edit the contract or create a
|
|
274
285
|
waiver are denied.
|
|
275
|
-
4. After tool execution, `change` gates run and fresh results are
|
|
286
|
+
4. After a main-agent tool execution, `change` gates run and fresh results are
|
|
287
|
+
injected. Subagent tool and stop events are ignored because their working
|
|
288
|
+
tree may be intentionally transient.
|
|
276
289
|
5. Before compaction, the completion policy is restated.
|
|
277
290
|
6. At `Stop` / `AfterAgent`, required `stop` gates block completion. The agent
|
|
278
|
-
receives
|
|
291
|
+
receives compact evidence and may repair the code a bounded number of times.
|
|
292
|
+
ConstraintLoop defers this evaluation while background tasks or scheduled
|
|
293
|
+
wakeups are active. Full retained output remains available through
|
|
294
|
+
`constraintloop debug ID`.
|
|
279
295
|
7. Advisory failures require either passing fresh evidence or an explicit
|
|
280
296
|
snapshot-bound explanation; delivery alone never counts as review.
|
|
281
297
|
8. Repeated required failure stops autonomous repair and requests a human
|
|
@@ -285,6 +301,12 @@ advisory until their false-positive and false-negative rates are measured.
|
|
|
285
301
|
is human.
|
|
286
302
|
9. `constraintloop ci` reruns every CI gate without local evidence or waivers.
|
|
287
303
|
|
|
304
|
+
Every command constraint has a finite timeout (300 seconds by default). On POSIX,
|
|
305
|
+
timeout cleanup terminates the entire spawned process group so TestContainers,
|
|
306
|
+
Docker clients, and other descendants cannot keep inherited output pipes open.
|
|
307
|
+
Commands run from their configured project-contained `cwd`, with the selected
|
|
308
|
+
project root prepended to `PYTHONPATH`.
|
|
309
|
+
|
|
288
310
|
Evidence is keyed by the constraint definition and the bytes of every file
|
|
289
311
|
matched by `watch`. A source change therefore makes old evidence and waivers
|
|
290
312
|
stale without a mutable invalidation list. Local state lives under the
|
|
@@ -295,8 +317,12 @@ For stronger machine-local gates, create a gitignored
|
|
|
295
317
|
`constraintloop.local.yml`. ConstraintLoop recursively merges mappings over the
|
|
296
318
|
repository contract and rejects changes that could weaken committed gates.
|
|
297
319
|
The authoritative `constraintloop ci` command ignores this overlay.
|
|
298
|
-
`init` and `setup` add the
|
|
299
|
-
selected project's `.gitignore`, and warn if
|
|
320
|
+
`init` and `setup` add local state, the uninstall tombstone, generated agent hook
|
|
321
|
+
settings, and overlay names to the selected project's `.gitignore`, and warn if
|
|
322
|
+
state is already tracked. Claude uses its dedicated
|
|
323
|
+
`.claude/settings.local.json` path. Explicit uninstall records a local tombstone,
|
|
324
|
+
so a checkout that restores old committed hook wiring does not silently
|
|
325
|
+
reactivate ConstraintLoop; setup clears the tombstone.
|
|
300
326
|
|
|
301
327
|
### Verdicts and what they mean
|
|
302
328
|
|
|
@@ -313,11 +339,14 @@ selected project's `.gitignore`, and warn if state is already tracked.
|
|
|
313
339
|
## Commands
|
|
314
340
|
|
|
315
341
|
- `constraintloop init` — generate a reviewable initial contract.
|
|
316
|
-
- `constraintloop setup --adapter claude|codex|gemini|all` — merge
|
|
317
|
-
while preserving existing hooks
|
|
342
|
+
- `constraintloop setup --adapter claude|codex|gemini|all [--pre-push]` — merge
|
|
343
|
+
agent hook entries while preserving existing hooks; optionally install an
|
|
344
|
+
owned Git pre-push hook for `push` gates.
|
|
318
345
|
- `constraintloop uninstall --adapter claude|codex|gemini|all` — remove only
|
|
319
|
-
ConstraintLoop hook entries while preserving unrelated settings
|
|
320
|
-
-
|
|
346
|
+
ConstraintLoop hook entries while preserving unrelated settings; pass
|
|
347
|
+
`--pre-push` to remove an owned Git hook too.
|
|
348
|
+
- `constraintloop run --phase change|stop|push` — run local gates with fresh
|
|
349
|
+
caching. Push gates do not honor local waivers.
|
|
321
350
|
- `constraintloop ci` — authoritative, uncached, waiver-free run.
|
|
322
351
|
- `constraintloop cycle NAME --json` — execute one journaled loop transition.
|
|
323
352
|
- `constraintloop supervise NAME` — poll pending evidence under a recoverable
|
|
@@ -325,7 +354,7 @@ selected project's `.gitignore`, and warn if state is already tracked.
|
|
|
325
354
|
- `constraintloop loop-prompt NAME --adapter claude|codex` — print the bounded
|
|
326
355
|
native-agent repair protocol without launching an agent.
|
|
327
356
|
- `constraintloop status` — inspect evidence without executing commands.
|
|
328
|
-
- `constraintloop explain --phase change|stop|ci` — show why each constraint
|
|
357
|
+
- `constraintloop explain --phase change|stop|push|ci` — show why each constraint
|
|
329
358
|
runs or is skipped, including matched and changed watch paths, cache state,
|
|
330
359
|
and dependency chains.
|
|
331
360
|
- `constraintloop baseline update ID|--all` — initialize or strengthen native
|
|
@@ -381,7 +410,7 @@ responses become `uncertain`; a required rubric therefore fails closed.
|
|
|
381
410
|
|
|
382
411
|
## Compatibility boundary
|
|
383
412
|
|
|
384
|
-
The supported v0.
|
|
413
|
+
The supported v0.4 surfaces are the CLI and exit codes, configuration schema,
|
|
385
414
|
evaluator command protocol, native hook responses, and schema-versioned
|
|
386
415
|
evidence and cycle JSON. Python submodules are internal during initial
|
|
387
416
|
development and are not covered by semantic-versioning compatibility promises.
|
|
@@ -401,7 +430,8 @@ policy. ConstraintLoop records which contract ran, which inputs it covered, and
|
|
|
401
430
|
whether the evidence is still fresh.
|
|
402
431
|
|
|
403
432
|
**Why do some constraints run after every action?** Put only fast feedback in
|
|
404
|
-
the `change` phase.
|
|
433
|
+
the `change` phase. Keep unit checks in `stop`; put heavyweight integration
|
|
434
|
+
suites in `push` and `ci`.
|
|
405
435
|
|
|
406
436
|
**Can I use Codex or Claude Code instead of an API evaluator?** Yes. The native
|
|
407
437
|
evaluator adapter prefers the active supported CLI and remains read-only.
|
|
@@ -80,6 +80,7 @@ uv tool install constraintloop
|
|
|
80
80
|
|
|
81
81
|
constraintloop init
|
|
82
82
|
constraintloop setup --adapter all
|
|
83
|
+
# Or add --pre-push above to wire heavyweight push gates locally.
|
|
83
84
|
constraintloop run
|
|
84
85
|
constraintloop ci
|
|
85
86
|
```
|
|
@@ -94,7 +95,7 @@ If you intentionally run setup through `uvx`, generated hooks pin the current
|
|
|
94
95
|
ConstraintLoop version. You can choose another persistent invocation with, for
|
|
95
96
|
example, `constraintloop setup --hook-executable "pipx run constraintloop"`.
|
|
96
97
|
|
|
97
|
-
The five commands above establish this flow:
|
|
98
|
+
The five core commands above establish this flow:
|
|
98
99
|
|
|
99
100
|
```mermaid
|
|
100
101
|
sequenceDiagram
|
|
@@ -104,7 +105,7 @@ sequenceDiagram
|
|
|
104
105
|
participant T as Project tools
|
|
105
106
|
U->>CL: init + review contract
|
|
106
107
|
U->>CL: setup hooks
|
|
107
|
-
A->>CL: run change/stop phase
|
|
108
|
+
A->>CL: run change/stop/push phase
|
|
108
109
|
CL->>T: execute ready constraints
|
|
109
110
|
T-->>CL: exit codes, metrics, artifacts
|
|
110
111
|
CL-->>A: pass, repair, wait, or escalate
|
|
@@ -117,18 +118,27 @@ sequenceDiagram
|
|
|
117
118
|
version: 1
|
|
118
119
|
settings:
|
|
119
120
|
max_auto_retries: 2
|
|
121
|
+
hook_output_limit: 4096
|
|
120
122
|
|
|
121
123
|
constraints:
|
|
122
124
|
tests:
|
|
123
125
|
kind: command
|
|
124
126
|
command: [python, -m, pytest, -q]
|
|
125
|
-
phases: [stop, ci]
|
|
127
|
+
phases: [stop, push, ci]
|
|
126
128
|
watch: ["src/**/*.py", "tests/**/*.py", pyproject.toml]
|
|
127
129
|
retry:
|
|
128
130
|
max_attempts: 3
|
|
129
131
|
exit_codes: [1]
|
|
130
132
|
delay_seconds: 2
|
|
131
133
|
|
|
134
|
+
integration_tests:
|
|
135
|
+
kind: command
|
|
136
|
+
command: [python, -m, pytest, -q, -m, integration]
|
|
137
|
+
phases: [push, ci]
|
|
138
|
+
watch: ["src/**/*.py", "tests/**/*.py", pyproject.toml]
|
|
139
|
+
needs: [tests]
|
|
140
|
+
timeout_seconds: 900
|
|
141
|
+
|
|
132
142
|
coverage:
|
|
133
143
|
kind: metric
|
|
134
144
|
command: [python, -m, pytest, --cov, "--cov-report=json:coverage.json"]
|
|
@@ -223,16 +233,22 @@ advisory until their false-positive and false-negative rates are measured.
|
|
|
223
233
|
| --- | --- | --- |
|
|
224
234
|
| `change` | After a file-changing tool action | Fast syntax, formatting, or diff checks |
|
|
225
235
|
| `stop` | When the agent attempts to finish | Tests, build checks, and advisory review |
|
|
236
|
+
| `push` | Explicit local run or opt-in Git pre-push hook | Full integration and platform suites |
|
|
226
237
|
| `ci` | Protected hosted workflow | Authoritative uncached and waiver-free verification |
|
|
227
238
|
|
|
228
239
|
1. `SessionStart` tells the coding agent which required gates exist.
|
|
229
240
|
2. The prompt hook records the user's goal as review evidence.
|
|
230
241
|
3. Before tool execution, agent attempts to edit the contract or create a
|
|
231
242
|
waiver are denied.
|
|
232
|
-
4. After tool execution, `change` gates run and fresh results are
|
|
243
|
+
4. After a main-agent tool execution, `change` gates run and fresh results are
|
|
244
|
+
injected. Subagent tool and stop events are ignored because their working
|
|
245
|
+
tree may be intentionally transient.
|
|
233
246
|
5. Before compaction, the completion policy is restated.
|
|
234
247
|
6. At `Stop` / `AfterAgent`, required `stop` gates block completion. The agent
|
|
235
|
-
receives
|
|
248
|
+
receives compact evidence and may repair the code a bounded number of times.
|
|
249
|
+
ConstraintLoop defers this evaluation while background tasks or scheduled
|
|
250
|
+
wakeups are active. Full retained output remains available through
|
|
251
|
+
`constraintloop debug ID`.
|
|
236
252
|
7. Advisory failures require either passing fresh evidence or an explicit
|
|
237
253
|
snapshot-bound explanation; delivery alone never counts as review.
|
|
238
254
|
8. Repeated required failure stops autonomous repair and requests a human
|
|
@@ -242,6 +258,12 @@ advisory until their false-positive and false-negative rates are measured.
|
|
|
242
258
|
is human.
|
|
243
259
|
9. `constraintloop ci` reruns every CI gate without local evidence or waivers.
|
|
244
260
|
|
|
261
|
+
Every command constraint has a finite timeout (300 seconds by default). On POSIX,
|
|
262
|
+
timeout cleanup terminates the entire spawned process group so TestContainers,
|
|
263
|
+
Docker clients, and other descendants cannot keep inherited output pipes open.
|
|
264
|
+
Commands run from their configured project-contained `cwd`, with the selected
|
|
265
|
+
project root prepended to `PYTHONPATH`.
|
|
266
|
+
|
|
245
267
|
Evidence is keyed by the constraint definition and the bytes of every file
|
|
246
268
|
matched by `watch`. A source change therefore makes old evidence and waivers
|
|
247
269
|
stale without a mutable invalidation list. Local state lives under the
|
|
@@ -252,8 +274,12 @@ For stronger machine-local gates, create a gitignored
|
|
|
252
274
|
`constraintloop.local.yml`. ConstraintLoop recursively merges mappings over the
|
|
253
275
|
repository contract and rejects changes that could weaken committed gates.
|
|
254
276
|
The authoritative `constraintloop ci` command ignores this overlay.
|
|
255
|
-
`init` and `setup` add the
|
|
256
|
-
selected project's `.gitignore`, and warn if
|
|
277
|
+
`init` and `setup` add local state, the uninstall tombstone, generated agent hook
|
|
278
|
+
settings, and overlay names to the selected project's `.gitignore`, and warn if
|
|
279
|
+
state is already tracked. Claude uses its dedicated
|
|
280
|
+
`.claude/settings.local.json` path. Explicit uninstall records a local tombstone,
|
|
281
|
+
so a checkout that restores old committed hook wiring does not silently
|
|
282
|
+
reactivate ConstraintLoop; setup clears the tombstone.
|
|
257
283
|
|
|
258
284
|
### Verdicts and what they mean
|
|
259
285
|
|
|
@@ -270,11 +296,14 @@ selected project's `.gitignore`, and warn if state is already tracked.
|
|
|
270
296
|
## Commands
|
|
271
297
|
|
|
272
298
|
- `constraintloop init` — generate a reviewable initial contract.
|
|
273
|
-
- `constraintloop setup --adapter claude|codex|gemini|all` — merge
|
|
274
|
-
while preserving existing hooks
|
|
299
|
+
- `constraintloop setup --adapter claude|codex|gemini|all [--pre-push]` — merge
|
|
300
|
+
agent hook entries while preserving existing hooks; optionally install an
|
|
301
|
+
owned Git pre-push hook for `push` gates.
|
|
275
302
|
- `constraintloop uninstall --adapter claude|codex|gemini|all` — remove only
|
|
276
|
-
ConstraintLoop hook entries while preserving unrelated settings
|
|
277
|
-
-
|
|
303
|
+
ConstraintLoop hook entries while preserving unrelated settings; pass
|
|
304
|
+
`--pre-push` to remove an owned Git hook too.
|
|
305
|
+
- `constraintloop run --phase change|stop|push` — run local gates with fresh
|
|
306
|
+
caching. Push gates do not honor local waivers.
|
|
278
307
|
- `constraintloop ci` — authoritative, uncached, waiver-free run.
|
|
279
308
|
- `constraintloop cycle NAME --json` — execute one journaled loop transition.
|
|
280
309
|
- `constraintloop supervise NAME` — poll pending evidence under a recoverable
|
|
@@ -282,7 +311,7 @@ selected project's `.gitignore`, and warn if state is already tracked.
|
|
|
282
311
|
- `constraintloop loop-prompt NAME --adapter claude|codex` — print the bounded
|
|
283
312
|
native-agent repair protocol without launching an agent.
|
|
284
313
|
- `constraintloop status` — inspect evidence without executing commands.
|
|
285
|
-
- `constraintloop explain --phase change|stop|ci` — show why each constraint
|
|
314
|
+
- `constraintloop explain --phase change|stop|push|ci` — show why each constraint
|
|
286
315
|
runs or is skipped, including matched and changed watch paths, cache state,
|
|
287
316
|
and dependency chains.
|
|
288
317
|
- `constraintloop baseline update ID|--all` — initialize or strengthen native
|
|
@@ -338,7 +367,7 @@ responses become `uncertain`; a required rubric therefore fails closed.
|
|
|
338
367
|
|
|
339
368
|
## Compatibility boundary
|
|
340
369
|
|
|
341
|
-
The supported v0.
|
|
370
|
+
The supported v0.4 surfaces are the CLI and exit codes, configuration schema,
|
|
342
371
|
evaluator command protocol, native hook responses, and schema-versioned
|
|
343
372
|
evidence and cycle JSON. Python submodules are internal during initial
|
|
344
373
|
development and are not covered by semantic-versioning compatibility promises.
|
|
@@ -358,7 +387,8 @@ policy. ConstraintLoop records which contract ran, which inputs it covered, and
|
|
|
358
387
|
whether the evidence is still fresh.
|
|
359
388
|
|
|
360
389
|
**Why do some constraints run after every action?** Put only fast feedback in
|
|
361
|
-
the `change` phase.
|
|
390
|
+
the `change` phase. Keep unit checks in `stop`; put heavyweight integration
|
|
391
|
+
suites in `push` and `ci`.
|
|
362
392
|
|
|
363
393
|
**Can I use Codex or Claude Code instead of an API evaluator?** Yes. The native
|
|
364
394
|
evaluator adapter prefers the active supported CLI and remains read-only.
|
|
@@ -22,6 +22,7 @@ Settings default to:
|
|
|
22
22
|
| `max_auto_retries` | 2 | 0–20 |
|
|
23
23
|
| `concurrency` | 4 | 1–32 |
|
|
24
24
|
| `evidence_output_limit` | 65536 | 1024–1048576 bytes |
|
|
25
|
+
| `hook_output_limit` | 4096 | 512–32768 bytes |
|
|
25
26
|
| `evaluation_bundle_limit` | 102400 | 4096–2097152 bytes |
|
|
26
27
|
| `progress_interval_seconds` | 15 | 0.1–300 seconds |
|
|
27
28
|
|
|
@@ -37,7 +38,7 @@ authoritative `constraintloop ci` command ignores local overlays and always
|
|
|
37
38
|
evaluates the committed repository contract.
|
|
38
39
|
|
|
39
40
|
Every constraint supports `description`, `enforcement` (`required` or
|
|
40
|
-
`advisory`), `phases` (`change`, `stop`, `ci`), `watch` globs, dependency IDs
|
|
41
|
+
`advisory`), `phases` (`change`, `stop`, `push`, `ci`), `watch` globs, dependency IDs
|
|
41
42
|
in `needs`, `timeout_seconds`, and `enabled`. Dependencies must exist and the
|
|
42
43
|
graph must be acyclic.
|
|
43
44
|
Identifiers may contain letters, numbers, dots, underscores, and hyphens.
|
|
@@ -63,13 +64,17 @@ retry:
|
|
|
63
64
|
|
|
64
65
|
No retries occur when `retry` is absent. A configured policy retries only the
|
|
65
66
|
listed exit codes and, when enabled, timeouts or process startup failures.
|
|
66
|
-
Every attempt is capped by the constraint's normal timeout
|
|
67
|
+
Every attempt is capped by the constraint's normal timeout, which defaults to
|
|
68
|
+
300 seconds and is always finite. On POSIX, a timeout terminates the entire
|
|
69
|
+
spawned process group, including descendants that inherited the command's output
|
|
70
|
+
pipes. Retry and periodic
|
|
67
71
|
running status lines are emitted during human-readable runs; `--json` remains a
|
|
68
72
|
single machine-readable document. `timeout_seconds` bounds each attempt and,
|
|
69
73
|
unless overridden, the complete retry sequence including delays. Timeout
|
|
70
74
|
retries require an explicit `total_timeout_seconds` greater than the per-attempt
|
|
71
75
|
timeout. This keeps the total bound visible while leaving enough budget for a
|
|
72
|
-
second attempt.
|
|
76
|
+
second attempt. Command and command-evaluator processes run from the selected
|
|
77
|
+
project root by default, and ConstraintLoop prepends that root to `PYTHONPATH`.
|
|
73
78
|
|
|
74
79
|
Metric constraints add `parser` and `threshold`. A parser has type `json` or
|
|
75
80
|
`regex`, reads `stdout`, `stderr`, or a project-contained `file`, and selects a
|
|
@@ -133,4 +138,7 @@ state, and dependency chains without executing any gate. Human-readable final
|
|
|
133
138
|
summaries label concrete policy failures as `constraint` and startup,
|
|
134
139
|
prerequisite, or evaluation errors as `environment`; the same
|
|
135
140
|
`failure_category` is retained in JSON evidence.
|
|
141
|
+
Hook responses use `hook_output_limit` to retain failing test names and the first
|
|
142
|
+
useful traceback line without injecting the complete test log. The unabridged
|
|
143
|
+
retained tail remains available with `constraintloop debug CONSTRAINT`.
|
|
136
144
|
See `docs/convergence-loops.md` for the cycle protocol and stable exit codes.
|
|
@@ -24,10 +24,11 @@ They should have different purposes:
|
|
|
24
24
|
| --- | --- |
|
|
25
25
|
| `change` | Fast feedback after edits |
|
|
26
26
|
| `stop` | Complete local evidence before the agent finishes |
|
|
27
|
+
| `push` | Opt-in heavyweight local evidence before Git push |
|
|
27
28
|
| `ci` | Independent, uncached, waiver-free verification |
|
|
28
29
|
|
|
29
|
-
If full tests run after every edit, remove `change` from that constraint
|
|
30
|
-
|
|
30
|
+
If full tests run after every edit, remove `change` from that constraint. Keep
|
|
31
|
+
fast unit tests in `stop`; put full integration suites in `push` and `ci`.
|
|
31
32
|
|
|
32
33
|
### What is the difference between a failure, uncertainty, and pending state?
|
|
33
34
|
|
|
@@ -134,9 +135,27 @@ trusted completion boundary.
|
|
|
134
135
|
No. `constraintloop setup` merges owned hook entries and preserves unrelated
|
|
135
136
|
configuration. In a monorepo, hooks retain the exact directory selected with
|
|
136
137
|
`--project` instead of falling back to the Git root. `constraintloop uninstall`
|
|
137
|
-
removes only owned entries
|
|
138
|
+
removes only owned entries and writes a gitignored local tombstone. Restoring an
|
|
139
|
+
old committed settings file therefore cannot silently reactivate an explicitly
|
|
140
|
+
uninstalled hook; running setup again clears the tombstone. Claude hooks are
|
|
141
|
+
installed into `.claude/settings.local.json`, not the shareable
|
|
142
|
+
`.claude/settings.json`. Review and trust newly installed Codex project hooks
|
|
138
143
|
through `/hooks`.
|
|
139
144
|
|
|
145
|
+
### Why did a Stop gate not run while background work was active?
|
|
146
|
+
|
|
147
|
+
ConstraintLoop evaluates completion only for the main agent at an idle turn
|
|
148
|
+
boundary. It ignores subagent hook events and defers Stop/AfterAgent while the
|
|
149
|
+
hook payload reports background tasks or scheduled wakeups. Run
|
|
150
|
+
`constraintloop run --phase stop` explicitly when you want an immediate check.
|
|
151
|
+
|
|
152
|
+
### How do I keep integration tests off the frequent Stop gate?
|
|
153
|
+
|
|
154
|
+
Assign them to `phases: [push, ci]`. Run them explicitly with
|
|
155
|
+
`constraintloop run --phase push`, or install the local pre-push integration with
|
|
156
|
+
`constraintloop setup --adapter all --pre-push`. Existing non-ConstraintLoop Git
|
|
157
|
+
hooks are never overwritten.
|
|
158
|
+
|
|
140
159
|
### Why does a hook say `Missing option --project`?
|
|
141
160
|
|
|
142
161
|
Regenerate the adapter configuration:
|
|
@@ -149,6 +168,22 @@ Generated commands include the resolved project argument. If the error remains,
|
|
|
149
168
|
inspect the relevant `.claude`, `.codex`, or `.gemini` settings file for a stale
|
|
150
169
|
manually copied command.
|
|
151
170
|
|
|
171
|
+
### Why does a hook reject documented contract keys as extra inputs?
|
|
172
|
+
|
|
173
|
+
The hook is running an older ConstraintLoop executable than the contract expects.
|
|
174
|
+
Upgrade that installation, then regenerate the hook commands so they resolve to the
|
|
175
|
+
same release:
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
constraintloop --version
|
|
179
|
+
constraintloop setup --adapter all --project .
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Current releases include their version and this recovery action when strict schema
|
|
183
|
+
validation encounters unknown keys. Recursive Claude Stop-hook calls are allowed to
|
|
184
|
+
finish after delivering the error once, so an invalid or version-skewed contract does
|
|
185
|
+
not create a repeated Stop-hook loop.
|
|
186
|
+
|
|
152
187
|
## Loops
|
|
153
188
|
|
|
154
189
|
### Can I configure an unlimited repair loop?
|
|
@@ -9,8 +9,8 @@ silently from the strict schema.
|
|
|
9
9
|
| Check | Recommended phase | Why |
|
|
10
10
|
| --- | --- | --- |
|
|
11
11
|
| Diff whitespace, syntax, targeted formatter | `change` | Fast enough to run after edits |
|
|
12
|
-
| Unit tests, type checking, package build | `stop`, `ci` | Useful before completion
|
|
13
|
-
| Full integration or platform suite | `ci` |
|
|
12
|
+
| Unit tests, type checking, package build | `stop`, `push`, `ci` | Useful before completion and cheap enough to repeat before push |
|
|
13
|
+
| Full integration or platform suite | `push`, `ci` | Local only when explicitly requested; authoritative in CI |
|
|
14
14
|
| Design/security rubric | `stop`, optionally `ci` | Needs a complete patch and bounded evidence bundle |
|
|
15
15
|
| Deployment or review status | `stop`, `ci` with pending code | External state may need polling |
|
|
16
16
|
|
|
@@ -28,8 +28,16 @@ constraintloop run --phase stop
|
|
|
28
28
|
constraintloop ci
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
The example separates fast syntax checking from session-end tests and
|
|
32
|
-
|
|
31
|
+
The example separates fast syntax checking from session-end tests, coverage, and
|
|
32
|
+
push-only integration tests. Coverage and integration depend on unit tests, so a
|
|
33
|
+
unit failure prevents redundant work. Install the optional local Git trigger with:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
constraintloop setup --adapter all --pre-push
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
If another tool already owns `.git/hooks/pre-push`, ConstraintLoop refuses to
|
|
40
|
+
replace it; add `constraintloop run --phase push` to that hook manually.
|
|
33
41
|
|
|
34
42
|
## Native Codex or Claude Code review
|
|
35
43
|
|
|
@@ -369,6 +369,13 @@
|
|
|
369
369
|
"title": "Evidence Output Limit",
|
|
370
370
|
"type": "integer"
|
|
371
371
|
},
|
|
372
|
+
"hook_output_limit": {
|
|
373
|
+
"default": 4096,
|
|
374
|
+
"maximum": 32768,
|
|
375
|
+
"minimum": 512,
|
|
376
|
+
"title": "Hook Output Limit",
|
|
377
|
+
"type": "integer"
|
|
378
|
+
},
|
|
372
379
|
"max_auto_retries": {
|
|
373
380
|
"default": 2,
|
|
374
381
|
"maximum": 20,
|
|
@@ -745,6 +752,7 @@
|
|
|
745
752
|
"enum": [
|
|
746
753
|
"change",
|
|
747
754
|
"stop",
|
|
755
|
+
"push",
|
|
748
756
|
"ci"
|
|
749
757
|
],
|
|
750
758
|
"title": "Phase",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""ConstraintLoop package metadata.
|
|
2
2
|
|
|
3
|
-
The v0.
|
|
3
|
+
The v0.4 supported interfaces are the CLI and documented versioned protocols.
|
|
4
4
|
Python submodules are internal and may change during initial development.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
__version__ = "0.
|
|
7
|
+
__version__ = "0.4.1"
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Bounded subprocess execution with descendant cleanup."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import signal
|
|
7
|
+
import subprocess
|
|
8
|
+
from collections.abc import Mapping, Sequence
|
|
9
|
+
from contextlib import suppress
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def run_bounded(
|
|
14
|
+
command: Sequence[str] | str,
|
|
15
|
+
*,
|
|
16
|
+
timeout: float,
|
|
17
|
+
cwd: Path | None = None,
|
|
18
|
+
env: Mapping[str, str] | None = None,
|
|
19
|
+
input_text: str | None = None,
|
|
20
|
+
shell: bool = False,
|
|
21
|
+
) -> subprocess.CompletedProcess[str]:
|
|
22
|
+
"""Run a command and tear down its whole process group after a timeout."""
|
|
23
|
+
process = subprocess.Popen(
|
|
24
|
+
command,
|
|
25
|
+
shell=shell,
|
|
26
|
+
cwd=cwd,
|
|
27
|
+
env=env,
|
|
28
|
+
stdin=subprocess.PIPE if input_text is not None else None,
|
|
29
|
+
stdout=subprocess.PIPE,
|
|
30
|
+
stderr=subprocess.PIPE,
|
|
31
|
+
encoding="utf-8",
|
|
32
|
+
errors="replace",
|
|
33
|
+
start_new_session=os.name == "posix",
|
|
34
|
+
)
|
|
35
|
+
try:
|
|
36
|
+
stdout, stderr = process.communicate(input=input_text, timeout=timeout)
|
|
37
|
+
except subprocess.TimeoutExpired as exc:
|
|
38
|
+
stdout, stderr = _terminate_process_tree(process)
|
|
39
|
+
raise subprocess.TimeoutExpired(
|
|
40
|
+
command,
|
|
41
|
+
timeout,
|
|
42
|
+
output=stdout,
|
|
43
|
+
stderr=stderr,
|
|
44
|
+
) from exc
|
|
45
|
+
return subprocess.CompletedProcess(command, process.returncode, stdout or "", stderr or "")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _terminate_process_tree(process: subprocess.Popen[str]) -> tuple[str, str]:
|
|
49
|
+
"""Terminate the process group, escalating quickly when descendants ignore TERM."""
|
|
50
|
+
if os.name == "posix":
|
|
51
|
+
with suppress(ProcessLookupError):
|
|
52
|
+
os.killpg(process.pid, signal.SIGTERM)
|
|
53
|
+
else:
|
|
54
|
+
process.terminate()
|
|
55
|
+
try:
|
|
56
|
+
return process.communicate(timeout=0.5)
|
|
57
|
+
except subprocess.TimeoutExpired:
|
|
58
|
+
if os.name == "posix":
|
|
59
|
+
with suppress(ProcessLookupError):
|
|
60
|
+
os.killpg(process.pid, signal.SIGKILL)
|
|
61
|
+
else:
|
|
62
|
+
process.kill()
|
|
63
|
+
return process.communicate()
|