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.
Files changed (65) hide show
  1. getcalx-0.1.0/LICENSE +21 -0
  2. getcalx-0.1.0/PKG-INFO +131 -0
  3. getcalx-0.1.0/README.md +107 -0
  4. getcalx-0.1.0/pyproject.toml +59 -0
  5. getcalx-0.1.0/setup.cfg +4 -0
  6. getcalx-0.1.0/src/calx/__init__.py +3 -0
  7. getcalx-0.1.0/src/calx/__main__.py +3 -0
  8. getcalx-0.1.0/src/calx/capture/__init__.py +1 -0
  9. getcalx-0.1.0/src/calx/capture/explicit.py +99 -0
  10. getcalx-0.1.0/src/calx/capture/recovery.py +25 -0
  11. getcalx-0.1.0/src/calx/capture/session_end.py +69 -0
  12. getcalx-0.1.0/src/calx/cli/__init__.py +1 -0
  13. getcalx-0.1.0/src/calx/cli/config_cmd.py +71 -0
  14. getcalx-0.1.0/src/calx/cli/correct.py +69 -0
  15. getcalx-0.1.0/src/calx/cli/dispatch_cmd.py +47 -0
  16. getcalx-0.1.0/src/calx/cli/distill.py +92 -0
  17. getcalx-0.1.0/src/calx/cli/health.py +223 -0
  18. getcalx-0.1.0/src/calx/cli/hook_cmd.py +191 -0
  19. getcalx-0.1.0/src/calx/cli/init_cmd.py +198 -0
  20. getcalx-0.1.0/src/calx/cli/main.py +39 -0
  21. getcalx-0.1.0/src/calx/cli/stats.py +80 -0
  22. getcalx-0.1.0/src/calx/cli/status.py +58 -0
  23. getcalx-0.1.0/src/calx/core/__init__.py +0 -0
  24. getcalx-0.1.0/src/calx/core/config.py +108 -0
  25. getcalx-0.1.0/src/calx/core/corrections.py +248 -0
  26. getcalx-0.1.0/src/calx/core/events.py +61 -0
  27. getcalx-0.1.0/src/calx/core/ids.py +34 -0
  28. getcalx-0.1.0/src/calx/core/integrity.py +84 -0
  29. getcalx-0.1.0/src/calx/core/phone_home.py +81 -0
  30. getcalx-0.1.0/src/calx/core/rules.py +161 -0
  31. getcalx-0.1.0/src/calx/core/state.py +119 -0
  32. getcalx-0.1.0/src/calx/core/telemetry.py +83 -0
  33. getcalx-0.1.0/src/calx/dispatch/__init__.py +1 -0
  34. getcalx-0.1.0/src/calx/dispatch/generator.py +72 -0
  35. getcalx-0.1.0/src/calx/dispatch/review.py +83 -0
  36. getcalx-0.1.0/src/calx/distillation/__init__.py +1 -0
  37. getcalx-0.1.0/src/calx/distillation/promotion.py +137 -0
  38. getcalx-0.1.0/src/calx/distillation/recurrence.py +107 -0
  39. getcalx-0.1.0/src/calx/distillation/review.py +146 -0
  40. getcalx-0.1.0/src/calx/distillation/similarity.py +55 -0
  41. getcalx-0.1.0/src/calx/health/__init__.py +1 -0
  42. getcalx-0.1.0/src/calx/health/conflicts.py +103 -0
  43. getcalx-0.1.0/src/calx/health/conversion.py +49 -0
  44. getcalx-0.1.0/src/calx/health/coverage.py +71 -0
  45. getcalx-0.1.0/src/calx/health/dedup.py +34 -0
  46. getcalx-0.1.0/src/calx/health/floor.py +100 -0
  47. getcalx-0.1.0/src/calx/health/scoring.py +78 -0
  48. getcalx-0.1.0/src/calx/health/staleness.py +60 -0
  49. getcalx-0.1.0/src/calx/hooks/__init__.py +1 -0
  50. getcalx-0.1.0/src/calx/hooks/installer.py +150 -0
  51. getcalx-0.1.0/src/calx/hooks/templates/collapse_guard.sh +40 -0
  52. getcalx-0.1.0/src/calx/hooks/templates/orientation_gate.sh +33 -0
  53. getcalx-0.1.0/src/calx/hooks/templates/session_end.sh +5 -0
  54. getcalx-0.1.0/src/calx/hooks/templates/session_start.sh +13 -0
  55. getcalx-0.1.0/src/calx/templates/__init__.py +1 -0
  56. getcalx-0.1.0/src/calx/templates/calx_readme.py +48 -0
  57. getcalx-0.1.0/src/calx/templates/claude_md_scaffold.py +35 -0
  58. getcalx-0.1.0/src/calx/templates/method_docs.py +328 -0
  59. getcalx-0.1.0/src/getcalx.egg-info/PKG-INFO +131 -0
  60. getcalx-0.1.0/src/getcalx.egg-info/SOURCES.txt +63 -0
  61. getcalx-0.1.0/src/getcalx.egg-info/dependency_links.txt +1 -0
  62. getcalx-0.1.0/src/getcalx.egg-info/entry_points.txt +2 -0
  63. getcalx-0.1.0/src/getcalx.egg-info/requires.txt +10 -0
  64. getcalx-0.1.0/src/getcalx.egg-info/top_level.txt +1 -0
  65. 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
@@ -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"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """Calx — behavioral governance layer for AI coding agents."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,3 @@
1
+ from calx.cli.main import cli
2
+
3
+ cli()
@@ -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)