repocodex 0.0.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 (81) hide show
  1. repocodex-0.0.1/LICENSE +21 -0
  2. repocodex-0.0.1/PKG-INFO +127 -0
  3. repocodex-0.0.1/README.md +104 -0
  4. repocodex-0.0.1/pyproject.toml +64 -0
  5. repocodex-0.0.1/setup.cfg +4 -0
  6. repocodex-0.0.1/src/repocodex/__init__.py +6 -0
  7. repocodex-0.0.1/src/repocodex/__main__.py +8 -0
  8. repocodex-0.0.1/src/repocodex/cli.py +189 -0
  9. repocodex-0.0.1/src/repocodex/commands/__init__.py +3 -0
  10. repocodex-0.0.1/src/repocodex/commands/advisory.py +95 -0
  11. repocodex-0.0.1/src/repocodex/commands/audit.py +153 -0
  12. repocodex-0.0.1/src/repocodex/commands/bootstrap.py +152 -0
  13. repocodex-0.0.1/src/repocodex/commands/context.py +28 -0
  14. repocodex-0.0.1/src/repocodex/commands/install.py +194 -0
  15. repocodex-0.0.1/src/repocodex/commands/reconcile.py +72 -0
  16. repocodex-0.0.1/src/repocodex/commands/relocate.py +131 -0
  17. repocodex-0.0.1/src/repocodex/commands/repair.py +125 -0
  18. repocodex-0.0.1/src/repocodex/commands/validate.py +321 -0
  19. repocodex-0.0.1/src/repocodex/commands/write.py +161 -0
  20. repocodex-0.0.1/src/repocodex/config.py +162 -0
  21. repocodex-0.0.1/src/repocodex/data/action/repocodex.yml +91 -0
  22. repocodex-0.0.1/src/repocodex/data/hooks/pre-commit +23 -0
  23. repocodex-0.0.1/src/repocodex/data/plugin/hooks/claude-pre-commit +2 -0
  24. repocodex-0.0.1/src/repocodex/data/plugin/hooks/cursor-pre-commit +4 -0
  25. repocodex-0.0.1/src/repocodex/data/plugin/hooks/pre-commit +23 -0
  26. repocodex-0.0.1/src/repocodex/data/plugin/mcp.json +8 -0
  27. repocodex-0.0.1/src/repocodex/data/plugin/plugin.json +7 -0
  28. repocodex-0.0.1/src/repocodex/data/plugin/skills/repocodex-coding/SKILL.md +70 -0
  29. repocodex-0.0.1/src/repocodex/data/plugin/skills/repocodex-review/SKILL.md +36 -0
  30. repocodex-0.0.1/src/repocodex/data/rules/claude/CLAUDE.md +3 -0
  31. repocodex-0.0.1/src/repocodex/data/rules/cursor/repocodex.mdc +6 -0
  32. repocodex-0.0.1/src/repocodex/data/skills/repocodex-coding/SKILL.md +70 -0
  33. repocodex-0.0.1/src/repocodex/data/skills/repocodex-review/SKILL.md +36 -0
  34. repocodex-0.0.1/src/repocodex/engine/__init__.py +3 -0
  35. repocodex-0.0.1/src/repocodex/engine/ack.py +34 -0
  36. repocodex-0.0.1/src/repocodex/engine/blocking.py +13 -0
  37. repocodex-0.0.1/src/repocodex/engine/code_impact.py +51 -0
  38. repocodex-0.0.1/src/repocodex/engine/contradiction.py +76 -0
  39. repocodex-0.0.1/src/repocodex/engine/dilution.py +69 -0
  40. repocodex-0.0.1/src/repocodex/engine/gate.py +329 -0
  41. repocodex-0.0.1/src/repocodex/engine/impact.py +84 -0
  42. repocodex-0.0.1/src/repocodex/engine/liveness.py +203 -0
  43. repocodex-0.0.1/src/repocodex/engine/match.py +232 -0
  44. repocodex-0.0.1/src/repocodex/engine/ratchet.py +289 -0
  45. repocodex-0.0.1/src/repocodex/engine/relocate.py +139 -0
  46. repocodex-0.0.1/src/repocodex/mcp_server.py +126 -0
  47. repocodex-0.0.1/src/repocodex/metrics.py +73 -0
  48. repocodex-0.0.1/src/repocodex/retrieval.py +166 -0
  49. repocodex-0.0.1/src/repocodex/schema.py +381 -0
  50. repocodex-0.0.1/src/repocodex/store/__init__.py +3 -0
  51. repocodex-0.0.1/src/repocodex/store/bundle.py +267 -0
  52. repocodex-0.0.1/src/repocodex/store/reverse_index.py +250 -0
  53. repocodex-0.0.1/src/repocodex/tools/__init__.py +3 -0
  54. repocodex-0.0.1/src/repocodex/tools/git.py +76 -0
  55. repocodex-0.0.1/src/repocodex/tools/ripgrep.py +102 -0
  56. repocodex-0.0.1/src/repocodex.egg-info/PKG-INFO +127 -0
  57. repocodex-0.0.1/src/repocodex.egg-info/SOURCES.txt +79 -0
  58. repocodex-0.0.1/src/repocodex.egg-info/dependency_links.txt +1 -0
  59. repocodex-0.0.1/src/repocodex.egg-info/entry_points.txt +2 -0
  60. repocodex-0.0.1/src/repocodex.egg-info/requires.txt +10 -0
  61. repocodex-0.0.1/src/repocodex.egg-info/top_level.txt +1 -0
  62. repocodex-0.0.1/tests/test_claims_required.py +70 -0
  63. repocodex-0.0.1/tests/test_cli_scaffold.py +22 -0
  64. repocodex-0.0.1/tests/test_commands.py +75 -0
  65. repocodex-0.0.1/tests/test_config.py +45 -0
  66. repocodex-0.0.1/tests/test_e2e.py +50 -0
  67. repocodex-0.0.1/tests/test_enforcement.py +99 -0
  68. repocodex-0.0.1/tests/test_engine.py +132 -0
  69. repocodex-0.0.1/tests/test_first_touch.py +229 -0
  70. repocodex-0.0.1/tests/test_identity_prefix.py +141 -0
  71. repocodex-0.0.1/tests/test_mcp.py +99 -0
  72. repocodex-0.0.1/tests/test_okf_v02.py +464 -0
  73. repocodex-0.0.1/tests/test_retrieval.py +79 -0
  74. repocodex-0.0.1/tests/test_review_gaps.py +599 -0
  75. repocodex-0.0.1/tests/test_schema.py +83 -0
  76. repocodex-0.0.1/tests/test_sharding.py +33 -0
  77. repocodex-0.0.1/tests/test_skill_type_recipe.py +67 -0
  78. repocodex-0.0.1/tests/test_store.py +79 -0
  79. repocodex-0.0.1/tests/test_tools.py +31 -0
  80. repocodex-0.0.1/tests/test_v2_review_gaps.py +692 -0
  81. repocodex-0.0.1/tests/test_validate.py +94 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ajay Lamba
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.
@@ -0,0 +1,127 @@
1
+ Metadata-Version: 2.4
2
+ Name: repocodex
3
+ Version: 0.0.1
4
+ Summary: Repository-native executable memory for coding agents
5
+ Author: Ajay Lamba
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/azaylamba/repocodex
8
+ Project-URL: Source, https://github.com/azaylamba/repocodex
9
+ Project-URL: Issues, https://github.com/azaylamba/repocodex/issues
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Requires-Python: >=3.11
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: typer>=0.27.1
15
+ Requires-Dist: pydantic>=2.13.4
16
+ Requires-Dist: pyyaml>=6.0.3
17
+ Provides-Extra: dev
18
+ Requires-Dist: pytest>=9.1.1; extra == "dev"
19
+ Requires-Dist: ruff>=0.12.0; extra == "dev"
20
+ Provides-Extra: mcp
21
+ Requires-Dist: mcp>=2.1.1; extra == "mcp"
22
+ Dynamic: license-file
23
+
24
+ # RepoCodex
25
+
26
+ [![Engine tests](https://img.shields.io/github/actions/workflow/status/azaylamba/repocodex/engine-tests.yml?branch=main&label=Engine%20tests)](https://github.com/azaylamba/repocodex/actions/workflows/engine-tests.yml)
27
+
28
+ **Git-native why next to code — with a pin check that proves it still matches.**
29
+
30
+ Coding agents write syntax well and forget _why_. Comments rot. Instruction files (`CLAUDE.md`, `AGENTS.md`, Cursor rules) never prove they still describe live text. Tests check behavior you remembered to assert; they do not keep institutional why attached to the lines that implement it.
31
+
32
+ RepoCodex stores that why in git beside the code. Agents retrieve it before they edit. A deterministic pin check (ripgrep + git) attests the attachment. Built for repositories where coding agents make the changes.
33
+
34
+ **Not** a test suite. **Not** another instruction file. **Not** a linter.
35
+
36
+ Experimental `0.0.1`. Requires Python 3.11+ and [ripgrep](https://github.com/BurntSushi/ripgrep) (`rg`) on `PATH`.
37
+
38
+ ## Install
39
+
40
+ ```bash
41
+ pip install "repocodex==0.0.1"
42
+ # or from a local clone: pip install -e .
43
+ repocodex install
44
+ ```
45
+
46
+ `repocodex install` writes the pre-commit hook, GitHub Action, agent skills, and `.repocodex.toml` (engine pin). The product is that loop: agents retrieve stored why before they edit, so they do not silently break existing behavior; the pin check fails the turn when why and code diverge, and a first substantive edit of an uncovered file is denied until a pinning concept is written.
47
+
48
+ ```bash
49
+ repocodex context src/billing/PaymentGateway.ts # retrieve why before edit
50
+ repocodex validate --diff # attest pins still hold
51
+ ```
52
+
53
+ Pin the engine in `.repocodex.toml`. Hook, local CLI, and CI resolve that pin so verdicts agree.
54
+
55
+ ## What a concept looks like
56
+
57
+ Types are orthogonal — one change may write more than one. Three common shapes:
58
+
59
+ **TechnicalDecision** (`decisions/…`) — why this construct exists:
60
+
61
+ ```yaml
62
+ ---
63
+ title: Capture streams enterprise retries instead of buffering
64
+ type: TechnicalDecision
65
+ verification:
66
+ engine: ripgrep
67
+ anchors:
68
+ - path: src/billing/PaymentGateway.ts
69
+ all_of: ["yield", "ENTERPRISE", "capturePayment"]
70
+ ---
71
+ Enterprise capture is a generator so retries stay backpressure-aware. Do not
72
+ replace with an in-memory list of attempts.
73
+ ```
74
+
75
+ **InvariantContract** (`invariants/…`) — must-hold token (requires `claims`):
76
+
77
+ ```yaml
78
+ ---
79
+ title: Enterprise capture grace is three attempts
80
+ type: InvariantContract
81
+ verification:
82
+ engine: ripgrep
83
+ anchors:
84
+ - path: src/billing/PaymentGateway.ts
85
+ all_of: ["ENTERPRISE", "grace", "= 3"]
86
+ claims:
87
+ - subject: enterprise_grace_attempts
88
+ literal: "3"
89
+ ---
90
+ Enterprise plans get three capture retries before failure. Do not silently shrink this window.
91
+ ```
92
+
93
+ **BusinessWorkflow** (`workflows/…`) — thin cross-package flow (one anchor per site):
94
+
95
+ ```yaml
96
+ ---
97
+ title: Checkout capture flows api → billing → ledger
98
+ type: BusinessWorkflow
99
+ verification:
100
+ engine: ripgrep
101
+ anchors:
102
+ - path: src/api/checkout.ts
103
+ all_of: ["capturePayment", "billing"]
104
+ - path: src/billing/PaymentGateway.ts
105
+ all_of: ["capturePayment", "ledger"]
106
+ - path: src/ledger/posting.ts
107
+ all_of: ["postCapture", "idempotency"]
108
+ ---
109
+ Checkout capture crosses api, billing, then ledger. Keep that order; do not
110
+ post to the ledger from the API layer.
111
+ ```
112
+
113
+ ## Docs
114
+
115
+ | Doc | Job |
116
+ | --- | --- |
117
+ | [How it works](https://github.com/azaylamba/repocodex/blob/main/docs/how-it-works.md) | Purpose, benefit, and the retrieve → read → edit → update why → pin-check loop |
118
+ | [Memory](https://github.com/azaylamba/repocodex/blob/main/docs/memory.md) | How to read `.context/` |
119
+ | [Agents](https://github.com/azaylamba/repocodex/blob/main/docs/agents.md) | How coding agents (and optionally humans) run the loop |
120
+ | [Install](https://github.com/azaylamba/repocodex/blob/main/docs/install.md) | CLI, pin, hook, GitHub Action, optional MCP |
121
+ | [Architecture](https://github.com/azaylamba/repocodex/blob/main/docs/architecture.md) | Current engine architecture (further reading) |
122
+
123
+ ## License and contributing
124
+
125
+ [MIT](https://github.com/azaylamba/repocodex/blob/main/LICENSE) · [Contributing](https://github.com/azaylamba/repocodex/blob/main/CONTRIBUTING.md) · [Security](https://github.com/azaylamba/repocodex/blob/main/SECURITY.md)
126
+
127
+ Created by [Ajay Lamba](https://github.com/azaylamba/repocodex).
@@ -0,0 +1,104 @@
1
+ # RepoCodex
2
+
3
+ [![Engine tests](https://img.shields.io/github/actions/workflow/status/azaylamba/repocodex/engine-tests.yml?branch=main&label=Engine%20tests)](https://github.com/azaylamba/repocodex/actions/workflows/engine-tests.yml)
4
+
5
+ **Git-native why next to code — with a pin check that proves it still matches.**
6
+
7
+ Coding agents write syntax well and forget _why_. Comments rot. Instruction files (`CLAUDE.md`, `AGENTS.md`, Cursor rules) never prove they still describe live text. Tests check behavior you remembered to assert; they do not keep institutional why attached to the lines that implement it.
8
+
9
+ RepoCodex stores that why in git beside the code. Agents retrieve it before they edit. A deterministic pin check (ripgrep + git) attests the attachment. Built for repositories where coding agents make the changes.
10
+
11
+ **Not** a test suite. **Not** another instruction file. **Not** a linter.
12
+
13
+ Experimental `0.0.1`. Requires Python 3.11+ and [ripgrep](https://github.com/BurntSushi/ripgrep) (`rg`) on `PATH`.
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ pip install "repocodex==0.0.1"
19
+ # or from a local clone: pip install -e .
20
+ repocodex install
21
+ ```
22
+
23
+ `repocodex install` writes the pre-commit hook, GitHub Action, agent skills, and `.repocodex.toml` (engine pin). The product is that loop: agents retrieve stored why before they edit, so they do not silently break existing behavior; the pin check fails the turn when why and code diverge, and a first substantive edit of an uncovered file is denied until a pinning concept is written.
24
+
25
+ ```bash
26
+ repocodex context src/billing/PaymentGateway.ts # retrieve why before edit
27
+ repocodex validate --diff # attest pins still hold
28
+ ```
29
+
30
+ Pin the engine in `.repocodex.toml`. Hook, local CLI, and CI resolve that pin so verdicts agree.
31
+
32
+ ## What a concept looks like
33
+
34
+ Types are orthogonal — one change may write more than one. Three common shapes:
35
+
36
+ **TechnicalDecision** (`decisions/…`) — why this construct exists:
37
+
38
+ ```yaml
39
+ ---
40
+ title: Capture streams enterprise retries instead of buffering
41
+ type: TechnicalDecision
42
+ verification:
43
+ engine: ripgrep
44
+ anchors:
45
+ - path: src/billing/PaymentGateway.ts
46
+ all_of: ["yield", "ENTERPRISE", "capturePayment"]
47
+ ---
48
+ Enterprise capture is a generator so retries stay backpressure-aware. Do not
49
+ replace with an in-memory list of attempts.
50
+ ```
51
+
52
+ **InvariantContract** (`invariants/…`) — must-hold token (requires `claims`):
53
+
54
+ ```yaml
55
+ ---
56
+ title: Enterprise capture grace is three attempts
57
+ type: InvariantContract
58
+ verification:
59
+ engine: ripgrep
60
+ anchors:
61
+ - path: src/billing/PaymentGateway.ts
62
+ all_of: ["ENTERPRISE", "grace", "= 3"]
63
+ claims:
64
+ - subject: enterprise_grace_attempts
65
+ literal: "3"
66
+ ---
67
+ Enterprise plans get three capture retries before failure. Do not silently shrink this window.
68
+ ```
69
+
70
+ **BusinessWorkflow** (`workflows/…`) — thin cross-package flow (one anchor per site):
71
+
72
+ ```yaml
73
+ ---
74
+ title: Checkout capture flows api → billing → ledger
75
+ type: BusinessWorkflow
76
+ verification:
77
+ engine: ripgrep
78
+ anchors:
79
+ - path: src/api/checkout.ts
80
+ all_of: ["capturePayment", "billing"]
81
+ - path: src/billing/PaymentGateway.ts
82
+ all_of: ["capturePayment", "ledger"]
83
+ - path: src/ledger/posting.ts
84
+ all_of: ["postCapture", "idempotency"]
85
+ ---
86
+ Checkout capture crosses api, billing, then ledger. Keep that order; do not
87
+ post to the ledger from the API layer.
88
+ ```
89
+
90
+ ## Docs
91
+
92
+ | Doc | Job |
93
+ | --- | --- |
94
+ | [How it works](https://github.com/azaylamba/repocodex/blob/main/docs/how-it-works.md) | Purpose, benefit, and the retrieve → read → edit → update why → pin-check loop |
95
+ | [Memory](https://github.com/azaylamba/repocodex/blob/main/docs/memory.md) | How to read `.context/` |
96
+ | [Agents](https://github.com/azaylamba/repocodex/blob/main/docs/agents.md) | How coding agents (and optionally humans) run the loop |
97
+ | [Install](https://github.com/azaylamba/repocodex/blob/main/docs/install.md) | CLI, pin, hook, GitHub Action, optional MCP |
98
+ | [Architecture](https://github.com/azaylamba/repocodex/blob/main/docs/architecture.md) | Current engine architecture (further reading) |
99
+
100
+ ## License and contributing
101
+
102
+ [MIT](https://github.com/azaylamba/repocodex/blob/main/LICENSE) · [Contributing](https://github.com/azaylamba/repocodex/blob/main/CONTRIBUTING.md) · [Security](https://github.com/azaylamba/repocodex/blob/main/SECURITY.md)
103
+
104
+ Created by [Ajay Lamba](https://github.com/azaylamba/repocodex).
@@ -0,0 +1,64 @@
1
+ [build-system]
2
+ requires = ["setuptools>=84.0.0", "wheel>=0.48.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "repocodex"
7
+ version = "0.0.1"
8
+ description = "Repository-native executable memory for coding agents"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "Ajay Lamba" }]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ ]
17
+ dependencies = [
18
+ "typer>=0.27.1",
19
+ "pydantic>=2.13.4",
20
+ "pyyaml>=6.0.3",
21
+ ]
22
+
23
+ [project.urls]
24
+ Homepage = "https://github.com/azaylamba/repocodex"
25
+ Source = "https://github.com/azaylamba/repocodex"
26
+ Issues = "https://github.com/azaylamba/repocodex/issues"
27
+
28
+ [project.optional-dependencies]
29
+ dev = [
30
+ "pytest>=9.1.1",
31
+ "ruff>=0.12.0",
32
+ ]
33
+ mcp = [
34
+ "mcp>=2.1.1",
35
+ ]
36
+
37
+ [project.scripts]
38
+ repocodex = "repocodex.cli:app"
39
+
40
+ [tool.setuptools.packages.find]
41
+ where = ["src"]
42
+
43
+ [tool.setuptools.package-data]
44
+ repocodex = ["data/**/*"]
45
+
46
+ [tool.pytest.ini_options]
47
+ pythonpath = ["src"]
48
+ testpaths = ["tests"]
49
+ addopts = "-q"
50
+ filterwarnings = ["ignore::DeprecationWarning"]
51
+
52
+ [tool.ruff]
53
+ target-version = "py311"
54
+ src = ["src", "tests"]
55
+
56
+ [tool.ruff.lint]
57
+ select = ["D"]
58
+ ignore = ["D105", "D107"]
59
+
60
+ [tool.ruff.lint.pydocstyle]
61
+ convention = "google"
62
+
63
+ [tool.ruff.lint.per-file-ignores]
64
+ "tests/test_*.py" = ["D101", "D102", "D103"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,6 @@
1
+ """RepoCodex engine: repository-native executable memory for coding agents."""
2
+
3
+ from __future__ import annotations
4
+
5
+ ENGINE_VERSION = "0.0.1"
6
+ __version__ = ENGINE_VERSION
@@ -0,0 +1,8 @@
1
+ """Run the RepoCodex CLI as `python -m repocodex`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from repocodex.cli import app
6
+
7
+ if __name__ == "__main__":
8
+ app()
@@ -0,0 +1,189 @@
1
+ """Typer CLI for RepoCodex. Commands print JSON envelopes and exit non-zero on failure."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+ from typing import Optional
8
+
9
+ import typer
10
+
11
+ from repocodex.commands.advisory import advisory as run_advisory
12
+ from repocodex.commands.audit import audit as run_audit
13
+ from repocodex.commands.bootstrap import bootstrap as run_bootstrap
14
+ from repocodex.commands.context import context_for
15
+ from repocodex.commands.install import install as run_install
16
+ from repocodex.commands.reconcile import apply_anchor_patch, reconcile_memory
17
+ from repocodex.commands.relocate import relocate_memory
18
+ from repocodex.commands.repair import repair as run_repair
19
+ from repocodex.commands.validate import validate as run_validate
20
+ from repocodex.commands.write import write_memory
21
+ from repocodex.config import EngineVersionMismatch
22
+ from repocodex.schema import envelope
23
+
24
+ app = typer.Typer(no_args_is_help=True, add_completion=False, help="RepoCodex executable memory CLI")
25
+
26
+
27
+ def _emit(payload: dict, exit_code: int = 0) -> None:
28
+ """Print ``payload`` as JSON and exit with ``exit_code``."""
29
+ typer.echo(json.dumps(payload, indent=2))
30
+ raise typer.Exit(exit_code)
31
+
32
+
33
+ def _repo() -> Path:
34
+ """Return the process working directory as the target repository."""
35
+ return Path.cwd()
36
+
37
+
38
+ def _guarded(fn):
39
+ """Run ``fn`` and emit an engine-version-mismatch envelope on pin failure."""
40
+ try:
41
+ return fn()
42
+ except EngineVersionMismatch as exc:
43
+ _emit(exc.to_json(), 1)
44
+ raise
45
+
46
+
47
+ @app.command("validate")
48
+ def validate_command(
49
+ diff: bool = typer.Option(False, "--diff", help="Attest anchors intersecting the diff"),
50
+ base: Optional[str] = typer.Option(None, "--base", help="Git diff base, e.g. origin/main...HEAD"),
51
+ staged: bool = typer.Option(False, "--staged"),
52
+ all_concepts: bool = typer.Option(False, "--all"),
53
+ check: bool = typer.Option(False, "--check", help="Exit 1 on deterministic blocking outcomes"),
54
+ hook: bool = typer.Option(False, "--hook"),
55
+ memory_exempt: bool = typer.Option(False, "--memory-exempt"),
56
+ review_ack: bool = typer.Option(False, "--review-ack", hidden=True),
57
+ ack_file: Optional[Path] = typer.Option(None, "--ack-file", help="Tracked review-agent acknowledgment record"),
58
+ apply_patches: bool = typer.Option(False, "--apply-patches"),
59
+ ) -> None:
60
+ """Attest anchors on the working tree or diff. JSON includes engine_version."""
61
+ payload = _guarded(
62
+ lambda: run_validate(
63
+ _repo(),
64
+ base=base,
65
+ staged=staged or hook,
66
+ all_concepts=all_concepts or not diff,
67
+ memory_exempt=memory_exempt,
68
+ review_ack=review_ack,
69
+ ack_file=str(ack_file) if ack_file else None,
70
+ )
71
+ )
72
+ if apply_patches:
73
+ for patch in payload.get("patches") or []:
74
+ apply_anchor_patch(_repo(), patch)
75
+ payload.setdefault("applied_patches", []).append(patch)
76
+ code = 1 if (check or hook) and payload.get("blocking") else 0
77
+ _emit(payload, code)
78
+
79
+
80
+ @app.command("write")
81
+ def write_command(
82
+ concept: Optional[Path] = typer.Argument(None),
83
+ identity: Optional[str] = typer.Option(None, "--identity"),
84
+ stdin: bool = typer.Option(False, "--stdin"),
85
+ ) -> None:
86
+ """Write-gate a concept into .context/."""
87
+ text = None
88
+ if stdin:
89
+ text = typer.get_text_stream("stdin").read()
90
+ if concept is None and text is None:
91
+ raise typer.BadParameter("provide a concept file or --stdin")
92
+ payload = _guarded(lambda: write_memory(_repo(), concept or Path("."), identity=identity, stdin_text=text))
93
+ _emit(payload, 0 if payload.get("accepted") else 1)
94
+
95
+
96
+ @app.command("relocate")
97
+ def relocate_command(
98
+ identity: Optional[str] = typer.Argument(None),
99
+ mismatched: bool = typer.Option(
100
+ False, "--mismatched", help="Move all authored-type concepts with wrong prefixes"
101
+ ),
102
+ ) -> None:
103
+ """Move authored concepts into the type-folder identity required by their type."""
104
+ if not mismatched and not identity:
105
+ raise typer.BadParameter("provide an identity or --mismatched")
106
+ payload = _guarded(lambda: relocate_memory(_repo(), identity, mismatched=mismatched))
107
+ _emit(payload)
108
+
109
+
110
+ @app.command("reconcile")
111
+ def reconcile_command(
112
+ concept: Optional[Path] = typer.Argument(None),
113
+ identity: Optional[str] = typer.Option(None, "--identity"),
114
+ apply_patch: Optional[str] = typer.Option(None, "--apply-patch", help="JSON patch object"),
115
+ ) -> None:
116
+ """Repair DRIFT with gate-enforced new anchors, or apply a REANCHOR patch."""
117
+ repo = _repo()
118
+ if apply_patch:
119
+ patch = json.loads(apply_patch)
120
+ path = apply_anchor_patch(repo, patch)
121
+ _emit(envelope({"applied": True, "path": str(path)}))
122
+ if concept is None:
123
+ raise typer.BadParameter("provide a concept file")
124
+ payload = _guarded(lambda: reconcile_memory(repo, concept, identity=identity))
125
+ _emit(payload, 0 if payload.get("accepted") else 1)
126
+
127
+
128
+ @app.command("context")
129
+ def context_command(
130
+ paths: list[Path] = typer.Argument(..., metavar="PATHS"),
131
+ drafts: bool = typer.Option(False, "--drafts"),
132
+ ) -> None:
133
+ """Staged retrieval: reverse index → catalogs → bodies + one link-hop of titles."""
134
+ payload = _guarded(lambda: context_for(_repo(), [str(p) for p in paths], include_drafts=drafts))
135
+ _emit(payload)
136
+
137
+
138
+ @app.command("repair")
139
+ def repair_command() -> None:
140
+ """Invoke a repair agent against the current RECONCILE state."""
141
+ payload = _guarded(lambda: run_repair(_repo()))
142
+ code = 1 if payload.get("error") else 0
143
+ _emit(payload, code)
144
+
145
+
146
+ @app.command("install")
147
+ def install_command(
148
+ mcp: bool = typer.Option(False, "--mcp", help="Register optional MCP wrapper"),
149
+ ) -> None:
150
+ """Install pre-commit hook, GitHub Action, skills, and optional MCP."""
151
+ payload = _guarded(lambda: run_install(_repo(), mcp=mcp))
152
+ _emit(payload, 0 if payload.get("ok", True) else 1)
153
+
154
+
155
+ @app.command("bootstrap")
156
+ def bootstrap_command() -> None:
157
+ """Mine history/comments/docs; keep only gate-passing drafts."""
158
+ _emit(_guarded(lambda: run_bootstrap(_repo())))
159
+
160
+
161
+ @app.command("audit")
162
+ def audit_command(
163
+ sample_size: int = typer.Option(10, "--sample-size"),
164
+ seed: int = typer.Option(0, "--seed"),
165
+ findings: Optional[Path] = typer.Option(None, "--findings", help="Out-of-band screening result JSON"),
166
+ ) -> None:
167
+ """Emit a screening payload for out-of-band review. No model is invoked."""
168
+ _emit(
169
+ _guarded(
170
+ lambda: run_audit(_repo(), sample_size=sample_size, seed=seed, findings_path=findings)
171
+ )
172
+ )
173
+
174
+
175
+ @app.command("advisory")
176
+ def advisory_command(
177
+ base: Optional[str] = typer.Option(None, "--base"),
178
+ staged: bool = typer.Option(False, "--staged"),
179
+ ) -> None:
180
+ """Agent-judged findings for the advisory CI check. Never affects the required verdict."""
181
+ _emit(_guarded(lambda: run_advisory(_repo(), base=base, staged=staged)))
182
+
183
+
184
+ @app.command("mcp")
185
+ def mcp_command() -> None:
186
+ """Run the optional MCP server wrapping the CLI."""
187
+ from repocodex.mcp_server import run_mcp
188
+
189
+ run_mcp()
@@ -0,0 +1,3 @@
1
+ """CLI command implementations invoked from `repocodex.cli`."""
2
+
3
+ from __future__ import annotations
@@ -0,0 +1,95 @@
1
+ """Rank code-side impact and wrap optional agent judgments into a non-blocking advisory envelope."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from repocodex.commands.validate import changed_files
9
+ from repocodex.config import load_config
10
+ from repocodex.engine.code_impact import rank_code_hits
11
+ from repocodex.schema import envelope
12
+
13
+ NOT_EVALUATED = "not_evaluated"
14
+ EVALUATED = "evaluated"
15
+
16
+
17
+ def _category(status: str, findings: list[dict] | None = None) -> dict[str, Any]:
18
+ """Build a category payload, attaching findings only when evaluated."""
19
+ payload: dict[str, Any] = {"status": status}
20
+ if status == EVALUATED:
21
+ payload["findings"] = findings or []
22
+ return payload
23
+
24
+
25
+ def advisory(
26
+ repo: Path,
27
+ *,
28
+ base: str | None = None,
29
+ staged: bool = False,
30
+ judgments: dict[str, list[dict]] | None = None,
31
+ ) -> dict:
32
+ """Return a non-blocking advisory envelope for the current diff.
33
+
34
+ Skips ``.context/`` paths and ``reverse-index.md``. Judgment categories
35
+ without a key in ``judgments`` stay ``not_evaluated``.
36
+
37
+ Returns:
38
+ Envelope with ``kind`` ``advisory``, ``code_side_impact`` (path/hits
39
+ rows), ``prose_versus_diff``, ``skipped_recipe_steps``, ``churn_flags``
40
+ (each ``status`` plus optional ``findings``), ``agent_judgment``, and
41
+ ``required_verdict_unaffected`` always ``True``.
42
+
43
+ """
44
+ config = load_config(repo)
45
+ files = changed_files(repo, base=base, staged=staged)
46
+ code_side: list[dict] = []
47
+ for path in files:
48
+ if path.startswith(".context/") or path.endswith("reverse-index.md"):
49
+ continue
50
+ symbols = Path(path).stem
51
+ hits = rank_code_hits(path, symbols, repo, cap=config.impact_read_cap, exclusions=config.all_exclusions)
52
+ if hits:
53
+ code_side.append({"path": path, "hits": hits})
54
+
55
+ judgments = judgments or {}
56
+
57
+ def category_for(name: str) -> dict[str, Any]:
58
+ if name in judgments:
59
+ return _category(EVALUATED, judgments[name])
60
+ return _category(NOT_EVALUATED)
61
+
62
+ prose = category_for("prose_versus_diff")
63
+ skipped = category_for("skipped_recipe_steps")
64
+ churn = category_for("churn_flags")
65
+ any_judgment = any(cat["status"] == EVALUATED for cat in (prose, skipped, churn))
66
+ return envelope(
67
+ {
68
+ "kind": "advisory",
69
+ "code_side_impact": code_side,
70
+ "prose_versus_diff": prose,
71
+ "skipped_recipe_steps": skipped,
72
+ "churn_flags": churn,
73
+ "agent_judgment": any_judgment,
74
+ "required_verdict_unaffected": True,
75
+ }
76
+ )
77
+
78
+
79
+ def scenario_integrity_status(root: Path) -> dict:
80
+ """Report whether an OKF concept bundle exists for scenario integrity.
81
+
82
+ Never falls back to a test table.
83
+
84
+ Returns:
85
+ ``{"status": "unsatisfied", "reason": "no_okf_bundle"}`` when no
86
+ concepts are loaded, otherwise
87
+ ``{"status": "available", "reason": "agent_read_okf"}``.
88
+
89
+ """
90
+ from repocodex.store.bundle import load_concepts
91
+
92
+ concepts = load_concepts(root)
93
+ if not concepts:
94
+ return {"status": "unsatisfied", "reason": "no_okf_bundle"}
95
+ return {"status": "available", "reason": "agent_read_okf"}