getcalx 0.1.0__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.
- getcalx-0.1.0/LICENSE +21 -0
- getcalx-0.1.0/PKG-INFO +131 -0
- getcalx-0.1.0/README.md +107 -0
- getcalx-0.1.0/pyproject.toml +59 -0
- getcalx-0.1.0/setup.cfg +4 -0
- getcalx-0.1.0/src/calx/__init__.py +3 -0
- getcalx-0.1.0/src/calx/__main__.py +3 -0
- getcalx-0.1.0/src/calx/capture/__init__.py +1 -0
- getcalx-0.1.0/src/calx/capture/explicit.py +99 -0
- getcalx-0.1.0/src/calx/capture/recovery.py +25 -0
- getcalx-0.1.0/src/calx/capture/session_end.py +69 -0
- getcalx-0.1.0/src/calx/cli/__init__.py +1 -0
- getcalx-0.1.0/src/calx/cli/config_cmd.py +71 -0
- getcalx-0.1.0/src/calx/cli/correct.py +69 -0
- getcalx-0.1.0/src/calx/cli/dispatch_cmd.py +47 -0
- getcalx-0.1.0/src/calx/cli/distill.py +92 -0
- getcalx-0.1.0/src/calx/cli/health.py +223 -0
- getcalx-0.1.0/src/calx/cli/hook_cmd.py +191 -0
- getcalx-0.1.0/src/calx/cli/init_cmd.py +198 -0
- getcalx-0.1.0/src/calx/cli/main.py +39 -0
- getcalx-0.1.0/src/calx/cli/stats.py +80 -0
- getcalx-0.1.0/src/calx/cli/status.py +58 -0
- getcalx-0.1.0/src/calx/core/__init__.py +0 -0
- getcalx-0.1.0/src/calx/core/config.py +108 -0
- getcalx-0.1.0/src/calx/core/corrections.py +248 -0
- getcalx-0.1.0/src/calx/core/events.py +61 -0
- getcalx-0.1.0/src/calx/core/ids.py +34 -0
- getcalx-0.1.0/src/calx/core/integrity.py +84 -0
- getcalx-0.1.0/src/calx/core/phone_home.py +81 -0
- getcalx-0.1.0/src/calx/core/rules.py +161 -0
- getcalx-0.1.0/src/calx/core/state.py +119 -0
- getcalx-0.1.0/src/calx/core/telemetry.py +83 -0
- getcalx-0.1.0/src/calx/dispatch/__init__.py +1 -0
- getcalx-0.1.0/src/calx/dispatch/generator.py +72 -0
- getcalx-0.1.0/src/calx/dispatch/review.py +83 -0
- getcalx-0.1.0/src/calx/distillation/__init__.py +1 -0
- getcalx-0.1.0/src/calx/distillation/promotion.py +137 -0
- getcalx-0.1.0/src/calx/distillation/recurrence.py +107 -0
- getcalx-0.1.0/src/calx/distillation/review.py +146 -0
- getcalx-0.1.0/src/calx/distillation/similarity.py +55 -0
- getcalx-0.1.0/src/calx/health/__init__.py +1 -0
- getcalx-0.1.0/src/calx/health/conflicts.py +103 -0
- getcalx-0.1.0/src/calx/health/conversion.py +49 -0
- getcalx-0.1.0/src/calx/health/coverage.py +71 -0
- getcalx-0.1.0/src/calx/health/dedup.py +34 -0
- getcalx-0.1.0/src/calx/health/floor.py +100 -0
- getcalx-0.1.0/src/calx/health/scoring.py +78 -0
- getcalx-0.1.0/src/calx/health/staleness.py +60 -0
- getcalx-0.1.0/src/calx/hooks/__init__.py +1 -0
- getcalx-0.1.0/src/calx/hooks/installer.py +150 -0
- getcalx-0.1.0/src/calx/hooks/templates/collapse_guard.sh +40 -0
- getcalx-0.1.0/src/calx/hooks/templates/orientation_gate.sh +33 -0
- getcalx-0.1.0/src/calx/hooks/templates/session_end.sh +5 -0
- getcalx-0.1.0/src/calx/hooks/templates/session_start.sh +13 -0
- getcalx-0.1.0/src/calx/templates/__init__.py +1 -0
- getcalx-0.1.0/src/calx/templates/calx_readme.py +48 -0
- getcalx-0.1.0/src/calx/templates/claude_md_scaffold.py +35 -0
- getcalx-0.1.0/src/calx/templates/method_docs.py +328 -0
- getcalx-0.1.0/src/getcalx.egg-info/PKG-INFO +131 -0
- getcalx-0.1.0/src/getcalx.egg-info/SOURCES.txt +63 -0
- getcalx-0.1.0/src/getcalx.egg-info/dependency_links.txt +1 -0
- getcalx-0.1.0/src/getcalx.egg-info/entry_points.txt +2 -0
- getcalx-0.1.0/src/getcalx.egg-info/requires.txt +10 -0
- getcalx-0.1.0/src/getcalx.egg-info/top_level.txt +1 -0
- getcalx-0.1.0/tests/test_integration.py +482 -0
getcalx-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Spencer Hardwick
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
getcalx-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: getcalx
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Behavioral governance layer for AI coding agents. Makes corrections compound.
|
|
5
|
+
Author: Spencer Hardwick
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Environment :: Console
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
12
|
+
Requires-Python: >=3.10
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: click>=8.1
|
|
16
|
+
Provides-Extra: llm
|
|
17
|
+
Requires-Dist: anthropic>=0.30; extra == "llm"
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
20
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
21
|
+
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
22
|
+
Requires-Dist: mypy>=1.10; extra == "dev"
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# Calx
|
|
26
|
+
|
|
27
|
+
Behavioral governance layer for AI coding agents. Makes corrections compound.
|
|
28
|
+
|
|
29
|
+
Calx captures the corrections you make to AI agents, detects when the same mistake recurs, and promotes recurring corrections into rules that get injected at the start of every session. Your agent stops making the same mistake twice.
|
|
30
|
+
|
|
31
|
+
## The problem
|
|
32
|
+
|
|
33
|
+
You correct an AI agent. It learns -- for that session. Next session, same mistake. You correct it again. The correction doesn't transfer between sessions, between agents, or between projects. Your knowledge leaks.
|
|
34
|
+
|
|
35
|
+
## How Calx works
|
|
36
|
+
|
|
37
|
+
1. **Capture** -- `calx correct "don't mock the database in integration tests"` logs the correction to an append-only event log (`corrections.jsonl`)
|
|
38
|
+
2. **Detect** -- Calx matches new corrections against existing ones. When the same correction recurs 3+ times, it surfaces for promotion
|
|
39
|
+
3. **Promote** -- Recurring corrections become rules, written to `.calx/rules/{domain}.md`
|
|
40
|
+
4. **Inject** -- At session start, all active rules are injected into the agent's context via hooks
|
|
41
|
+
|
|
42
|
+
This is the **learning loop**: correct -> detect recurrence -> promote to rule -> inject at session start -> fewer corrections needed.
|
|
43
|
+
|
|
44
|
+
## Install
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install getcalx
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Quick start
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# Initialize Calx in your project
|
|
54
|
+
calx init
|
|
55
|
+
|
|
56
|
+
# Log a correction
|
|
57
|
+
calx correct "always validate API inputs before processing"
|
|
58
|
+
|
|
59
|
+
# Check status
|
|
60
|
+
calx status
|
|
61
|
+
|
|
62
|
+
# Run distillation (promote recurring corrections to rules)
|
|
63
|
+
calx distill
|
|
64
|
+
|
|
65
|
+
# View health of your rule set
|
|
66
|
+
calx health score
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Architecture
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
.calx/
|
|
73
|
+
├── calx.json # Configuration
|
|
74
|
+
├── .gitignore # Tracks what to commit vs ignore
|
|
75
|
+
├── corrections.jsonl # Append-only event log (local, gitignored)
|
|
76
|
+
├── rules/
|
|
77
|
+
│ └── {domain}.md # Promoted rules per domain (committed)
|
|
78
|
+
├── health/
|
|
79
|
+
│ ├── state.json # Health scores
|
|
80
|
+
│ └── .last_clean_exit # Session state marker
|
|
81
|
+
└── method/
|
|
82
|
+
├── how-we-document.md # Learning loop methodology
|
|
83
|
+
├── orchestration.md # Hook and session management
|
|
84
|
+
├── dispatch.md # Agent dispatch scaffolding
|
|
85
|
+
└── review.md # Review process
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Event-sourced corrections** -- `corrections.jsonl` is truly append-only. State is derived by replaying events. No rewrites, no mutations.
|
|
89
|
+
|
|
90
|
+
**Three-tier distillation:**
|
|
91
|
+
- **Tier 1** -- Silent recurrence counter (automatic)
|
|
92
|
+
- **Tier 2** -- Binary promote/reject at threshold (developer approves)
|
|
93
|
+
- **Tier 3** -- Weekly review of rule effectiveness
|
|
94
|
+
|
|
95
|
+
**Hook-based orchestration:**
|
|
96
|
+
- `session-start` -- Rule injection, dirty exit check, effectiveness signal, token discipline
|
|
97
|
+
- `session-end` -- Uncommitted changes check, undistilled reminder, clean exit marker
|
|
98
|
+
- `orientation-gate` / `collapse-guard` -- Pure bash PreToolUse hooks (no Python on the hot path). `collapse-guard` uses `jq` if available (falls back silently without it)
|
|
99
|
+
|
|
100
|
+
## Commands
|
|
101
|
+
|
|
102
|
+
| Command | Description |
|
|
103
|
+
|---------|-------------|
|
|
104
|
+
| `calx init` | Initialize Calx in current project |
|
|
105
|
+
| `calx correct <text>` | Log a correction |
|
|
106
|
+
| `calx status` | Show project status |
|
|
107
|
+
| `calx distill` | Promote recurring corrections to rules |
|
|
108
|
+
| `calx config` | View or modify configuration |
|
|
109
|
+
| `calx health` | Health analysis (score, conflicts, staleness, dedup, coverage, conversion) |
|
|
110
|
+
| `calx dispatch` | Generate dispatch prompt for a domain agent |
|
|
111
|
+
| `calx stats` | Show local metrics |
|
|
112
|
+
|
|
113
|
+
## Development
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
# Install in development mode
|
|
117
|
+
pip install -e ".[dev]"
|
|
118
|
+
|
|
119
|
+
# Run tests
|
|
120
|
+
pytest
|
|
121
|
+
|
|
122
|
+
# Lint
|
|
123
|
+
ruff check src/ tests/
|
|
124
|
+
|
|
125
|
+
# Type check
|
|
126
|
+
mypy src/calx/
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## License
|
|
130
|
+
|
|
131
|
+
MIT
|
getcalx-0.1.0/README.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Calx
|
|
2
|
+
|
|
3
|
+
Behavioral governance layer for AI coding agents. Makes corrections compound.
|
|
4
|
+
|
|
5
|
+
Calx captures the corrections you make to AI agents, detects when the same mistake recurs, and promotes recurring corrections into rules that get injected at the start of every session. Your agent stops making the same mistake twice.
|
|
6
|
+
|
|
7
|
+
## The problem
|
|
8
|
+
|
|
9
|
+
You correct an AI agent. It learns -- for that session. Next session, same mistake. You correct it again. The correction doesn't transfer between sessions, between agents, or between projects. Your knowledge leaks.
|
|
10
|
+
|
|
11
|
+
## How Calx works
|
|
12
|
+
|
|
13
|
+
1. **Capture** -- `calx correct "don't mock the database in integration tests"` logs the correction to an append-only event log (`corrections.jsonl`)
|
|
14
|
+
2. **Detect** -- Calx matches new corrections against existing ones. When the same correction recurs 3+ times, it surfaces for promotion
|
|
15
|
+
3. **Promote** -- Recurring corrections become rules, written to `.calx/rules/{domain}.md`
|
|
16
|
+
4. **Inject** -- At session start, all active rules are injected into the agent's context via hooks
|
|
17
|
+
|
|
18
|
+
This is the **learning loop**: correct -> detect recurrence -> promote to rule -> inject at session start -> fewer corrections needed.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install getcalx
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Quick start
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# Initialize Calx in your project
|
|
30
|
+
calx init
|
|
31
|
+
|
|
32
|
+
# Log a correction
|
|
33
|
+
calx correct "always validate API inputs before processing"
|
|
34
|
+
|
|
35
|
+
# Check status
|
|
36
|
+
calx status
|
|
37
|
+
|
|
38
|
+
# Run distillation (promote recurring corrections to rules)
|
|
39
|
+
calx distill
|
|
40
|
+
|
|
41
|
+
# View health of your rule set
|
|
42
|
+
calx health score
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Architecture
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
.calx/
|
|
49
|
+
├── calx.json # Configuration
|
|
50
|
+
├── .gitignore # Tracks what to commit vs ignore
|
|
51
|
+
├── corrections.jsonl # Append-only event log (local, gitignored)
|
|
52
|
+
├── rules/
|
|
53
|
+
│ └── {domain}.md # Promoted rules per domain (committed)
|
|
54
|
+
├── health/
|
|
55
|
+
│ ├── state.json # Health scores
|
|
56
|
+
│ └── .last_clean_exit # Session state marker
|
|
57
|
+
└── method/
|
|
58
|
+
├── how-we-document.md # Learning loop methodology
|
|
59
|
+
├── orchestration.md # Hook and session management
|
|
60
|
+
├── dispatch.md # Agent dispatch scaffolding
|
|
61
|
+
└── review.md # Review process
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**Event-sourced corrections** -- `corrections.jsonl` is truly append-only. State is derived by replaying events. No rewrites, no mutations.
|
|
65
|
+
|
|
66
|
+
**Three-tier distillation:**
|
|
67
|
+
- **Tier 1** -- Silent recurrence counter (automatic)
|
|
68
|
+
- **Tier 2** -- Binary promote/reject at threshold (developer approves)
|
|
69
|
+
- **Tier 3** -- Weekly review of rule effectiveness
|
|
70
|
+
|
|
71
|
+
**Hook-based orchestration:**
|
|
72
|
+
- `session-start` -- Rule injection, dirty exit check, effectiveness signal, token discipline
|
|
73
|
+
- `session-end` -- Uncommitted changes check, undistilled reminder, clean exit marker
|
|
74
|
+
- `orientation-gate` / `collapse-guard` -- Pure bash PreToolUse hooks (no Python on the hot path). `collapse-guard` uses `jq` if available (falls back silently without it)
|
|
75
|
+
|
|
76
|
+
## Commands
|
|
77
|
+
|
|
78
|
+
| Command | Description |
|
|
79
|
+
|---------|-------------|
|
|
80
|
+
| `calx init` | Initialize Calx in current project |
|
|
81
|
+
| `calx correct <text>` | Log a correction |
|
|
82
|
+
| `calx status` | Show project status |
|
|
83
|
+
| `calx distill` | Promote recurring corrections to rules |
|
|
84
|
+
| `calx config` | View or modify configuration |
|
|
85
|
+
| `calx health` | Health analysis (score, conflicts, staleness, dedup, coverage, conversion) |
|
|
86
|
+
| `calx dispatch` | Generate dispatch prompt for a domain agent |
|
|
87
|
+
| `calx stats` | Show local metrics |
|
|
88
|
+
|
|
89
|
+
## Development
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
# Install in development mode
|
|
93
|
+
pip install -e ".[dev]"
|
|
94
|
+
|
|
95
|
+
# Run tests
|
|
96
|
+
pytest
|
|
97
|
+
|
|
98
|
+
# Lint
|
|
99
|
+
ruff check src/ tests/
|
|
100
|
+
|
|
101
|
+
# Type check
|
|
102
|
+
mypy src/calx/
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## License
|
|
106
|
+
|
|
107
|
+
MIT
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "getcalx"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Behavioral governance layer for AI coding agents. Makes corrections compound."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "Spencer Hardwick"}
|
|
14
|
+
]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Environment :: Console",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
21
|
+
]
|
|
22
|
+
dependencies = [
|
|
23
|
+
"click>=8.1",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.optional-dependencies]
|
|
27
|
+
llm = ["anthropic>=0.30"]
|
|
28
|
+
dev = [
|
|
29
|
+
"pytest>=8.0",
|
|
30
|
+
"pytest-cov",
|
|
31
|
+
"ruff>=0.4",
|
|
32
|
+
"mypy>=1.10",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.scripts]
|
|
36
|
+
calx = "calx.cli.main:cli"
|
|
37
|
+
|
|
38
|
+
[tool.setuptools.packages.find]
|
|
39
|
+
where = ["src"]
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.package-data]
|
|
42
|
+
calx = ["hooks/templates/*.sh"]
|
|
43
|
+
|
|
44
|
+
[tool.ruff]
|
|
45
|
+
target-version = "py310"
|
|
46
|
+
line-length = 100
|
|
47
|
+
|
|
48
|
+
[tool.ruff.lint]
|
|
49
|
+
select = ["E", "F", "I", "UP", "B", "SIM"]
|
|
50
|
+
|
|
51
|
+
[tool.mypy]
|
|
52
|
+
python_version = "3.10"
|
|
53
|
+
strict = true
|
|
54
|
+
warn_return_any = true
|
|
55
|
+
warn_unused_configs = true
|
|
56
|
+
|
|
57
|
+
[tool.pytest.ini_options]
|
|
58
|
+
testpaths = ["tests"]
|
|
59
|
+
addopts = "-v --tb=short"
|
getcalx-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Capture layer — explicit corrections, session-end prompts, recovery."""
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Explicit correction capture — ``calx correct [message]``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from calx.core.config import load_config
|
|
8
|
+
from calx.core.corrections import CorrectionState, create_correction
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def capture_explicit(
|
|
12
|
+
calx_dir: Path,
|
|
13
|
+
message: str,
|
|
14
|
+
domain: str | None = None,
|
|
15
|
+
correction_type: str = "process",
|
|
16
|
+
context: str = "",
|
|
17
|
+
) -> tuple[CorrectionState, str]:
|
|
18
|
+
"""Capture an explicit correction and return (correction, feedback).
|
|
19
|
+
|
|
20
|
+
Domain resolution order:
|
|
21
|
+
1. Explicit ``domain`` argument
|
|
22
|
+
2. Auto-detect from cwd
|
|
23
|
+
3. First domain in config
|
|
24
|
+
"""
|
|
25
|
+
config = load_config(calx_dir)
|
|
26
|
+
|
|
27
|
+
resolved_domain = domain or _auto_detect_domain(calx_dir) or _first_domain(config)
|
|
28
|
+
|
|
29
|
+
correction = create_correction(
|
|
30
|
+
calx_dir,
|
|
31
|
+
domain=resolved_domain,
|
|
32
|
+
description=message,
|
|
33
|
+
correction_type=correction_type,
|
|
34
|
+
context=context,
|
|
35
|
+
source="explicit",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# Try recurrence check — distillation module may not exist yet
|
|
39
|
+
feedback = f"Logged as {correction.id} in {resolved_domain} domain."
|
|
40
|
+
try:
|
|
41
|
+
from calx.core.corrections import materialize
|
|
42
|
+
from calx.distillation.recurrence import check_recurrence
|
|
43
|
+
|
|
44
|
+
result = check_recurrence(calx_dir, correction)
|
|
45
|
+
if result.is_recurrence and result.original_id:
|
|
46
|
+
# Look up the original correction's description
|
|
47
|
+
all_corr = materialize(calx_dir)
|
|
48
|
+
by_id = {c.id: c for c in all_corr}
|
|
49
|
+
original = by_id.get(result.original_id)
|
|
50
|
+
original_desc = original.description if original else result.original_id
|
|
51
|
+
count = result.new_count
|
|
52
|
+
|
|
53
|
+
threshold = config.promotion_threshold
|
|
54
|
+
if count >= threshold:
|
|
55
|
+
feedback = (
|
|
56
|
+
f'Logged. Matches {result.original_id}: "{original_desc}". '
|
|
57
|
+
f"({count} occurrence — promotion eligible.)"
|
|
58
|
+
)
|
|
59
|
+
else:
|
|
60
|
+
feedback = (
|
|
61
|
+
f'Logged. Matches {result.original_id}: "{original_desc}". '
|
|
62
|
+
f"({count} occurrences.)"
|
|
63
|
+
)
|
|
64
|
+
except ImportError:
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
return correction, feedback
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _auto_detect_domain(calx_dir: Path) -> str | None:
|
|
71
|
+
"""Infer domain from cwd relative to the project root.
|
|
72
|
+
|
|
73
|
+
The project root is the parent of the ``.calx`` directory.
|
|
74
|
+
If cwd is inside a subdirectory whose name matches a configured domain,
|
|
75
|
+
that domain is returned.
|
|
76
|
+
"""
|
|
77
|
+
config = load_config(calx_dir)
|
|
78
|
+
if not config.domains:
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
project_root = calx_dir.parent
|
|
82
|
+
try:
|
|
83
|
+
cwd = Path.cwd()
|
|
84
|
+
relative = cwd.relative_to(project_root)
|
|
85
|
+
except ValueError:
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
parts = relative.parts
|
|
89
|
+
for part in parts:
|
|
90
|
+
if part in config.domains:
|
|
91
|
+
return part
|
|
92
|
+
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _first_domain(config: object) -> str:
|
|
97
|
+
"""Return the first configured domain, or 'general' as fallback."""
|
|
98
|
+
domains = getattr(config, "domains", [])
|
|
99
|
+
return domains[0] if domains else "general"
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Dirty-exit recovery check at session start."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from calx.core.state import check_clean_exit
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def recovery_check(calx_dir: Path) -> str | None:
|
|
11
|
+
"""Check whether the previous session exited cleanly.
|
|
12
|
+
|
|
13
|
+
Returns a recovery prompt message if the exit was dirty,
|
|
14
|
+
or ``None`` if everything is fine.
|
|
15
|
+
"""
|
|
16
|
+
status = check_clean_exit(calx_dir)
|
|
17
|
+
|
|
18
|
+
if status.was_clean:
|
|
19
|
+
return None
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
"Previous session did not exit cleanly. "
|
|
23
|
+
"There may be undistilled corrections or uncommitted changes. "
|
|
24
|
+
"Run `calx status` to review."
|
|
25
|
+
)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Session-end capture prompt. Called by the session-end hook."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from calx.core.corrections import get_undistilled
|
|
10
|
+
from calx.core.events import Event, log_event
|
|
11
|
+
from calx.core.state import write_clean_exit
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def session_end_prompt(calx_dir: Path) -> str:
|
|
15
|
+
"""Generate a session-end summary and write clean-exit marker.
|
|
16
|
+
|
|
17
|
+
Steps:
|
|
18
|
+
1. Count undistilled corrections
|
|
19
|
+
2. Check for uncommitted git changes
|
|
20
|
+
3. Write clean-exit marker
|
|
21
|
+
4. Log session_end event
|
|
22
|
+
5. Return formatted message
|
|
23
|
+
"""
|
|
24
|
+
undistilled = get_undistilled(calx_dir)
|
|
25
|
+
uncommitted = _has_uncommitted_changes(calx_dir)
|
|
26
|
+
|
|
27
|
+
write_clean_exit(calx_dir)
|
|
28
|
+
|
|
29
|
+
log_event(calx_dir, Event(
|
|
30
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
31
|
+
event="session_end",
|
|
32
|
+
data={
|
|
33
|
+
"undistilled_count": len(undistilled),
|
|
34
|
+
"uncommitted_changes": uncommitted,
|
|
35
|
+
},
|
|
36
|
+
))
|
|
37
|
+
|
|
38
|
+
parts: list[str] = []
|
|
39
|
+
|
|
40
|
+
if undistilled:
|
|
41
|
+
ids = ", ".join(c.id for c in undistilled)
|
|
42
|
+
parts.append(
|
|
43
|
+
f"{len(undistilled)} undistilled correction(s): {ids}. "
|
|
44
|
+
"Consider running `calx distill`."
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
if uncommitted:
|
|
48
|
+
parts.append("Uncommitted changes detected. Consider committing before exit.")
|
|
49
|
+
|
|
50
|
+
if not parts:
|
|
51
|
+
return "Session ended cleanly. No pending items."
|
|
52
|
+
|
|
53
|
+
return "Session end summary:\n" + "\n".join(f"- {p}" for p in parts)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _has_uncommitted_changes(calx_dir: Path) -> bool:
|
|
57
|
+
"""Check for uncommitted git changes in the project root."""
|
|
58
|
+
project_root = calx_dir.parent
|
|
59
|
+
try:
|
|
60
|
+
result = subprocess.run(
|
|
61
|
+
["git", "status", "--porcelain"],
|
|
62
|
+
capture_output=True,
|
|
63
|
+
text=True,
|
|
64
|
+
cwd=str(project_root),
|
|
65
|
+
timeout=5,
|
|
66
|
+
)
|
|
67
|
+
return bool(result.stdout.strip())
|
|
68
|
+
except (subprocess.SubprocessError, FileNotFoundError, OSError):
|
|
69
|
+
return False
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Calx CLI package."""
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""calx config — settings management."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
|
|
6
|
+
from calx.core.config import find_calx_dir, load_config, save_config
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@click.command("config")
|
|
10
|
+
@click.option("--show", is_flag=True, help="Show current config")
|
|
11
|
+
@click.option("--set", "key_value", nargs=2, help="Set a config value: --set key value")
|
|
12
|
+
def config_cmd(show: bool, key_value: tuple[str, str] | None):
|
|
13
|
+
"""View or modify Calx configuration."""
|
|
14
|
+
calx_dir = find_calx_dir()
|
|
15
|
+
if not calx_dir:
|
|
16
|
+
click.echo("Not a Calx project. Run `calx init` first.", err=True)
|
|
17
|
+
raise SystemExit(1)
|
|
18
|
+
|
|
19
|
+
config = load_config(calx_dir)
|
|
20
|
+
|
|
21
|
+
if key_value:
|
|
22
|
+
key, value = key_value
|
|
23
|
+
_set_config(calx_dir, config, key, value)
|
|
24
|
+
else:
|
|
25
|
+
# Default to showing config
|
|
26
|
+
click.echo("Calx Config")
|
|
27
|
+
click.echo(f" Domains: {', '.join(config.domains)}")
|
|
28
|
+
click.echo(f" Agent naming: {config.agent_naming}")
|
|
29
|
+
click.echo(f" Promotion threshold: {config.promotion_threshold}")
|
|
30
|
+
click.echo(f" Max prompts/session: {config.max_prompts_per_session}")
|
|
31
|
+
click.echo(f" Staleness days: {config.staleness_days}")
|
|
32
|
+
click.echo(f" Stats opt-in: {config.stats_opt_in}")
|
|
33
|
+
click.echo(f" Phone home: {config.phone_home}")
|
|
34
|
+
td = config.token_discipline
|
|
35
|
+
click.echo(f" Token soft cap: {td.soft_cap:,}")
|
|
36
|
+
click.echo(f" Token ceiling: {td.ceiling:,}")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _set_config(calx_dir, config, key, value):
|
|
40
|
+
"""Set a single config value."""
|
|
41
|
+
int_keys = {
|
|
42
|
+
"promotion_threshold": (1, 100),
|
|
43
|
+
"max_prompts_per_session": (1, 50),
|
|
44
|
+
"staleness_days": (1, 365),
|
|
45
|
+
}
|
|
46
|
+
if key in int_keys:
|
|
47
|
+
lo, hi = int_keys[key]
|
|
48
|
+
try:
|
|
49
|
+
parsed = int(value)
|
|
50
|
+
except ValueError:
|
|
51
|
+
click.echo(f"Invalid value for {key}: must be an integer", err=True)
|
|
52
|
+
return
|
|
53
|
+
if parsed < lo or parsed > hi:
|
|
54
|
+
click.echo(f"Invalid value for {key}: must be between {lo} and {hi}", err=True)
|
|
55
|
+
return
|
|
56
|
+
setattr(config, key, parsed)
|
|
57
|
+
elif key == "agent_naming":
|
|
58
|
+
if value not in ("self", "developer", "none"):
|
|
59
|
+
click.echo("Invalid value. Must be: self, developer, none", err=True)
|
|
60
|
+
return
|
|
61
|
+
config.agent_naming = value
|
|
62
|
+
elif key == "stats_opt_in":
|
|
63
|
+
config.stats_opt_in = value.lower() in ("true", "1", "yes")
|
|
64
|
+
elif key == "phone_home":
|
|
65
|
+
config.phone_home = value.lower() in ("true", "1", "yes")
|
|
66
|
+
else:
|
|
67
|
+
click.echo(f"Unknown config key: {key}", err=True)
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
save_config(calx_dir, config)
|
|
71
|
+
click.echo(f"Set {key} = {value}")
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""calx correct — explicit correction capture."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
from calx.capture.explicit import capture_explicit
|
|
9
|
+
from calx.core.config import find_calx_dir
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@click.command()
|
|
13
|
+
@click.argument("message")
|
|
14
|
+
@click.option("--domain", "-d", default=None, help="Override auto-detected domain")
|
|
15
|
+
@click.option(
|
|
16
|
+
"--type",
|
|
17
|
+
"-t",
|
|
18
|
+
"correction_type",
|
|
19
|
+
default="process",
|
|
20
|
+
type=click.Choice(["process", "architectural"]),
|
|
21
|
+
help="Correction type",
|
|
22
|
+
)
|
|
23
|
+
@click.option("--context", "-c", default="", help="Additional context")
|
|
24
|
+
@click.option("--json", "as_json", is_flag=True, help="Output as JSON")
|
|
25
|
+
def correct(
|
|
26
|
+
message: str,
|
|
27
|
+
domain: str | None,
|
|
28
|
+
correction_type: str,
|
|
29
|
+
context: str,
|
|
30
|
+
as_json: bool,
|
|
31
|
+
) -> None:
|
|
32
|
+
"""Log a correction. Golden path: calx correct 'message'"""
|
|
33
|
+
calx_dir = find_calx_dir()
|
|
34
|
+
if not calx_dir:
|
|
35
|
+
click.echo("Not a Calx project. Run `calx init` first.", err=True)
|
|
36
|
+
raise SystemExit(1)
|
|
37
|
+
|
|
38
|
+
correction, feedback = capture_explicit(
|
|
39
|
+
calx_dir,
|
|
40
|
+
message,
|
|
41
|
+
domain=domain,
|
|
42
|
+
correction_type=correction_type,
|
|
43
|
+
context=context,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Phone home — correct event (domain + type only, no content)
|
|
47
|
+
from calx.core.phone_home import send_event
|
|
48
|
+
|
|
49
|
+
send_event(calx_dir, "correct", {
|
|
50
|
+
"domain": correction.domain,
|
|
51
|
+
"correction_type": correction.type,
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
if as_json:
|
|
55
|
+
click.echo(
|
|
56
|
+
json.dumps(
|
|
57
|
+
{
|
|
58
|
+
"id": correction.id,
|
|
59
|
+
"domain": correction.domain,
|
|
60
|
+
"type": correction.type,
|
|
61
|
+
"description": correction.description,
|
|
62
|
+
"status": correction.status,
|
|
63
|
+
"feedback": feedback,
|
|
64
|
+
},
|
|
65
|
+
indent=2,
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
else:
|
|
69
|
+
click.echo(feedback)
|