ctxora 6.2.0

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 (115) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +441 -0
  3. package/README.vi.md +441 -0
  4. package/bin/ctxora.mjs +147 -0
  5. package/package.json +45 -0
  6. package/pyproject.toml +59 -0
  7. package/src/chunking/compressor.py +104 -0
  8. package/src/chunking/treesitter_chunker.py +240 -0
  9. package/src/compact/anthropic.py +98 -0
  10. package/src/compact/gemini.py +88 -0
  11. package/src/compact/handoff.py +179 -0
  12. package/src/compact/openai.py +318 -0
  13. package/src/compact/summarizer.py +186 -0
  14. package/src/context/assembler.py +298 -0
  15. package/src/context/budgeting.py +137 -0
  16. package/src/context/sanitizer.py +23 -0
  17. package/src/evaluation/__init__.py +1 -0
  18. package/src/evaluation/gates.py +172 -0
  19. package/src/evaluation/metrics.py +41 -0
  20. package/src/harness_context/__init__.py +5 -0
  21. package/src/harness_context/adapters/__init__.py +1 -0
  22. package/src/harness_context/adapters/clients/__init__.py +4 -0
  23. package/src/harness_context/adapters/clients/formatters.py +47 -0
  24. package/src/harness_context/adapters/clients/profiles.py +29 -0
  25. package/src/harness_context/adapters/ecc/__init__.py +4 -0
  26. package/src/harness_context/adapters/ecc/detection.py +41 -0
  27. package/src/harness_context/adapters/ecc/mapping.py +32 -0
  28. package/src/harness_context/adapters/ecc/memory_reader.py +162 -0
  29. package/src/harness_context/adapters/ecc/provenance.py +16 -0
  30. package/src/harness_context/api/__init__.py +1 -0
  31. package/src/harness_context/api/v2/__init__.py +12 -0
  32. package/src/harness_context/api/v2/contracts.py +119 -0
  33. package/src/harness_context/api/v2/diagnostics.py +13 -0
  34. package/src/harness_context/api/v2/enums.py +17 -0
  35. package/src/harness_context/api/v2/errors.py +32 -0
  36. package/src/harness_context/api/v2/models.py +4 -0
  37. package/src/harness_context/api/v2/requests.py +17 -0
  38. package/src/harness_context/api/v2/responses.py +22 -0
  39. package/src/harness_context/application/__init__.py +3 -0
  40. package/src/harness_context/application/container.py +31 -0
  41. package/src/harness_context/application/context_service.py +51 -0
  42. package/src/harness_context/application/ecc_service.py +7 -0
  43. package/src/harness_context/application/handoff_service.py +11 -0
  44. package/src/harness_context/application/memory_service.py +9 -0
  45. package/src/harness_context/application/protocols.py +46 -0
  46. package/src/harness_context/application/refresh_service.py +25 -0
  47. package/src/harness_context/application/retrieval_service.py +22 -0
  48. package/src/harness_context/application/services.py +4 -0
  49. package/src/harness_context/application/workspace_service.py +18 -0
  50. package/src/harness_context/bootstrap.py +47 -0
  51. package/src/harness_context/branding.py +16 -0
  52. package/src/harness_context/cli/__init__.py +1 -0
  53. package/src/harness_context/cli/app.py +239 -0
  54. package/src/harness_context/cli/exit_codes.py +25 -0
  55. package/src/harness_context/domain/__init__.py +9 -0
  56. package/src/harness_context/domain/cag.py +18 -0
  57. package/src/harness_context/domain/chunking.py +17 -0
  58. package/src/harness_context/domain/planning.py +30 -0
  59. package/src/harness_context/domain/ports.py +24 -0
  60. package/src/harness_context/domain/retrieval.py +46 -0
  61. package/src/harness_context/engine.py +10 -0
  62. package/src/harness_context/free_tools.py +143 -0
  63. package/src/harness_context/infrastructure/__init__.py +10 -0
  64. package/src/harness_context/infrastructure/graph.py +26 -0
  65. package/src/harness_context/infrastructure/indexes.py +33 -0
  66. package/src/harness_context/infrastructure/local_engine.py +296 -0
  67. package/src/harness_context/infrastructure/parsing.py +38 -0
  68. package/src/harness_context/infrastructure/scanning.py +51 -0
  69. package/src/harness_context/installer/__init__.py +4 -0
  70. package/src/harness_context/installer/models.py +22 -0
  71. package/src/harness_context/installer/service.py +168 -0
  72. package/src/harness_context/mcp/__init__.py +3 -0
  73. package/src/harness_context/mcp/capabilities.py +11 -0
  74. package/src/harness_context/mcp/errors.py +8 -0
  75. package/src/harness_context/mcp/lifecycle.py +72 -0
  76. package/src/harness_context/mcp/middleware.py +57 -0
  77. package/src/harness_context/mcp/server.py +3 -0
  78. package/src/harness_context/mcp/tool_handlers/__init__.py +7 -0
  79. package/src/harness_context/mcp/tool_handlers/context.py +16 -0
  80. package/src/harness_context/mcp/tool_handlers/ecc.py +8 -0
  81. package/src/harness_context/mcp/tool_handlers/handoffs.py +20 -0
  82. package/src/harness_context/mcp/tool_handlers/memory.py +16 -0
  83. package/src/harness_context/mcp/tool_handlers/workspace.py +12 -0
  84. package/src/harness_context/mcp/tools.py +15 -0
  85. package/src/harness_context/observability/__init__.py +6 -0
  86. package/src/harness_context/observability/events.py +25 -0
  87. package/src/harness_context/observability/metrics.py +20 -0
  88. package/src/harness_context/paths.py +35 -0
  89. package/src/harness_context/runtime.py +127 -0
  90. package/src/harness_context/schemas.py +38 -0
  91. package/src/harness_context/security/__init__.py +3 -0
  92. package/src/harness_context/security/secret_patterns.py +15 -0
  93. package/src/harness_context/server.py +1077 -0
  94. package/src/harness_context/storage/__init__.py +6 -0
  95. package/src/harness_context/storage/migrations.py +24 -0
  96. package/src/harness_context/storage/pins.py +10 -0
  97. package/src/harness_context/storage/snapshots.py +149 -0
  98. package/src/harness_context/tokenize.py +12 -0
  99. package/src/harness_context/topology.py +65 -0
  100. package/src/harness_context/watcher/__init__.py +3 -0
  101. package/src/harness_context/watcher/service.py +32 -0
  102. package/src/harness_context/workspace/__init__.py +13 -0
  103. package/src/harness_context/workspace/identity.py +9 -0
  104. package/src/harness_context/workspace/lock.py +24 -0
  105. package/src/harness_context/workspace/policy.py +3 -0
  106. package/src/harness_context/workspace/roots.py +84 -0
  107. package/src/harness_context/workspace/state.py +35 -0
  108. package/src/memory/episodic.py +257 -0
  109. package/src/memory/vector_store.py +104 -0
  110. package/src/retrieval/bm25.py +23 -0
  111. package/src/retrieval/cache.py +76 -0
  112. package/src/retrieval/embeddings.py +75 -0
  113. package/src/retrieval/graph.py +45 -0
  114. package/src/retrieval/reranker.py +78 -0
  115. package/src/retrieval/tokenize.py +11 -0
@@ -0,0 +1,51 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+
5
+ from compact.handoff import ConversationHandoffStore
6
+ from harness_context.adapters.ecc import EccMemoryReader
7
+ from harness_context.api.v2 import ContextPackageV2, PrepareContextRequest
8
+ from harness_context.engine import ContextEngine
9
+ from harness_context.schemas import HarnessError
10
+ from memory.episodic import MemoryStore
11
+
12
+
13
+ class ContextService:
14
+ def __init__(self, engine: ContextEngine, memory: MemoryStore, handoffs: ConversationHandoffStore, ecc: EccMemoryReader | None = None):
15
+ self.engine = engine
16
+ self.memory = memory
17
+ self.handoffs = handoffs
18
+ self.ecc = ecc
19
+
20
+ def prepare(self, request: PrepareContextRequest) -> ContextPackageV2:
21
+ if request.deadline_ms <= 0:
22
+ raise HarnessError("invalid_deadline", "deadline_ms must be positive")
23
+ started = time.perf_counter()
24
+ preferred = "" if request.preferred_strategy == "auto" else request.preferred_strategy
25
+ with self.engine.snapshot_pin(request.workspace_id) as pin:
26
+ if request.freshness == "current" and not self.engine.snapshot_is_current(request.workspace_id, pin.state):
27
+ raise HarnessError("stale_snapshot", "active snapshot is stale; refresh_workspace is required")
28
+ prepared = self.engine.prepare_context(request.workspace_id, request.query, request.available_input_tokens, preferred, pin)
29
+ evidence = prepared["retrieval"]
30
+ memory = self.memory.search(request.query, top_k=4, workspace_id=request.workspace_id) if request.include_memory else []
31
+ handoff = None
32
+ if request.include_handoff:
33
+ if not request.handoff_id:
34
+ raise HarnessError("missing_handoff_id", "handoff_id is required when include_handoff is true")
35
+ handoff = self.handoffs.restore(request.handoff_id, request.workspace_id)
36
+ if handoff is None:
37
+ raise HarnessError("handoff_not_found", "handoff was not found in this workspace")
38
+ external_context, ecc_diagnostics = [], []
39
+ if request.include_ecc:
40
+ if self.ecc is None or not self.ecc.installation.available:
41
+ raise HarnessError("ecc_not_available", "ECC adapter is not enabled or no local ECC vault was detected")
42
+ ecc_result = self.ecc.search(request.query, limit=4)
43
+ external_context, ecc_diagnostics = ecc_result["entries"], ecc_result["diagnostics"]
44
+ elapsed_ms = (time.perf_counter() - started) * 1000
45
+ if elapsed_ms > request.deadline_ms:
46
+ raise HarnessError("deadline_exceeded", "context preparation exceeded deadline_ms")
47
+ diagnostics = {"coverage": evidence.get("coverage", "not_applicable") if evidence else "not_applicable", "freshness": request.freshness, "untrusted_content": True, "latency_ms": round(elapsed_ms, 3), "ecc": {"enabled": self.ecc is not None, "diagnostics": ecc_diagnostics}}
48
+ return ContextPackageV2(api_version="2.0", workspace_id=request.workspace_id, snapshot_id=prepared["snapshot_id"], snapshot_version=prepared["snapshot_version"], plan=prepared["plan"], stable_context=prepared["bundle"], evidence=evidence, memory=memory, handoff=handoff, external_context=external_context, diagnostics=diagnostics)
49
+
50
+ def prepare_values(self, **values) -> dict:
51
+ return self.prepare(PrepareContextRequest(**values)).to_dict()
@@ -0,0 +1,7 @@
1
+ class EccService:
2
+ def __init__(self, reader): self.reader = reader
3
+ def status(self): return self.reader.status() if self.reader else {"enabled": False, "available": False, "read_only": True}
4
+ def search(self, query, limit=4):
5
+ if self.reader is None or not self.reader.installation.available:
6
+ raise ValueError("ECC adapter is not enabled or no local ECC vault was detected")
7
+ return self.reader.search(query, limit)
@@ -0,0 +1,11 @@
1
+ class HandoffService:
2
+ def __init__(self, store): self.store = store
3
+ def prepare(self, workspace_id, messages_json, threshold_tokens=30_000, label="", retention_seconds=604_800, consent=False): return self.store.prepare(messages_json, threshold_tokens, label, workspace_id, retention_seconds, consent)
4
+ def restore(self, workspace_id, handoff_id):
5
+ result = self.store.restore(handoff_id, workspace_id)
6
+ if result is None:
7
+ raise ValueError("handoff not found")
8
+ return result
9
+ def list(self, workspace_id, limit=30): return self.store.list(workspace_id, limit)
10
+ def delete(self, workspace_id, handoff_id): return {"deleted": self.store.delete(handoff_id, workspace_id)}
11
+ def purge(self): return {"purged": self.store.purge_expired()}
@@ -0,0 +1,9 @@
1
+ from memory.episodic import MemoryType
2
+
3
+
4
+ class MemoryService:
5
+ def __init__(self, store): self.store = store
6
+ def save(self, workspace_id, key, value, mtype="semantic", tags="", scope="workspace", source="user", confidence=1.0, expires_at=None): return self.store.save(MemoryType(mtype), key, value, tags, workspace_id=workspace_id, scope=scope, source=source, confidence=confidence, expires_at=expires_at)
7
+ def search(self, workspace_id, query, mtype="", top_k=5, min_sim=0.12): return self.store.search(query, MemoryType(mtype) if mtype else None, top_k, min_sim, workspace_id)
8
+ def delete(self, workspace_id, key, mtype="semantic"): return {"deleted": self.store.delete(MemoryType(mtype), key, workspace_id)}
9
+ def list(self, workspace_id, mtype="", limit=30): return self.store.list_keys(MemoryType(mtype) if mtype else None, limit, workspace_id)
@@ -0,0 +1,46 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Protocol
4
+
5
+ from harness_context.api.v2 import ContextPackageV2, PrepareContextRequest
6
+
7
+
8
+ class RefreshCommands(Protocol):
9
+ def execute(self, workspace_id: str, paths: list[str] | None = None) -> dict: ...
10
+ def invalidate(self, workspace_id: str, target: str) -> dict: ...
11
+
12
+
13
+ class ContextQueries(Protocol):
14
+ def prepare(self, request: PrepareContextRequest) -> ContextPackageV2: ...
15
+ def prepare_values(self, **values) -> dict: ...
16
+
17
+
18
+ class WorkspaceCommands(Protocol):
19
+ def register(self, workspace_id: str, roots: list[str], initial_refresh: bool = False) -> dict: ...
20
+
21
+
22
+ class RetrievalQueries(Protocol):
23
+ def plan(self, workspace_id: str, query: str, available_input_tokens: int, strategy_override: str = "") -> dict: ...
24
+ def retrieve(self, workspace_id: str, query: str, top_k: int = 12, graph_expand: bool = True, token_budget: int = 4_000) -> dict: ...
25
+ def stats(self, workspace_id: str) -> dict: ...
26
+ def snapshot_is_current(self, workspace_id: str) -> bool: ...
27
+
28
+
29
+ class MemoryCommandsQueries(Protocol):
30
+ def save(self, workspace_id: str, key: str, value: str, **options) -> dict: ...
31
+ def search(self, workspace_id: str, query: str, **options) -> list[dict]: ...
32
+ def delete(self, workspace_id: str, key: str, mtype: str = "semantic") -> dict: ...
33
+ def list(self, workspace_id: str, mtype: str = "", limit: int = 30) -> list[str]: ...
34
+
35
+
36
+ class HandoffCommandsQueries(Protocol):
37
+ def prepare(self, workspace_id: str, messages_json: str, **options) -> dict: ...
38
+ def restore(self, workspace_id: str, handoff_id: str) -> dict: ...
39
+ def list(self, workspace_id: str, limit: int = 30) -> list[dict]: ...
40
+ def delete(self, workspace_id: str, handoff_id: str) -> dict: ...
41
+ def purge(self) -> dict: ...
42
+
43
+
44
+ class EccQueries(Protocol):
45
+ def status(self) -> dict: ...
46
+ def search(self, query: str, limit: int = 4) -> dict: ...
@@ -0,0 +1,25 @@
1
+ from harness_context.engine import ContextEngine
2
+ from harness_context.storage import SnapshotStore
3
+
4
+
5
+ class RefreshService:
6
+ def __init__(self, engine: ContextEngine, snapshots: SnapshotStore):
7
+ self.engine, self.snapshots = engine, snapshots
8
+
9
+ def execute(self, workspace_id: str, paths: list[str] | None = None) -> dict:
10
+ candidate_id = self.snapshots.begin_candidate(workspace_id)
11
+ try:
12
+ candidate, result = self.engine.build_workspace_snapshot(workspace_id, paths)
13
+ self.engine.prepare_bundle(workspace_id, _state=candidate, _record=True)
14
+ self.snapshots.validate_candidate(candidate_id, self.engine.export_state(workspace_id, candidate))
15
+ self.snapshots.promote_candidate(candidate_id)
16
+ self.engine.activate_snapshot(workspace_id, candidate)
17
+ return result
18
+ except Exception as error:
19
+ self.snapshots.fail_candidate(candidate_id, error)
20
+ raise
21
+
22
+ def invalidate(self, workspace_id: str, target: str) -> dict:
23
+ result = self.engine.invalidate(workspace_id, target)
24
+ self.snapshots.promote(self.engine.export_snapshot(workspace_id))
25
+ return result
@@ -0,0 +1,22 @@
1
+ class RetrievalService:
2
+ def __init__(self, engine, ecc=None):
3
+ self.engine, self.ecc = engine, ecc
4
+
5
+ def plan(self, workspace_id, query, available_input_tokens, strategy_override=""):
6
+ with self.engine.snapshot_pin(workspace_id) as pin:
7
+ return self.engine.plan_context(workspace_id, query, available_input_tokens, strategy_override, pin.state)
8
+
9
+ def retrieve(self, workspace_id, query, top_k=12, graph_expand=True, token_budget=4_000):
10
+ with self.engine.snapshot_pin(workspace_id) as pin:
11
+ result = self.engine.retrieve_context(workspace_id, query, top_k, graph_expand, token_budget, pin.state)
12
+ result["snapshot_id"] = pin.snapshot_id
13
+ result["snapshot_version"] = pin.snapshot_version
14
+ return result
15
+
16
+ def stats(self, workspace_id):
17
+ result = self.engine.stats(workspace_id)
18
+ result["adapters"] = {"ecc": self.ecc.status() if self.ecc else {"enabled": False, "available": False, "read_only": True}}
19
+ return result
20
+
21
+ def snapshot_is_current(self, workspace_id):
22
+ return self.engine.snapshot_is_current(workspace_id)
@@ -0,0 +1,4 @@
1
+ from harness_context.application.context_service import ContextService
2
+ from harness_context.application.refresh_service import RefreshService
3
+
4
+ __all__ = ["ContextService", "RefreshService"]
@@ -0,0 +1,18 @@
1
+ from pathlib import Path
2
+
3
+
4
+ class WorkspaceService:
5
+ def __init__(self, engine, refresh, default_workspace_id):
6
+ self.engine, self.refresh, self.default_workspace_id = engine, refresh, default_workspace_id
7
+
8
+ def register(self, workspace_id, roots, initial_refresh=False):
9
+ if workspace_id != self.default_workspace_id:
10
+ raise ValueError("one runtime serves exactly one workspace")
11
+ policy = self.engine.registry.get(self.default_workspace_id)
12
+ requested = tuple(sorted(str(Path(root).resolve()) for root in roots))
13
+ if requested != policy.roots:
14
+ raise ValueError("workspace roots cannot change after runtime startup")
15
+ result = {"workspace_id": workspace_id, "roots": list(policy.roots), "status": self.engine.stats(workspace_id)["status"]}
16
+ if initial_refresh:
17
+ result["refresh"] = self.refresh.execute(workspace_id)
18
+ return result
@@ -0,0 +1,47 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from compact.handoff import ConversationHandoffStore
6
+ from harness_context.adapters.ecc import EccMemoryReader, detect_ecc
7
+ from harness_context.application.container import ApplicationContainer
8
+ from harness_context.application.ecc_service import EccService
9
+ from harness_context.application.handoff_service import HandoffService
10
+ from harness_context.application.memory_service import MemoryService
11
+ from harness_context.application.retrieval_service import RetrievalService
12
+ from harness_context.application.services import ContextService, RefreshService
13
+ from harness_context.application.workspace_service import WorkspaceService
14
+ from harness_context.engine import ContextEngine
15
+ from harness_context.paths import workspace_state_dir
16
+ from harness_context.storage import SnapshotStore
17
+ from harness_context.workspace.identity import workspace_identity
18
+ from memory.episodic import MemoryStore
19
+
20
+
21
+ def build_container(root: str | Path, workspace_id: str = "", *, ecc_enabled: bool = False, ecc_allow_user_scope: bool = False) -> ApplicationContainer:
22
+ canonical = Path(root).expanduser().resolve(strict=True)
23
+ identity = workspace_id or workspace_identity(canonical)
24
+ engine = ContextEngine()
25
+ engine.register_workspace(identity, [str(canonical)])
26
+ snapshots = SnapshotStore(canonical)
27
+ snapshots.prepare()
28
+ active = snapshots.load_active(identity)
29
+ if active:
30
+ engine.load_snapshot(active)
31
+ data_dir = workspace_state_dir(canonical)
32
+ memory = MemoryStore(str(data_dir / "memory.sqlite3"))
33
+ handoffs = ConversationHandoffStore(str(data_dir / "handoffs.sqlite3"))
34
+ ecc = EccMemoryReader(detect_ecc(canonical, allow_user_scope=ecc_allow_user_scope)) if ecc_enabled else None
35
+ refresh = RefreshService(engine, snapshots)
36
+ return ApplicationContainer(
37
+ engine=engine, snapshots=snapshots,
38
+ refresh=refresh,
39
+ context=ContextService(engine, memory, handoffs, ecc),
40
+ memory=memory, handoffs=handoffs,
41
+ ecc=ecc,
42
+ workspace=WorkspaceService(engine, refresh, identity),
43
+ retrieval=RetrievalService(engine, ecc),
44
+ memories=MemoryService(memory),
45
+ handoff=HandoffService(handoffs),
46
+ ecc_queries=EccService(ecc),
47
+ )
@@ -0,0 +1,16 @@
1
+ """Public CTXORA product metadata."""
2
+
3
+ BRAND_NAME = "CTXORA"
4
+ PRODUCT_NAME = "CTXORA Engine"
5
+ MCP_SERVER_NAME = "CTXORA MCP"
6
+ CLI_NAME = "ctxora"
7
+ MCP_CLI_NAME = "ctxora-mcp"
8
+ DISTRIBUTION_NAME = "ctxora-engine"
9
+ MCP_SERVER_KEY = "ctxora"
10
+ FREE_PLAN_NAME = "CTXORA Free"
11
+ PAID_PLAN_NAME = "CTXORA Pro"
12
+ PAID_PLAN_STATUS = "waitlist"
13
+ TAGLINE = "Index once. Ground every agent."
14
+ DESCRIPTION = "Local-first context engine for coding agents."
15
+ WORKSPACE_DIR_NAME = ".ctxora"
16
+ LEGACY_WORKSPACE_DIR_NAME = ".harness"
@@ -0,0 +1 @@
1
+ """Command-line transport."""
@@ -0,0 +1,239 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import os
6
+ import shutil
7
+ import signal
8
+ import subprocess
9
+ import sys
10
+ from dataclasses import asdict
11
+ from pathlib import Path
12
+
13
+ from harness_context.adapters.clients import PROFILES
14
+ from harness_context.api.v2 import PrepareContextRequest
15
+ from harness_context.branding import CLI_NAME, PAID_PLAN_NAME, PAID_PLAN_STATUS
16
+ from harness_context.cli.exit_codes import error_report, exit_code_for
17
+ from harness_context.free_tools import (
18
+ context_score,
19
+ explain_context,
20
+ instruction_document,
21
+ repository_map,
22
+ write_instruction,
23
+ )
24
+ from harness_context.installer import ClientInstaller
25
+ from harness_context.paths import workspace_data_dir, workspace_data_dirs, workspace_runtime_dir
26
+ from harness_context.runtime import HarnessRuntime
27
+
28
+
29
+ def _runtime(args) -> HarnessRuntime:
30
+ if args.host and args.host not in {"127.0.0.1", "localhost", "::1"} and not args.allow_external:
31
+ raise SystemExit("external HTTP binding requires --allow-external")
32
+ return HarnessRuntime.for_workspace(
33
+ args.workspace, args.transport, args.host, args.port, args.watch,
34
+ args.ecc, args.ecc_user_scope,
35
+ )
36
+
37
+
38
+ def _print(payload: dict) -> None:
39
+ print(json.dumps(payload, ensure_ascii=False, indent=2))
40
+
41
+
42
+ def _pid_path(workspace: str) -> Path:
43
+ return workspace_runtime_dir(workspace) / "run.pid"
44
+
45
+
46
+ def build_parser() -> argparse.ArgumentParser:
47
+ parser = argparse.ArgumentParser(prog=CLI_NAME)
48
+ commands = parser.add_subparsers(dest="command", required=True)
49
+ for name in (
50
+ "setup", "install", "profile", "run", "start", "index", "query", "explain",
51
+ "context-score", "repo-map", "generate-agents-md",
52
+ "generate-copilot-instructions", "generate-cursor-rules", "pro", "inspect",
53
+ "status", "stop", "doctor", "repair", "register", "export", "uninstall", "ci",
54
+ ):
55
+ command = commands.add_parser(name)
56
+ command.add_argument("--workspace", default=".")
57
+ command.add_argument("--transport", choices=("stdio", "streamable-http"))
58
+ command.add_argument("--host")
59
+ command.add_argument("--port", type=int)
60
+ command.add_argument("--allow-external", action="store_true")
61
+ command.add_argument("--watch", action="store_true", default=None)
62
+ command.add_argument("--ecc", action="store_true", default=None)
63
+ command.add_argument("--ecc-user-scope", action="store_true", default=None)
64
+ if name in {"query", "explain"}:
65
+ command.add_argument("query")
66
+ command.add_argument("--tokens", type=int, default=8_000)
67
+ if name in {"generate-agents-md", "generate-copilot-instructions", "generate-cursor-rules"}:
68
+ command.add_argument("--output")
69
+ command.add_argument("--force", action="store_true")
70
+ if name == "inspect":
71
+ command.add_argument("target", choices=("workspace", "snapshot", "bundle", "ecc"), default="workspace", nargs="?")
72
+ if name == "index":
73
+ command.add_argument("--incremental", action="store_true")
74
+ if name == "ci":
75
+ command.add_argument("--base", default="origin/main")
76
+ command.add_argument("--head", default="HEAD")
77
+ if name == "export":
78
+ command.add_argument("--output", required=True)
79
+ if name in {"install", "uninstall"}:
80
+ command.add_argument("--profile", choices=tuple(PROFILES))
81
+ command.add_argument("--client-config")
82
+ command.add_argument("--dry-run", action="store_true")
83
+ if name == "uninstall":
84
+ command.add_argument("--delete-data", action="store_true")
85
+ return parser
86
+
87
+
88
+ def _main(argv: list[str] | None = None) -> int:
89
+ args = build_parser().parse_args(argv)
90
+ root = Path(args.workspace).expanduser().resolve(strict=True)
91
+ if args.command == "setup":
92
+ config = workspace_data_dir(root) / "config.toml"
93
+ config.parent.mkdir(parents=True, exist_ok=True)
94
+ if not config.exists():
95
+ config.write_text('transport = "stdio"\nstartup_policy = "block_until_ready"\nwatch = false\necc_enabled = false\necc_allow_user_scope = false\n', "utf-8")
96
+ _print({"status": "configured", "config": str(config)})
97
+ return 0
98
+ if args.command == "profile":
99
+ _print({"profiles": [asdict(profile) | {"default_config": str(profile.default_config)} for profile in PROFILES.values()]})
100
+ return 0
101
+ if args.command == "pro":
102
+ _print({
103
+ "plan": PAID_PLAN_NAME,
104
+ "status": PAID_PLAN_STATUS,
105
+ "features": ["managed automation", "private workflows", "team context"],
106
+ })
107
+ return 0
108
+ if args.command == "install":
109
+ if not args.profile:
110
+ raise SystemExit("install requires --profile")
111
+ plan = ClientInstaller(root).install(args.profile, args.client_config, args.dry_run)
112
+ _print({"status": "planned" if args.dry_run else "installed", "plan": plan.to_dict()})
113
+ return 0
114
+ if args.command == "register":
115
+ runtime = _runtime(args)
116
+ _print({"workspace_id": runtime.config.workspace_id, "root": str(root), "status": "registered"})
117
+ return 0
118
+ if args.command == "uninstall":
119
+ if args.profile:
120
+ plan = ClientInstaller(root).uninstall(args.profile, args.client_config, args.dry_run, args.delete_data)
121
+ _print({"status": "planned" if args.dry_run else "uninstalled", "plan": plan.to_dict(), "data_deleted": args.delete_data})
122
+ return 0
123
+ if args.delete_data:
124
+ for owned in workspace_data_dirs(root):
125
+ shutil.rmtree(owned, ignore_errors=True)
126
+ _print({"status": "uninstalled", "data_deleted": args.delete_data})
127
+ return 0
128
+ if args.command == "start":
129
+ if args.host and args.host not in {"127.0.0.1", "localhost", "::1"} and not args.allow_external:
130
+ raise SystemExit("external HTTP binding requires --allow-external")
131
+ pid_path = _pid_path(str(root))
132
+ pid_path.parent.mkdir(parents=True, exist_ok=True)
133
+ log = (pid_path.parent / "runtime.log").open("a", encoding="utf-8")
134
+ command = [
135
+ sys.executable, "-m", "harness_context.cli.app", "run",
136
+ "--workspace", str(root), "--transport", "streamable-http",
137
+ "--host", args.host or "127.0.0.1", "--port", str(args.port or 8765),
138
+ "--watch",
139
+ ]
140
+ if args.ecc:
141
+ command.append("--ecc")
142
+ if args.ecc_user_scope:
143
+ command.append("--ecc-user-scope")
144
+ process = subprocess.Popen(command, stdout=log, stderr=log, start_new_session=True)
145
+ pid_path.write_text(str(process.pid), "utf-8")
146
+ _print({"status": "started", "pid": process.pid})
147
+ return 0
148
+ if args.command in {"status", "stop"}:
149
+ pid_path = _pid_path(str(root))
150
+ pid = int(pid_path.read_text("utf-8")) if pid_path.exists() else 0
151
+ running = False
152
+ if pid:
153
+ try:
154
+ os.kill(pid, 0)
155
+ running = True
156
+ except OSError:
157
+ pass
158
+ if args.command == "stop" and running:
159
+ os.kill(pid, signal.SIGTERM)
160
+ pid_path.unlink(missing_ok=True)
161
+ running = False
162
+ _print({"status": "running" if running else "stopped", "pid": pid or None})
163
+ return 0
164
+ runtime = _runtime(args)
165
+ startup = runtime.startup()
166
+ if args.command == "run":
167
+ runtime.run()
168
+ return 0
169
+ if args.command == "index":
170
+ _print(startup)
171
+ return 0
172
+ if args.command == "repair":
173
+ repaired = runtime.container.refresh.execute(runtime.config.workspace_id)
174
+ _print({"status": "repaired", "workspace": str(root), "runtime": repaired})
175
+ return 0
176
+ if args.command == "doctor":
177
+ _print(runtime.health_report())
178
+ return 0
179
+ if args.command == "ci":
180
+ changed = subprocess.run(
181
+ ["git", "diff", "--name-only", args.base, args.head],
182
+ cwd=root, check=True, capture_output=True, text=True,
183
+ ).stdout.splitlines()
184
+ paths = [str(root / path) for path in changed]
185
+ result = runtime.container.refresh.execute(runtime.config.workspace_id, paths) if paths else startup
186
+ _print({"status": "ok", "changed_files": changed, "refresh": result})
187
+ return 0
188
+ assert runtime.container is not None
189
+ if args.command in {"query", "explain"}:
190
+ request = PrepareContextRequest(runtime.config.workspace_id, args.query, args.tokens)
191
+ package = runtime.container.context.prepare(request).to_dict()
192
+ _print(package if args.command == "query" else explain_context(args.query, package, root))
193
+ return 0
194
+ snapshot = runtime.container.engine.export_snapshot(runtime.config.workspace_id)
195
+ if args.command == "context-score":
196
+ _print(context_score(snapshot, root))
197
+ return 0
198
+ if args.command == "repo-map":
199
+ _print(repository_map(snapshot, root))
200
+ return 0
201
+ generators = {
202
+ "generate-agents-md": (root / "AGENTS.md", "AGENTS.md"),
203
+ "generate-copilot-instructions": (root / ".github" / "copilot-instructions.md", "GitHub Copilot"),
204
+ "generate-cursor-rules": (root / ".cursor" / "rules" / "ctxora.mdc", "Cursor"),
205
+ }
206
+ if args.command in generators:
207
+ default_output, target = generators[args.command]
208
+ output = Path(args.output).expanduser().resolve() if args.output else default_output
209
+ write_instruction(output, instruction_document(snapshot, root, target), args.force)
210
+ _print({"status": "generated", "target": target, "output": str(output), "snapshot_id": snapshot["snapshot_id"]})
211
+ return 0
212
+ stats = runtime.container.engine.stats(runtime.config.workspace_id)
213
+ if args.command == "export":
214
+ output = Path(args.output).expanduser().resolve()
215
+ output.parent.mkdir(parents=True, exist_ok=True)
216
+ output.write_text(json.dumps(runtime.container.engine.export_snapshot(runtime.config.workspace_id), ensure_ascii=False, indent=2), "utf-8")
217
+ _print({"status": "exported", "output": str(output), "snapshot_id": stats["snapshot_id"]})
218
+ return 0
219
+ if args.target == "bundle":
220
+ _print(runtime.container.engine.prepare_bundle(runtime.config.workspace_id))
221
+ elif args.target == "ecc":
222
+ _print(runtime.container.ecc.status() if runtime.container.ecc else {"available": False, "enabled": False, "read_only": True})
223
+ else:
224
+ _print(stats)
225
+ return 0
226
+
227
+
228
+ def main(argv: list[str] | None = None) -> int:
229
+ try:
230
+ return _main(argv)
231
+ except SystemExit:
232
+ raise
233
+ except Exception as error: # noqa: BLE001 - CLI boundary maps all failures to stable exits.
234
+ print(json.dumps(error_report(error), sort_keys=True), file=sys.stderr)
235
+ return int(exit_code_for(error))
236
+
237
+
238
+ if __name__ == "__main__":
239
+ raise SystemExit(main())
@@ -0,0 +1,25 @@
1
+ from enum import IntEnum
2
+
3
+
4
+ class ExitCode(IntEnum):
5
+ OK = 0
6
+ USAGE = 2
7
+ CONFIGURATION = 3
8
+ NOT_READY = 4
9
+ TEMPORARY_FAILURE = 5
10
+ INTERNAL_ERROR = 70
11
+
12
+
13
+ def exit_code_for(error: BaseException) -> ExitCode:
14
+ if isinstance(error, (FileNotFoundError, PermissionError, ValueError)):
15
+ return ExitCode.CONFIGURATION
16
+ if getattr(error, "code", "") in {"workspace_not_ready", "stale_snapshot"}:
17
+ return ExitCode.NOT_READY
18
+ if isinstance(error, (TimeoutError, ConnectionError)):
19
+ return ExitCode.TEMPORARY_FAILURE
20
+ return ExitCode.INTERNAL_ERROR
21
+
22
+
23
+ def error_report(error: BaseException) -> dict[str, object]:
24
+ code = exit_code_for(error)
25
+ return {"status": "error", "exit_code": int(code), "error": code.name.lower()}
@@ -0,0 +1,9 @@
1
+ from harness_context.domain.cag import CAGStore
2
+ from harness_context.domain.chunking import bounded_windows, stable_chunk_id
3
+ from harness_context.domain.planning import ContextPlanner
4
+ from harness_context.domain.retrieval import CoveragePolicy, GraphExpander, RankFusion, Selector
5
+
6
+ __all__ = [
7
+ "CAGStore", "ContextPlanner", "CoveragePolicy", "GraphExpander",
8
+ "RankFusion", "Selector", "bounded_windows", "stable_chunk_id",
9
+ ]
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import json
5
+ import time
6
+ from collections.abc import Sequence
7
+
8
+ from harness_context.domain.planning import ContextPlanner
9
+ from harness_context.schemas import ContextItem
10
+
11
+
12
+ class CAGStore:
13
+ def build(self, workspace_id: str, items: Sequence[ContextItem], version: int, ttl_seconds: int) -> dict:
14
+ stable = [item for item in items if ContextPlanner.is_stable(item)]
15
+ sources = [{"path": item.path, "content_hash": item.content_hash, "start_line": item.start_line, "end_line": item.end_line} for item in sorted(stable, key=lambda item: (item.path, item.start_line))]
16
+ bundle_id = "sha256:" + hashlib.sha256(json.dumps(sources, sort_keys=True).encode()).hexdigest()
17
+ now = time.time()
18
+ return {"bundle_id": bundle_id, "workspace_id": workspace_id, "version": version, "bundle_type": "project_core", "model_family": "provider-neutral", "token_count": sum(item.tokens for item in stable), "created_at": now, "expires_at": now + ttl_seconds, "sources": sources, "items": [item.to_dict() for item in stable]}
@@ -0,0 +1,17 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ from collections.abc import Iterator
5
+ from pathlib import Path
6
+
7
+
8
+ def stable_chunk_id(workspace_id: str, path: Path, kind: str, symbol: str, start: int, end: int, content_hash: str) -> str:
9
+ identity = f"{workspace_id}\0{path}\0{kind}\0{symbol}\0{start}\0{end}\0{content_hash}"
10
+ return hashlib.sha256(identity.encode()).hexdigest()
11
+
12
+
13
+ def bounded_windows(lines: list[str], window: int = 160, overlap: int = 20) -> Iterator[tuple[int, int, str]]:
14
+ if window <= 0 or overlap < 0 or overlap >= window:
15
+ raise ValueError("window must be positive and overlap smaller than window")
16
+ for offset in range(0, max(1, len(lines)), window - overlap):
17
+ yield offset + 1, min(offset + window, len(lines)), "\n".join(lines[offset:offset + window])
@@ -0,0 +1,30 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Sequence
4
+ from pathlib import Path
5
+
6
+ from harness_context.schemas import ContextItem
7
+
8
+
9
+ class ContextPlanner:
10
+ STABLE_FILES = {"AGENTS.md", "CLAUDE.md", "README.md", "pyproject.toml"}
11
+
12
+ @classmethod
13
+ def is_stable(cls, item: ContextItem) -> bool:
14
+ return Path(item.path).name in cls.STABLE_FILES or "/docs/" in item.path
15
+
16
+ def plan(self, workspace_id: str, query: str, available: int, items: Sequence[ContextItem], override: str = "") -> dict:
17
+ corpus = sum(item.tokens for item in items)
18
+ stable = sum(item.tokens for item in items if self.is_stable(item))
19
+ repo_wide = any(word in query.casefold() for word in ("architecture", "kiến trúc", "repo", "toàn bộ", "cross-module"))
20
+ if override:
21
+ strategy, reason, confidence = override, "Caller override requested.", 1.0
22
+ elif corpus <= available * 0.7:
23
+ strategy, reason, confidence = "long_context", "Authorized corpus fits comfortably in the supplied budget.", 0.84
24
+ elif stable and repo_wide:
25
+ strategy, reason, confidence = "hybrid_cag_rag", "Reusable stable core plus fresh cross-file evidence is required.", 0.88
26
+ elif repo_wide:
27
+ strategy, reason, confidence = "graph_augmented", "Cross-module query benefits from parsed dependency expansion.", 0.78
28
+ else:
29
+ strategy, reason, confidence = "hybrid_rag", "Narrow task over a larger changing corpus.", 0.82
30
+ return {"workspace_id": workspace_id, "strategy": strategy, "reason": reason, "confidence": confidence, "estimated_context_tokens": min(corpus, available), "rag_budget_tokens": min(5000, available), "graph_expand": strategy in {"hybrid_cag_rag", "graph_augmented"}, "alternatives": ["hybrid_rag"] if strategy != "hybrid_rag" else ["long_context"], "override_allowed": True}
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Sequence
4
+ from pathlib import Path
5
+ from typing import Protocol
6
+
7
+ from harness_context.schemas import ContextItem
8
+
9
+
10
+ class FileScanner(Protocol):
11
+ def scan(self, workspace_id: str, requested: Sequence[str]) -> list[Path]: ...
12
+
13
+
14
+ class ChunkParser(Protocol):
15
+ def parse(self, workspace_id: str, path: Path) -> list[ContextItem]: ...
16
+
17
+
18
+ class SearchIndex(Protocol):
19
+ def rebuild(self, items: Sequence[ContextItem]) -> None: ...
20
+ def scores(self, query: str, items: Sequence[ContextItem]) -> list[float]: ...
21
+
22
+
23
+ class DependencyGraph(Protocol):
24
+ def build(self, items: Sequence[ContextItem]) -> dict[str, set[str]]: ...