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.
Files changed (70) hide show
  1. {constraintloop-0.3.0 → constraintloop-0.4.1}/.gitignore +3 -0
  2. {constraintloop-0.3.0 → constraintloop-0.4.1}/CHANGELOG.md +28 -0
  3. {constraintloop-0.3.0 → constraintloop-0.4.1}/PKG-INFO +45 -15
  4. {constraintloop-0.3.0 → constraintloop-0.4.1}/README.md +44 -14
  5. {constraintloop-0.3.0 → constraintloop-0.4.1}/docs/configuration.md +11 -3
  6. {constraintloop-0.3.0 → constraintloop-0.4.1}/docs/faq.md +38 -3
  7. {constraintloop-0.3.0 → constraintloop-0.4.1}/docs/recipes.md +12 -4
  8. {constraintloop-0.3.0 → constraintloop-0.4.1}/pyproject.toml +1 -1
  9. {constraintloop-0.3.0 → constraintloop-0.4.1}/schema/constraintloop.schema.json +8 -0
  10. {constraintloop-0.3.0 → constraintloop-0.4.1}/scripts/check_coverage.py +2 -2
  11. {constraintloop-0.3.0 → constraintloop-0.4.1}/src/constraintloop/__init__.py +2 -2
  12. constraintloop-0.4.1/src/constraintloop/_process.py +63 -0
  13. {constraintloop-0.3.0 → constraintloop-0.4.1}/src/constraintloop/cli.py +32 -7
  14. {constraintloop-0.3.0 → constraintloop-0.4.1}/src/constraintloop/config.py +11 -1
  15. {constraintloop-0.3.0 → constraintloop-0.4.1}/src/constraintloop/engine.py +60 -3
  16. {constraintloop-0.3.0 → constraintloop-0.4.1}/src/constraintloop/evaluators.py +12 -9
  17. {constraintloop-0.3.0 → constraintloop-0.4.1}/src/constraintloop/hooks.py +52 -4
  18. {constraintloop-0.3.0 → constraintloop-0.4.1}/src/constraintloop/hygiene.py +4 -0
  19. {constraintloop-0.3.0 → constraintloop-0.4.1}/src/constraintloop/models.py +2 -0
  20. {constraintloop-0.3.0 → constraintloop-0.4.1}/src/constraintloop/native_cli_evaluator.py +3 -6
  21. {constraintloop-0.3.0 → constraintloop-0.4.1}/src/constraintloop/runners.py +12 -8
  22. {constraintloop-0.3.0 → constraintloop-0.4.1}/src/constraintloop/scaffold.py +1 -0
  23. {constraintloop-0.3.0 → constraintloop-0.4.1}/src/constraintloop/setup_hooks.py +126 -7
  24. {constraintloop-0.3.0 → constraintloop-0.4.1}/tests/test_cli.py +63 -1
  25. {constraintloop-0.3.0 → constraintloop-0.4.1}/tests/test_cli_commands.py +278 -0
  26. {constraintloop-0.3.0 → constraintloop-0.4.1}/tests/test_engine.py +35 -0
  27. {constraintloop-0.3.0 → constraintloop-0.4.1}/tests/test_environment.py +13 -0
  28. {constraintloop-0.3.0 → constraintloop-0.4.1}/tests/test_eval_corpus.py +33 -0
  29. {constraintloop-0.3.0 → constraintloop-0.4.1}/tests/test_hooks.py +201 -6
  30. {constraintloop-0.3.0 → constraintloop-0.4.1}/tests/test_models.py +91 -0
  31. {constraintloop-0.3.0 → constraintloop-0.4.1}/tests/test_native_cli_evaluator.py +109 -8
  32. constraintloop-0.4.1/tests/test_process.py +50 -0
  33. {constraintloop-0.3.0 → constraintloop-0.4.1}/tests/test_runners.py +38 -0
  34. {constraintloop-0.3.0 → constraintloop-0.4.1}/CONTRIBUTING.md +0 -0
  35. {constraintloop-0.3.0 → constraintloop-0.4.1}/GOVERNANCE.md +0 -0
  36. {constraintloop-0.3.0 → constraintloop-0.4.1}/LICENSE +0 -0
  37. {constraintloop-0.3.0 → constraintloop-0.4.1}/RELEASE.md +0 -0
  38. {constraintloop-0.3.0 → constraintloop-0.4.1}/SECURITY.md +0 -0
  39. {constraintloop-0.3.0 → constraintloop-0.4.1}/SUPPORT.md +0 -0
  40. {constraintloop-0.3.0 → constraintloop-0.4.1}/docs/convergence-loops.md +0 -0
  41. {constraintloop-0.3.0 → constraintloop-0.4.1}/docs/native-cli-evaluators.md +0 -0
  42. {constraintloop-0.3.0 → constraintloop-0.4.1}/docs/openai-evaluation.md +0 -0
  43. {constraintloop-0.3.0 → constraintloop-0.4.1}/docs/provider-privacy.md +0 -0
  44. {constraintloop-0.3.0 → constraintloop-0.4.1}/docs/release-readiness.md +0 -0
  45. {constraintloop-0.3.0 → constraintloop-0.4.1}/docs/threat-model.md +0 -0
  46. {constraintloop-0.3.0 → constraintloop-0.4.1}/scripts/check_anthropic_sdk.py +0 -0
  47. {constraintloop-0.3.0 → constraintloop-0.4.1}/scripts/check_openai_sdk.py +0 -0
  48. {constraintloop-0.3.0 → constraintloop-0.4.1}/scripts/check_sdist_contents.py +0 -0
  49. {constraintloop-0.3.0 → constraintloop-0.4.1}/scripts/generate_schema.py +0 -0
  50. {constraintloop-0.3.0 → constraintloop-0.4.1}/scripts/openai_eval_canary.py +0 -0
  51. {constraintloop-0.3.0 → constraintloop-0.4.1}/scripts/wheel_failure_smoke.py +0 -0
  52. {constraintloop-0.3.0 → constraintloop-0.4.1}/src/constraintloop/__main__.py +0 -0
  53. {constraintloop-0.3.0 → constraintloop-0.4.1}/src/constraintloop/diagnostics.py +0 -0
  54. {constraintloop-0.3.0 → constraintloop-0.4.1}/src/constraintloop/digest.py +0 -0
  55. {constraintloop-0.3.0 → constraintloop-0.4.1}/src/constraintloop/environment.py +0 -0
  56. {constraintloop-0.3.0 → constraintloop-0.4.1}/src/constraintloop/eval_corpus.py +0 -0
  57. {constraintloop-0.3.0 → constraintloop-0.4.1}/src/constraintloop/loops.py +0 -0
  58. {constraintloop-0.3.0 → constraintloop-0.4.1}/src/constraintloop/py.typed +0 -0
  59. {constraintloop-0.3.0 → constraintloop-0.4.1}/src/constraintloop/state.py +0 -0
  60. {constraintloop-0.3.0 → constraintloop-0.4.1}/tests/__init__.py +0 -0
  61. {constraintloop-0.3.0 → constraintloop-0.4.1}/tests/failure_lab.py +0 -0
  62. {constraintloop-0.3.0 → constraintloop-0.4.1}/tests/fixtures/openai_eval_corpus_v1.yml +0 -0
  63. {constraintloop-0.3.0 → constraintloop-0.4.1}/tests/test_diagnostics.py +0 -0
  64. {constraintloop-0.3.0 → constraintloop-0.4.1}/tests/test_digest.py +0 -0
  65. {constraintloop-0.3.0 → constraintloop-0.4.1}/tests/test_evaluators.py +0 -0
  66. {constraintloop-0.3.0 → constraintloop-0.4.1}/tests/test_failure_lab.py +0 -0
  67. {constraintloop-0.3.0 → constraintloop-0.4.1}/tests/test_loops.py +0 -0
  68. {constraintloop-0.3.0 → constraintloop-0.4.1}/tests/test_release_metadata.py +0 -0
  69. {constraintloop-0.3.0 → constraintloop-0.4.1}/tests/test_security.py +0 -0
  70. {constraintloop-0.3.0 → constraintloop-0.4.1}/tests/test_state.py +0 -0
@@ -16,3 +16,6 @@ dist/
16
16
  .constraintloop/
17
17
  constraintloop.local.yml
18
18
  constraintloop.local.yaml
19
+ .claude/settings.local.json
20
+ .codex/hooks.json
21
+ .gemini/settings.json
@@ -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.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 injected.
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 precise evidence and may repair the code a bounded number of times.
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 overlay names and `.constraintloop/state/` to the
299
- selected project's `.gitignore`, and warn if state is already tracked.
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 hook entries
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
- - `constraintloop run --phase change|stop` run local gates with fresh caching.
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.3 surfaces are the CLI and exit codes, configuration schema,
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. Expensive tests and reviews belong in `stop` and `ci`.
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 injected.
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 precise evidence and may repair the code a bounded number of times.
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 overlay names and `.constraintloop/state/` to the
256
- selected project's `.gitignore`, and warn if state is already tracked.
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 hook entries
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
- - `constraintloop run --phase change|stop` run local gates with fresh caching.
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.3 surfaces are the CLI and exit codes, configuration schema,
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. Expensive tests and reviews belong in `stop` and `ci`.
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. Retry and periodic
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 and
30
- keep it in `stop` and `ci`.
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. Review and trust newly installed Codex project hooks
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, but noisy after every action |
13
- | Full integration or platform suite | `ci` | Hosted dependencies and longer runtime |
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 coverage.
32
- Coverage depends on tests, so a test failure prevents a redundant coverage run.
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
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "constraintloop"
3
- version = "0.3.0"
3
+ version = "0.4.1"
4
4
  description = "Evidence-based completion gates for AI coding agents"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -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",
@@ -6,8 +6,8 @@ import json
6
6
  import sys
7
7
  from pathlib import Path
8
8
 
9
- STATEMENT_FLOOR = 90.0
10
- BRANCH_FLOOR = 80.0
9
+ STATEMENT_FLOOR = 95.0
10
+ BRANCH_FLOOR = 90.0
11
11
 
12
12
 
13
13
  def main() -> int:
@@ -1,7 +1,7 @@
1
1
  """ConstraintLoop package metadata.
2
2
 
3
- The v0.2 supported interfaces are the CLI and documented versioned protocols.
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.3.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()