llmcode-cli 1.0.0__py3-none-any.whl

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 (212) hide show
  1. llm_code/__init__.py +2 -0
  2. llm_code/analysis/__init__.py +6 -0
  3. llm_code/analysis/cache.py +33 -0
  4. llm_code/analysis/engine.py +256 -0
  5. llm_code/analysis/go_rules.py +114 -0
  6. llm_code/analysis/js_rules.py +84 -0
  7. llm_code/analysis/python_rules.py +311 -0
  8. llm_code/analysis/rules.py +140 -0
  9. llm_code/analysis/rust_rules.py +108 -0
  10. llm_code/analysis/universal_rules.py +111 -0
  11. llm_code/api/__init__.py +0 -0
  12. llm_code/api/client.py +90 -0
  13. llm_code/api/errors.py +73 -0
  14. llm_code/api/openai_compat.py +390 -0
  15. llm_code/api/provider.py +35 -0
  16. llm_code/api/sse.py +52 -0
  17. llm_code/api/types.py +140 -0
  18. llm_code/cli/__init__.py +0 -0
  19. llm_code/cli/commands.py +70 -0
  20. llm_code/cli/image.py +122 -0
  21. llm_code/cli/render.py +214 -0
  22. llm_code/cli/status_line.py +79 -0
  23. llm_code/cli/streaming.py +92 -0
  24. llm_code/cli/tui_main.py +220 -0
  25. llm_code/computer_use/__init__.py +11 -0
  26. llm_code/computer_use/app_detect.py +49 -0
  27. llm_code/computer_use/app_tier.py +57 -0
  28. llm_code/computer_use/coordinator.py +99 -0
  29. llm_code/computer_use/input_control.py +71 -0
  30. llm_code/computer_use/screenshot.py +93 -0
  31. llm_code/cron/__init__.py +13 -0
  32. llm_code/cron/parser.py +145 -0
  33. llm_code/cron/scheduler.py +135 -0
  34. llm_code/cron/storage.py +126 -0
  35. llm_code/enterprise/__init__.py +1 -0
  36. llm_code/enterprise/audit.py +59 -0
  37. llm_code/enterprise/auth.py +26 -0
  38. llm_code/enterprise/oidc.py +95 -0
  39. llm_code/enterprise/rbac.py +65 -0
  40. llm_code/harness/__init__.py +5 -0
  41. llm_code/harness/config.py +33 -0
  42. llm_code/harness/engine.py +129 -0
  43. llm_code/harness/guides.py +41 -0
  44. llm_code/harness/sensors.py +68 -0
  45. llm_code/harness/templates.py +84 -0
  46. llm_code/hida/__init__.py +1 -0
  47. llm_code/hida/classifier.py +187 -0
  48. llm_code/hida/engine.py +49 -0
  49. llm_code/hida/profiles.py +95 -0
  50. llm_code/hida/types.py +28 -0
  51. llm_code/ide/__init__.py +1 -0
  52. llm_code/ide/bridge.py +80 -0
  53. llm_code/ide/detector.py +76 -0
  54. llm_code/ide/server.py +169 -0
  55. llm_code/logging.py +29 -0
  56. llm_code/lsp/__init__.py +0 -0
  57. llm_code/lsp/client.py +298 -0
  58. llm_code/lsp/detector.py +42 -0
  59. llm_code/lsp/manager.py +56 -0
  60. llm_code/lsp/tools.py +288 -0
  61. llm_code/marketplace/__init__.py +0 -0
  62. llm_code/marketplace/builtin_registry.py +102 -0
  63. llm_code/marketplace/installer.py +162 -0
  64. llm_code/marketplace/plugin.py +78 -0
  65. llm_code/marketplace/registry.py +360 -0
  66. llm_code/mcp/__init__.py +0 -0
  67. llm_code/mcp/bridge.py +87 -0
  68. llm_code/mcp/client.py +117 -0
  69. llm_code/mcp/health.py +120 -0
  70. llm_code/mcp/manager.py +214 -0
  71. llm_code/mcp/oauth.py +219 -0
  72. llm_code/mcp/transport.py +254 -0
  73. llm_code/mcp/types.py +53 -0
  74. llm_code/remote/__init__.py +0 -0
  75. llm_code/remote/client.py +136 -0
  76. llm_code/remote/protocol.py +22 -0
  77. llm_code/remote/server.py +275 -0
  78. llm_code/remote/ssh_proxy.py +56 -0
  79. llm_code/runtime/__init__.py +0 -0
  80. llm_code/runtime/auto_commit.py +56 -0
  81. llm_code/runtime/auto_diagnose.py +62 -0
  82. llm_code/runtime/checkpoint.py +70 -0
  83. llm_code/runtime/checkpoint_recovery.py +142 -0
  84. llm_code/runtime/compaction.py +35 -0
  85. llm_code/runtime/compressor.py +415 -0
  86. llm_code/runtime/config.py +533 -0
  87. llm_code/runtime/context.py +49 -0
  88. llm_code/runtime/conversation.py +921 -0
  89. llm_code/runtime/cost_tracker.py +126 -0
  90. llm_code/runtime/dream.py +127 -0
  91. llm_code/runtime/file_protection.py +150 -0
  92. llm_code/runtime/hardware.py +85 -0
  93. llm_code/runtime/hooks.py +223 -0
  94. llm_code/runtime/indexer.py +230 -0
  95. llm_code/runtime/knowledge_compiler.py +232 -0
  96. llm_code/runtime/memory.py +132 -0
  97. llm_code/runtime/memory_layers.py +467 -0
  98. llm_code/runtime/memory_lint.py +252 -0
  99. llm_code/runtime/model_aliases.py +37 -0
  100. llm_code/runtime/ollama.py +93 -0
  101. llm_code/runtime/overlay.py +124 -0
  102. llm_code/runtime/permissions.py +200 -0
  103. llm_code/runtime/plan.py +45 -0
  104. llm_code/runtime/prompt.py +238 -0
  105. llm_code/runtime/repo_map.py +174 -0
  106. llm_code/runtime/sandbox.py +116 -0
  107. llm_code/runtime/session.py +268 -0
  108. llm_code/runtime/skill_resolver.py +61 -0
  109. llm_code/runtime/skills.py +133 -0
  110. llm_code/runtime/speculative.py +75 -0
  111. llm_code/runtime/streaming_executor.py +216 -0
  112. llm_code/runtime/telemetry.py +196 -0
  113. llm_code/runtime/token_budget.py +26 -0
  114. llm_code/runtime/vcr.py +142 -0
  115. llm_code/runtime/vision.py +102 -0
  116. llm_code/swarm/__init__.py +1 -0
  117. llm_code/swarm/backend_subprocess.py +108 -0
  118. llm_code/swarm/backend_tmux.py +103 -0
  119. llm_code/swarm/backend_worktree.py +306 -0
  120. llm_code/swarm/checkpoint.py +74 -0
  121. llm_code/swarm/coordinator.py +236 -0
  122. llm_code/swarm/mailbox.py +88 -0
  123. llm_code/swarm/manager.py +202 -0
  124. llm_code/swarm/memory_sync.py +80 -0
  125. llm_code/swarm/recovery.py +21 -0
  126. llm_code/swarm/team.py +67 -0
  127. llm_code/swarm/types.py +31 -0
  128. llm_code/task/__init__.py +16 -0
  129. llm_code/task/diagnostics.py +93 -0
  130. llm_code/task/manager.py +162 -0
  131. llm_code/task/types.py +112 -0
  132. llm_code/task/verifier.py +104 -0
  133. llm_code/tools/__init__.py +0 -0
  134. llm_code/tools/agent.py +145 -0
  135. llm_code/tools/agent_roles.py +82 -0
  136. llm_code/tools/base.py +94 -0
  137. llm_code/tools/bash.py +565 -0
  138. llm_code/tools/computer_use_tools.py +278 -0
  139. llm_code/tools/coordinator_tool.py +75 -0
  140. llm_code/tools/cron_create.py +90 -0
  141. llm_code/tools/cron_delete.py +49 -0
  142. llm_code/tools/cron_list.py +51 -0
  143. llm_code/tools/deferred.py +92 -0
  144. llm_code/tools/dump.py +116 -0
  145. llm_code/tools/edit_file.py +282 -0
  146. llm_code/tools/git_tools.py +531 -0
  147. llm_code/tools/glob_search.py +112 -0
  148. llm_code/tools/grep_search.py +144 -0
  149. llm_code/tools/ide_diagnostics.py +59 -0
  150. llm_code/tools/ide_open.py +58 -0
  151. llm_code/tools/ide_selection.py +52 -0
  152. llm_code/tools/memory_tools.py +138 -0
  153. llm_code/tools/multi_edit.py +143 -0
  154. llm_code/tools/notebook_edit.py +107 -0
  155. llm_code/tools/notebook_read.py +81 -0
  156. llm_code/tools/parsing.py +63 -0
  157. llm_code/tools/read_file.py +154 -0
  158. llm_code/tools/registry.py +58 -0
  159. llm_code/tools/search_backends/__init__.py +56 -0
  160. llm_code/tools/search_backends/brave.py +56 -0
  161. llm_code/tools/search_backends/duckduckgo.py +129 -0
  162. llm_code/tools/search_backends/searxng.py +71 -0
  163. llm_code/tools/search_backends/tavily.py +73 -0
  164. llm_code/tools/swarm_create.py +109 -0
  165. llm_code/tools/swarm_delete.py +95 -0
  166. llm_code/tools/swarm_list.py +44 -0
  167. llm_code/tools/swarm_message.py +109 -0
  168. llm_code/tools/task_close.py +79 -0
  169. llm_code/tools/task_plan.py +79 -0
  170. llm_code/tools/task_verify.py +90 -0
  171. llm_code/tools/tool_search.py +65 -0
  172. llm_code/tools/web_common.py +258 -0
  173. llm_code/tools/web_fetch.py +223 -0
  174. llm_code/tools/web_search.py +280 -0
  175. llm_code/tools/write_file.py +118 -0
  176. llm_code/tui/__init__.py +1 -0
  177. llm_code/tui/app.py +2432 -0
  178. llm_code/tui/chat_view.py +82 -0
  179. llm_code/tui/chat_widgets.py +309 -0
  180. llm_code/tui/header_bar.py +46 -0
  181. llm_code/tui/input_bar.py +349 -0
  182. llm_code/tui/keybindings.py +142 -0
  183. llm_code/tui/marketplace.py +210 -0
  184. llm_code/tui/status_bar.py +72 -0
  185. llm_code/tui/theme.py +96 -0
  186. llm_code/utils/__init__.py +0 -0
  187. llm_code/utils/diff.py +111 -0
  188. llm_code/utils/errors.py +70 -0
  189. llm_code/utils/hyperlink.py +73 -0
  190. llm_code/utils/notebook.py +179 -0
  191. llm_code/utils/search.py +69 -0
  192. llm_code/utils/text_normalize.py +28 -0
  193. llm_code/utils/version_check.py +62 -0
  194. llm_code/vim/__init__.py +4 -0
  195. llm_code/vim/engine.py +51 -0
  196. llm_code/vim/motions.py +172 -0
  197. llm_code/vim/operators.py +183 -0
  198. llm_code/vim/text_objects.py +139 -0
  199. llm_code/vim/transitions.py +279 -0
  200. llm_code/vim/types.py +68 -0
  201. llm_code/voice/__init__.py +1 -0
  202. llm_code/voice/languages.py +43 -0
  203. llm_code/voice/recorder.py +136 -0
  204. llm_code/voice/stt.py +36 -0
  205. llm_code/voice/stt_anthropic.py +66 -0
  206. llm_code/voice/stt_google.py +32 -0
  207. llm_code/voice/stt_whisper.py +52 -0
  208. llmcode_cli-1.0.0.dist-info/METADATA +524 -0
  209. llmcode_cli-1.0.0.dist-info/RECORD +212 -0
  210. llmcode_cli-1.0.0.dist-info/WHEEL +4 -0
  211. llmcode_cli-1.0.0.dist-info/entry_points.txt +2 -0
  212. llmcode_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,33 @@
1
+ """Harness configuration types."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class HarnessControl:
9
+ """A single harness control (guide or sensor)."""
10
+
11
+ name: str
12
+ category: str # "guide" | "sensor"
13
+ kind: str # "computational" | "inferential"
14
+ enabled: bool = True
15
+ trigger: str = "post_tool" # "pre_tool" | "post_tool" | "pre_turn" | "post_turn" | "on_demand"
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class HarnessFinding:
20
+ """A finding reported by a sensor after tool execution."""
21
+
22
+ sensor: str
23
+ message: str
24
+ file_path: str = ""
25
+ severity: str = "info" # "error" | "warning" | "info"
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class HarnessConfig:
30
+ """Configuration for the Harness Engine."""
31
+
32
+ template: str = "auto"
33
+ controls: tuple[HarnessControl, ...] = ()
@@ -0,0 +1,129 @@
1
+ """HarnessEngine — orchestrates guides (feedforward) and sensors (feedback)."""
2
+ from __future__ import annotations
3
+
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from llm_code.harness.config import HarnessConfig, HarnessControl, HarnessFinding
8
+ from llm_code.harness.guides import (
9
+ analysis_context_guide,
10
+ plan_mode_denied_tools,
11
+ repo_map_guide,
12
+ )
13
+ from llm_code.harness.sensors import (
14
+ auto_commit_sensor,
15
+ code_rules_sensor,
16
+ lsp_diagnose_sensor,
17
+ )
18
+
19
+ _WRITE_TOOLS = frozenset({"write_file", "edit_file"})
20
+
21
+
22
+ class HarnessEngine:
23
+ """Central orchestrator for all quality controls."""
24
+
25
+ def __init__(self, config: HarnessConfig, cwd: Path) -> None:
26
+ self._config = config
27
+ self._cwd = cwd
28
+ self._overrides: dict[str, bool] = {}
29
+ self.plan_mode: bool = False
30
+ self.analysis_context: str | None = None
31
+ self.lsp_manager: Any | None = None
32
+
33
+ @property
34
+ def config(self) -> HarnessConfig:
35
+ return self._config
36
+
37
+ def _is_enabled(self, ctrl: HarnessControl) -> bool:
38
+ if ctrl.name in self._overrides:
39
+ return self._overrides[ctrl.name]
40
+ return ctrl.enabled
41
+
42
+ def _controls_by(self, category: str, trigger: str) -> list[HarnessControl]:
43
+ return [
44
+ c for c in self._config.controls
45
+ if c.category == category and c.trigger == trigger and self._is_enabled(c)
46
+ ]
47
+
48
+ def pre_turn(self) -> list[str]:
49
+ """Run pre_turn guides. Returns strings to inject into system prompt."""
50
+ injections: list[str] = []
51
+ for ctrl in self._controls_by("guide", "pre_turn"):
52
+ text = self._run_guide(ctrl)
53
+ if text:
54
+ injections.append(text)
55
+ return injections
56
+
57
+ def check_pre_tool(self, tool_name: str) -> str | None:
58
+ """Check pre_tool guides (plan mode). Returns denial message or None."""
59
+ for ctrl in self._controls_by("guide", "pre_tool"):
60
+ if ctrl.name == "plan_mode":
61
+ denied = plan_mode_denied_tools(self.plan_mode)
62
+ if tool_name in denied:
63
+ return f"Plan mode: read-only. Tool '{tool_name}' denied. Use /plan to switch to Act mode."
64
+ return None
65
+
66
+ def _run_guide(self, ctrl: HarnessControl) -> str:
67
+ if ctrl.name == "repo_map":
68
+ return repo_map_guide(cwd=self._cwd)
69
+ if ctrl.name == "analysis_context":
70
+ return analysis_context_guide(context=self.analysis_context)
71
+ if ctrl.name == "architecture_doc":
72
+ doc_path = self._cwd / ".llm-code" / "architecture.md"
73
+ if doc_path.exists():
74
+ try:
75
+ return doc_path.read_text(encoding="utf-8")
76
+ except OSError:
77
+ return ""
78
+ return ""
79
+ if ctrl.name == "knowledge":
80
+ from llm_code.harness.guides import knowledge_guide
81
+ return knowledge_guide(cwd=self._cwd)
82
+ return ""
83
+
84
+ async def post_tool(
85
+ self, tool_name: str, file_path: str, is_error: bool,
86
+ ) -> list[HarnessFinding]:
87
+ """Run post_tool sensors. Returns findings for agent context."""
88
+ if is_error or tool_name not in _WRITE_TOOLS:
89
+ return []
90
+
91
+ findings: list[HarnessFinding] = []
92
+ for ctrl in self._controls_by("sensor", "post_tool"):
93
+ new_findings = await self._run_sensor(ctrl, tool_name, file_path)
94
+ findings.extend(new_findings)
95
+ return findings
96
+
97
+ async def _run_sensor(
98
+ self, ctrl: HarnessControl, tool_name: str, file_path: str
99
+ ) -> list[HarnessFinding]:
100
+ if ctrl.name == "lsp_diagnose":
101
+ return await lsp_diagnose_sensor(lsp_manager=self.lsp_manager, file_path=file_path)
102
+ if ctrl.name == "code_rules":
103
+ return code_rules_sensor(cwd=self._cwd, file_path=file_path)
104
+ if ctrl.name == "auto_commit":
105
+ finding = auto_commit_sensor(file_path=Path(file_path), tool_name=tool_name)
106
+ return [finding] if finding else []
107
+ return []
108
+
109
+ def enable(self, name: str) -> None:
110
+ self._overrides[name] = True
111
+
112
+ def disable(self, name: str) -> None:
113
+ self._overrides[name] = False
114
+
115
+ def status(self) -> dict:
116
+ guides = []
117
+ sensors = []
118
+ for ctrl in self._config.controls:
119
+ entry = {
120
+ "name": ctrl.name,
121
+ "trigger": ctrl.trigger,
122
+ "kind": ctrl.kind,
123
+ "enabled": self._is_enabled(ctrl),
124
+ }
125
+ if ctrl.category == "guide":
126
+ guides.append(entry)
127
+ else:
128
+ sensors.append(entry)
129
+ return {"template": self._config.template, "guides": guides, "sensors": sensors}
@@ -0,0 +1,41 @@
1
+ """Guide implementations — feedforward controls that inject context before turns."""
2
+ from __future__ import annotations
3
+
4
+ from pathlib import Path
5
+
6
+ from llm_code.runtime.knowledge_compiler import KnowledgeCompiler
7
+ from llm_code.runtime.repo_map import build_repo_map
8
+
9
+ PLAN_DENIED_TOOLS: frozenset[str] = frozenset({
10
+ "write_file", "edit_file", "bash", "git_commit", "git_push", "notebook_edit",
11
+ })
12
+
13
+
14
+ def repo_map_guide(cwd: Path, max_tokens: int = 2000) -> str:
15
+ """Build a compact repo map string. Returns empty on failure."""
16
+ try:
17
+ rmap = build_repo_map(cwd)
18
+ return rmap.to_compact(max_tokens=max_tokens)
19
+ except Exception:
20
+ return ""
21
+
22
+
23
+ def analysis_context_guide(context: str | None) -> str:
24
+ """Return stored analysis context, or empty string."""
25
+ return context or ""
26
+
27
+
28
+ def plan_mode_denied_tools(active: bool) -> frozenset[str]:
29
+ """Return the set of tools denied in plan mode. Empty when inactive."""
30
+ if active:
31
+ return PLAN_DENIED_TOOLS
32
+ return frozenset()
33
+
34
+
35
+ def knowledge_guide(cwd: Path, max_tokens: int = 3000) -> str:
36
+ """Return compiled project knowledge for system prompt injection."""
37
+ try:
38
+ compiler = KnowledgeCompiler(cwd=cwd, llm_provider=None)
39
+ return compiler.query(max_tokens=max_tokens)
40
+ except Exception:
41
+ return ""
@@ -0,0 +1,68 @@
1
+ """Sensor implementations — feedback controls that run after tool execution."""
2
+ from __future__ import annotations
3
+
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from llm_code.harness.config import HarnessFinding
8
+ from llm_code.runtime.auto_commit import auto_commit_file
9
+ from llm_code.runtime.auto_diagnose import auto_diagnose
10
+ from llm_code.analysis.engine import run_analysis
11
+
12
+
13
+ async def lsp_diagnose_sensor(
14
+ lsp_manager: Any | None,
15
+ file_path: str,
16
+ ) -> list[HarnessFinding]:
17
+ """Run LSP diagnostics on a file. Returns findings or empty list."""
18
+ if lsp_manager is None:
19
+ return []
20
+ try:
21
+ diag_errors = await auto_diagnose(lsp_manager, file_path)
22
+ return [
23
+ HarnessFinding(
24
+ sensor="lsp_diagnose",
25
+ message=msg,
26
+ file_path=file_path,
27
+ severity="error",
28
+ )
29
+ for msg in diag_errors
30
+ ]
31
+ except Exception:
32
+ return []
33
+
34
+
35
+ def code_rules_sensor(cwd: Path, file_path: str) -> list[HarnessFinding]:
36
+ """Run code analysis rules and return findings for the given file."""
37
+ try:
38
+ result = run_analysis(cwd)
39
+ return [
40
+ HarnessFinding(
41
+ sensor="code_rules",
42
+ message=f"{v.rule_key}: {v.message}",
43
+ file_path=v.file_path,
44
+ severity=v.severity,
45
+ )
46
+ for v in result.violations
47
+ if v.file_path == file_path
48
+ or file_path.endswith(v.file_path)
49
+ or v.file_path.endswith(file_path)
50
+ ]
51
+ except Exception:
52
+ return []
53
+
54
+
55
+ def auto_commit_sensor(file_path: Path, tool_name: str) -> HarnessFinding | None:
56
+ """Attempt auto-commit checkpoint. Returns finding on success, None on failure."""
57
+ try:
58
+ ok = auto_commit_file(file_path, tool_name)
59
+ if ok:
60
+ return HarnessFinding(
61
+ sensor="auto_commit",
62
+ message=f"checkpoint: {tool_name} {file_path.name}",
63
+ file_path=str(file_path),
64
+ severity="info",
65
+ )
66
+ return None
67
+ except Exception:
68
+ return None
@@ -0,0 +1,84 @@
1
+ """Project-type detection and default harness templates."""
2
+ from __future__ import annotations
3
+
4
+ from pathlib import Path
5
+
6
+ from llm_code.harness.config import HarnessControl
7
+
8
+ _WEB_FRAMEWORKS = ("fastapi", "flask", "django", "starlette", "sanic")
9
+
10
+
11
+ def detect_template(cwd: Path) -> str:
12
+ """Auto-detect project type from files in *cwd*."""
13
+ if (cwd / "pnpm-workspace.yaml").exists() or (cwd / "turbo.json").exists():
14
+ return "monorepo"
15
+
16
+ if (cwd / "manage.py").exists():
17
+ return "python-web"
18
+
19
+ has_pyproject = (cwd / "pyproject.toml").exists()
20
+ has_requirements = (cwd / "requirements.txt").exists()
21
+
22
+ if has_pyproject or has_requirements:
23
+ if _has_web_dep(cwd):
24
+ return "python-web"
25
+ return "python-cli"
26
+
27
+ if (cwd / "package.json").exists():
28
+ return "node-app"
29
+
30
+ return "generic"
31
+
32
+
33
+ def _has_web_dep(cwd: Path) -> bool:
34
+ for fname in ("pyproject.toml", "requirements.txt", "setup.cfg"):
35
+ fpath = cwd / fname
36
+ if fpath.exists():
37
+ try:
38
+ content = fpath.read_text(encoding="utf-8").lower()
39
+ if any(fw in content for fw in _WEB_FRAMEWORKS):
40
+ return True
41
+ except OSError:
42
+ pass
43
+ return False
44
+
45
+
46
+ def default_controls(template: str) -> tuple[HarnessControl, ...]:
47
+ _TEMPLATES: dict[str, tuple[HarnessControl, ...]] = {
48
+ "python-cli": (
49
+ HarnessControl(name="repo_map", category="guide", kind="computational", trigger="pre_turn"),
50
+ HarnessControl(name="analysis_context", category="guide", kind="computational", trigger="pre_turn"),
51
+ HarnessControl(name="knowledge", category="guide", kind="computational", trigger="pre_turn"),
52
+ HarnessControl(name="plan_mode", category="guide", kind="computational", trigger="pre_tool"),
53
+ HarnessControl(name="lsp_diagnose", category="sensor", kind="computational", trigger="post_tool"),
54
+ HarnessControl(name="code_rules", category="sensor", kind="computational", trigger="post_tool"),
55
+ HarnessControl(name="auto_commit", category="sensor", kind="computational", trigger="post_tool"),
56
+ ),
57
+ "python-web": (
58
+ HarnessControl(name="repo_map", category="guide", kind="computational", trigger="pre_turn"),
59
+ HarnessControl(name="architecture_doc", category="guide", kind="computational", trigger="pre_turn"),
60
+ HarnessControl(name="analysis_context", category="guide", kind="computational", trigger="pre_turn"),
61
+ HarnessControl(name="knowledge", category="guide", kind="computational", trigger="pre_turn"),
62
+ HarnessControl(name="plan_mode", category="guide", kind="computational", trigger="pre_tool"),
63
+ HarnessControl(name="lsp_diagnose", category="sensor", kind="computational", trigger="post_tool"),
64
+ HarnessControl(name="code_rules", category="sensor", kind="computational", trigger="post_tool"),
65
+ HarnessControl(name="auto_commit", category="sensor", kind="computational", trigger="post_tool"),
66
+ ),
67
+ "node-app": (
68
+ HarnessControl(name="repo_map", category="guide", kind="computational", trigger="pre_turn"),
69
+ HarnessControl(name="plan_mode", category="guide", kind="computational", trigger="pre_tool"),
70
+ HarnessControl(name="code_rules", category="sensor", kind="computational", trigger="post_tool"),
71
+ ),
72
+ "monorepo": (
73
+ HarnessControl(name="repo_map", category="guide", kind="computational", trigger="pre_turn"),
74
+ HarnessControl(name="architecture_doc", category="guide", kind="computational", trigger="pre_turn"),
75
+ HarnessControl(name="plan_mode", category="guide", kind="computational", trigger="pre_tool"),
76
+ HarnessControl(name="code_rules", category="sensor", kind="computational", trigger="post_tool"),
77
+ ),
78
+ "generic": (
79
+ HarnessControl(name="repo_map", category="guide", kind="computational", trigger="pre_turn"),
80
+ HarnessControl(name="plan_mode", category="guide", kind="computational", trigger="pre_tool"),
81
+ HarnessControl(name="code_rules", category="sensor", kind="computational", trigger="post_tool"),
82
+ ),
83
+ }
84
+ return _TEMPLATES.get(template, _TEMPLATES["generic"])
@@ -0,0 +1 @@
1
+ """HIDA: Hierarchical Intent-Driven Architecture for dynamic context loading."""
@@ -0,0 +1,187 @@
1
+ """2-layer task classifier: keyword matching first, LLM fallback second."""
2
+ from __future__ import annotations
3
+
4
+ import re
5
+ from dataclasses import replace
6
+ from typing import TYPE_CHECKING, Any
7
+
8
+ from llm_code.hida.types import TaskProfile, TaskType
9
+
10
+ if TYPE_CHECKING:
11
+ pass
12
+
13
+ # Keyword patterns per task type — order matters (first match wins within a type)
14
+ # IMPORTANT: More specific types are listed first. CODING is a low-priority fallback.
15
+ # Patterns use exclusive keywords that are unambiguous for each type.
16
+ _KEYWORD_PATTERNS: dict[TaskType, list[re.Pattern[str]]] = {
17
+ TaskType.DEBUGGING: [
18
+ re.compile(r"\b(fix|bug|crash|traceback|exception|debug|broken|fails?|failing)\b", re.I),
19
+ ],
20
+ TaskType.TESTING: [
21
+ re.compile(r"\b(tests?|unittest|pytest|coverage|spec|assert)\b", re.I),
22
+ ],
23
+ TaskType.REVIEWING: [
24
+ re.compile(r"\b(review|code.?review|pull.?request|pr|diff|audit)\b", re.I),
25
+ ],
26
+ TaskType.REFACTORING: [
27
+ re.compile(r"\b(refactor|restructure|reorganize|clean.?up|simplify|extract)\b", re.I),
28
+ ],
29
+ TaskType.PLANNING: [
30
+ re.compile(r"\b(plan|roadmap|proposal|rfc)\b", re.I),
31
+ ],
32
+ TaskType.DEPLOYMENT: [
33
+ re.compile(r"\b(deploy|release|ci/?cd|docker|kubernetes|k8s|production|staging)\b", re.I),
34
+ ],
35
+ TaskType.DOCUMENTATION: [
36
+ re.compile(r"\b(document(?:ation)?|readme|docstring|jsdoc|api.?doc)\b", re.I),
37
+ ],
38
+ TaskType.RESEARCH: [
39
+ re.compile(r"\b(research|investigate|explore|compare|evaluate|benchmark)\b", re.I),
40
+ ],
41
+ # CODING is a low-priority generic fallback — only unique code-creation keywords
42
+ TaskType.CODING: [
43
+ re.compile(r"\b(implement|build|function|class|endpoint)\b", re.I),
44
+ ],
45
+ }
46
+
47
+ # Priority ordering: when scores are tied, higher-priority type wins.
48
+ # Specific types beat CODING (low priority) and CONVERSATION (lowest).
49
+ _TYPE_PRIORITY: dict[TaskType, int] = {
50
+ TaskType.DEBUGGING: 10,
51
+ TaskType.TESTING: 10,
52
+ TaskType.REVIEWING: 10,
53
+ TaskType.REFACTORING: 10,
54
+ TaskType.PLANNING: 10,
55
+ TaskType.DEPLOYMENT: 10,
56
+ TaskType.DOCUMENTATION: 10,
57
+ TaskType.RESEARCH: 10,
58
+ TaskType.CODING: 5,
59
+ TaskType.CONVERSATION: 1,
60
+ }
61
+
62
+ # Classification confidence for keyword matches
63
+ _KEYWORD_CONFIDENCE = 0.85
64
+
65
+ # LLM classification prompt
66
+ _CLASSIFY_PROMPT = """\
67
+ Classify the following user message into exactly one task type.
68
+ Respond with ONLY the task type name, nothing else.
69
+
70
+ Valid types: coding, debugging, reviewing, planning, testing, refactoring, research, deployment, documentation, conversation
71
+
72
+ User message: {message}
73
+
74
+ Task type:"""
75
+
76
+
77
+ _FULL_LOAD_PROFILE = TaskProfile(
78
+ task_type=TaskType.CONVERSATION,
79
+ confidence=0.0,
80
+ tools=frozenset(),
81
+ memory_keys=frozenset(),
82
+ governance_categories=frozenset(),
83
+ load_full_prompt=True,
84
+ )
85
+
86
+
87
+ class TaskClassifier:
88
+ """Classifies user messages into task types using a 2-layer approach."""
89
+
90
+ def __init__(
91
+ self,
92
+ profiles: dict[TaskType, TaskProfile],
93
+ custom_patterns: dict[TaskType, list[re.Pattern[str]]] | None = None,
94
+ ) -> None:
95
+ self._profiles = profiles
96
+ self._patterns = custom_patterns if custom_patterns is not None else _KEYWORD_PATTERNS
97
+
98
+ def classify_by_keywords(self, message: str) -> TaskProfile | None:
99
+ """Layer 1: Fast keyword-based classification.
100
+
101
+ Returns a TaskProfile with keyword confidence, or None if ambiguous.
102
+ """
103
+ scores: dict[TaskType, int] = {}
104
+ for task_type, patterns in self._patterns.items():
105
+ for pattern in patterns:
106
+ matches = pattern.findall(message)
107
+ if matches:
108
+ scores[task_type] = scores.get(task_type, 0) + len(matches)
109
+
110
+ if not scores:
111
+ return None
112
+
113
+ # Pick highest scoring type; if tie, use priority to break it.
114
+ # If tied AND same priority, return None (ambiguous).
115
+ sorted_scores = sorted(
116
+ scores.items(),
117
+ key=lambda x: (x[1], _TYPE_PRIORITY.get(x[0], 0)),
118
+ reverse=True,
119
+ )
120
+ if len(sorted_scores) > 1:
121
+ top_score, top_priority = sorted_scores[0][1], _TYPE_PRIORITY.get(sorted_scores[0][0], 0)
122
+ second_score, second_priority = sorted_scores[1][1], _TYPE_PRIORITY.get(sorted_scores[1][0], 0)
123
+ if top_score == second_score and top_priority == second_priority:
124
+ return None
125
+
126
+ best_type = sorted_scores[0][0]
127
+ base_profile = self._profiles.get(best_type)
128
+ if base_profile is None:
129
+ return None
130
+
131
+ return replace(base_profile, confidence=_KEYWORD_CONFIDENCE)
132
+
133
+ async def classify_by_llm(
134
+ self, message: str, provider: Any
135
+ ) -> TaskProfile | None:
136
+ """Layer 2: LLM-based classification for ambiguous inputs.
137
+
138
+ Args:
139
+ message: The user message to classify.
140
+ provider: An LLM provider with a `complete(prompt)` async method.
141
+
142
+ Returns:
143
+ A TaskProfile if classification succeeds, None otherwise.
144
+ """
145
+ try:
146
+ prompt = _CLASSIFY_PROMPT.format(message=message[:500])
147
+ response = await provider.complete(prompt)
148
+ task_name = response.strip().lower()
149
+
150
+ # Try to match response to a TaskType
151
+ try:
152
+ task_type = TaskType(task_name)
153
+ except ValueError:
154
+ return None
155
+
156
+ base_profile = self._profiles.get(task_type)
157
+ if base_profile is None:
158
+ return None
159
+
160
+ return replace(base_profile, confidence=0.7)
161
+ except Exception:
162
+ return None
163
+
164
+ async def classify(
165
+ self,
166
+ message: str,
167
+ provider: Any | None = None,
168
+ confidence_threshold: float = 0.6,
169
+ ) -> TaskProfile:
170
+ """Full 2-layer classification: keywords first, LLM fallback.
171
+
172
+ Always returns a TaskProfile. Falls back to full-load profile
173
+ when confidence is below threshold or classification fails.
174
+ """
175
+ # Layer 1: keyword matching
176
+ result = self.classify_by_keywords(message)
177
+ if result is not None and result.confidence >= confidence_threshold:
178
+ return result
179
+
180
+ # Layer 2: LLM fallback (only if provider available)
181
+ if provider is not None:
182
+ llm_result = await self.classify_by_llm(message, provider)
183
+ if llm_result is not None and llm_result.confidence >= confidence_threshold:
184
+ return llm_result
185
+
186
+ # Fallback: full context load
187
+ return _FULL_LOAD_PROFILE
@@ -0,0 +1,49 @@
1
+ """HIDA engine: filters tools, memory, and prompt sections based on TaskProfile."""
2
+ from __future__ import annotations
3
+
4
+ from llm_code.hida.types import TaskProfile
5
+
6
+
7
+ class HidaEngine:
8
+ """Applies a TaskProfile to filter context before prompt building."""
9
+
10
+ def filter_tools(
11
+ self, profile: TaskProfile, available_tools: set[str]
12
+ ) -> set[str]:
13
+ """Return the subset of tools allowed by the profile.
14
+
15
+ If load_full_prompt is True, returns all available tools.
16
+ """
17
+ if profile.load_full_prompt:
18
+ return set(available_tools)
19
+ return profile.tools & available_tools
20
+
21
+ def filter_memory(
22
+ self, profile: TaskProfile, all_memory: dict[str, str]
23
+ ) -> dict[str, str]:
24
+ """Return the subset of memory entries relevant to the profile.
25
+
26
+ If load_full_prompt is True, returns all memory entries.
27
+ """
28
+ if profile.load_full_prompt:
29
+ return dict(all_memory)
30
+ return {k: v for k, v in all_memory.items() if k in profile.memory_keys}
31
+
32
+ def build_summary(self, profile: TaskProfile) -> str:
33
+ """Build a human-readable summary of the current classification.
34
+
35
+ Used by the /hida slash command.
36
+ """
37
+ if profile.load_full_prompt:
38
+ return (
39
+ f"Task: {profile.task_type.value} | "
40
+ f"Confidence: {profile.confidence:.2f} | "
41
+ f"Mode: full context load"
42
+ )
43
+ return (
44
+ f"Task: {profile.task_type.value} | "
45
+ f"Confidence: {profile.confidence:.2f} | "
46
+ f"Tools: {len(profile.tools)} | "
47
+ f"Memory keys: {len(profile.memory_keys)} | "
48
+ f"Categories: {', '.join(sorted(profile.governance_categories)) or 'none'}"
49
+ )