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.
- llm_code/__init__.py +2 -0
- llm_code/analysis/__init__.py +6 -0
- llm_code/analysis/cache.py +33 -0
- llm_code/analysis/engine.py +256 -0
- llm_code/analysis/go_rules.py +114 -0
- llm_code/analysis/js_rules.py +84 -0
- llm_code/analysis/python_rules.py +311 -0
- llm_code/analysis/rules.py +140 -0
- llm_code/analysis/rust_rules.py +108 -0
- llm_code/analysis/universal_rules.py +111 -0
- llm_code/api/__init__.py +0 -0
- llm_code/api/client.py +90 -0
- llm_code/api/errors.py +73 -0
- llm_code/api/openai_compat.py +390 -0
- llm_code/api/provider.py +35 -0
- llm_code/api/sse.py +52 -0
- llm_code/api/types.py +140 -0
- llm_code/cli/__init__.py +0 -0
- llm_code/cli/commands.py +70 -0
- llm_code/cli/image.py +122 -0
- llm_code/cli/render.py +214 -0
- llm_code/cli/status_line.py +79 -0
- llm_code/cli/streaming.py +92 -0
- llm_code/cli/tui_main.py +220 -0
- llm_code/computer_use/__init__.py +11 -0
- llm_code/computer_use/app_detect.py +49 -0
- llm_code/computer_use/app_tier.py +57 -0
- llm_code/computer_use/coordinator.py +99 -0
- llm_code/computer_use/input_control.py +71 -0
- llm_code/computer_use/screenshot.py +93 -0
- llm_code/cron/__init__.py +13 -0
- llm_code/cron/parser.py +145 -0
- llm_code/cron/scheduler.py +135 -0
- llm_code/cron/storage.py +126 -0
- llm_code/enterprise/__init__.py +1 -0
- llm_code/enterprise/audit.py +59 -0
- llm_code/enterprise/auth.py +26 -0
- llm_code/enterprise/oidc.py +95 -0
- llm_code/enterprise/rbac.py +65 -0
- llm_code/harness/__init__.py +5 -0
- llm_code/harness/config.py +33 -0
- llm_code/harness/engine.py +129 -0
- llm_code/harness/guides.py +41 -0
- llm_code/harness/sensors.py +68 -0
- llm_code/harness/templates.py +84 -0
- llm_code/hida/__init__.py +1 -0
- llm_code/hida/classifier.py +187 -0
- llm_code/hida/engine.py +49 -0
- llm_code/hida/profiles.py +95 -0
- llm_code/hida/types.py +28 -0
- llm_code/ide/__init__.py +1 -0
- llm_code/ide/bridge.py +80 -0
- llm_code/ide/detector.py +76 -0
- llm_code/ide/server.py +169 -0
- llm_code/logging.py +29 -0
- llm_code/lsp/__init__.py +0 -0
- llm_code/lsp/client.py +298 -0
- llm_code/lsp/detector.py +42 -0
- llm_code/lsp/manager.py +56 -0
- llm_code/lsp/tools.py +288 -0
- llm_code/marketplace/__init__.py +0 -0
- llm_code/marketplace/builtin_registry.py +102 -0
- llm_code/marketplace/installer.py +162 -0
- llm_code/marketplace/plugin.py +78 -0
- llm_code/marketplace/registry.py +360 -0
- llm_code/mcp/__init__.py +0 -0
- llm_code/mcp/bridge.py +87 -0
- llm_code/mcp/client.py +117 -0
- llm_code/mcp/health.py +120 -0
- llm_code/mcp/manager.py +214 -0
- llm_code/mcp/oauth.py +219 -0
- llm_code/mcp/transport.py +254 -0
- llm_code/mcp/types.py +53 -0
- llm_code/remote/__init__.py +0 -0
- llm_code/remote/client.py +136 -0
- llm_code/remote/protocol.py +22 -0
- llm_code/remote/server.py +275 -0
- llm_code/remote/ssh_proxy.py +56 -0
- llm_code/runtime/__init__.py +0 -0
- llm_code/runtime/auto_commit.py +56 -0
- llm_code/runtime/auto_diagnose.py +62 -0
- llm_code/runtime/checkpoint.py +70 -0
- llm_code/runtime/checkpoint_recovery.py +142 -0
- llm_code/runtime/compaction.py +35 -0
- llm_code/runtime/compressor.py +415 -0
- llm_code/runtime/config.py +533 -0
- llm_code/runtime/context.py +49 -0
- llm_code/runtime/conversation.py +921 -0
- llm_code/runtime/cost_tracker.py +126 -0
- llm_code/runtime/dream.py +127 -0
- llm_code/runtime/file_protection.py +150 -0
- llm_code/runtime/hardware.py +85 -0
- llm_code/runtime/hooks.py +223 -0
- llm_code/runtime/indexer.py +230 -0
- llm_code/runtime/knowledge_compiler.py +232 -0
- llm_code/runtime/memory.py +132 -0
- llm_code/runtime/memory_layers.py +467 -0
- llm_code/runtime/memory_lint.py +252 -0
- llm_code/runtime/model_aliases.py +37 -0
- llm_code/runtime/ollama.py +93 -0
- llm_code/runtime/overlay.py +124 -0
- llm_code/runtime/permissions.py +200 -0
- llm_code/runtime/plan.py +45 -0
- llm_code/runtime/prompt.py +238 -0
- llm_code/runtime/repo_map.py +174 -0
- llm_code/runtime/sandbox.py +116 -0
- llm_code/runtime/session.py +268 -0
- llm_code/runtime/skill_resolver.py +61 -0
- llm_code/runtime/skills.py +133 -0
- llm_code/runtime/speculative.py +75 -0
- llm_code/runtime/streaming_executor.py +216 -0
- llm_code/runtime/telemetry.py +196 -0
- llm_code/runtime/token_budget.py +26 -0
- llm_code/runtime/vcr.py +142 -0
- llm_code/runtime/vision.py +102 -0
- llm_code/swarm/__init__.py +1 -0
- llm_code/swarm/backend_subprocess.py +108 -0
- llm_code/swarm/backend_tmux.py +103 -0
- llm_code/swarm/backend_worktree.py +306 -0
- llm_code/swarm/checkpoint.py +74 -0
- llm_code/swarm/coordinator.py +236 -0
- llm_code/swarm/mailbox.py +88 -0
- llm_code/swarm/manager.py +202 -0
- llm_code/swarm/memory_sync.py +80 -0
- llm_code/swarm/recovery.py +21 -0
- llm_code/swarm/team.py +67 -0
- llm_code/swarm/types.py +31 -0
- llm_code/task/__init__.py +16 -0
- llm_code/task/diagnostics.py +93 -0
- llm_code/task/manager.py +162 -0
- llm_code/task/types.py +112 -0
- llm_code/task/verifier.py +104 -0
- llm_code/tools/__init__.py +0 -0
- llm_code/tools/agent.py +145 -0
- llm_code/tools/agent_roles.py +82 -0
- llm_code/tools/base.py +94 -0
- llm_code/tools/bash.py +565 -0
- llm_code/tools/computer_use_tools.py +278 -0
- llm_code/tools/coordinator_tool.py +75 -0
- llm_code/tools/cron_create.py +90 -0
- llm_code/tools/cron_delete.py +49 -0
- llm_code/tools/cron_list.py +51 -0
- llm_code/tools/deferred.py +92 -0
- llm_code/tools/dump.py +116 -0
- llm_code/tools/edit_file.py +282 -0
- llm_code/tools/git_tools.py +531 -0
- llm_code/tools/glob_search.py +112 -0
- llm_code/tools/grep_search.py +144 -0
- llm_code/tools/ide_diagnostics.py +59 -0
- llm_code/tools/ide_open.py +58 -0
- llm_code/tools/ide_selection.py +52 -0
- llm_code/tools/memory_tools.py +138 -0
- llm_code/tools/multi_edit.py +143 -0
- llm_code/tools/notebook_edit.py +107 -0
- llm_code/tools/notebook_read.py +81 -0
- llm_code/tools/parsing.py +63 -0
- llm_code/tools/read_file.py +154 -0
- llm_code/tools/registry.py +58 -0
- llm_code/tools/search_backends/__init__.py +56 -0
- llm_code/tools/search_backends/brave.py +56 -0
- llm_code/tools/search_backends/duckduckgo.py +129 -0
- llm_code/tools/search_backends/searxng.py +71 -0
- llm_code/tools/search_backends/tavily.py +73 -0
- llm_code/tools/swarm_create.py +109 -0
- llm_code/tools/swarm_delete.py +95 -0
- llm_code/tools/swarm_list.py +44 -0
- llm_code/tools/swarm_message.py +109 -0
- llm_code/tools/task_close.py +79 -0
- llm_code/tools/task_plan.py +79 -0
- llm_code/tools/task_verify.py +90 -0
- llm_code/tools/tool_search.py +65 -0
- llm_code/tools/web_common.py +258 -0
- llm_code/tools/web_fetch.py +223 -0
- llm_code/tools/web_search.py +280 -0
- llm_code/tools/write_file.py +118 -0
- llm_code/tui/__init__.py +1 -0
- llm_code/tui/app.py +2432 -0
- llm_code/tui/chat_view.py +82 -0
- llm_code/tui/chat_widgets.py +309 -0
- llm_code/tui/header_bar.py +46 -0
- llm_code/tui/input_bar.py +349 -0
- llm_code/tui/keybindings.py +142 -0
- llm_code/tui/marketplace.py +210 -0
- llm_code/tui/status_bar.py +72 -0
- llm_code/tui/theme.py +96 -0
- llm_code/utils/__init__.py +0 -0
- llm_code/utils/diff.py +111 -0
- llm_code/utils/errors.py +70 -0
- llm_code/utils/hyperlink.py +73 -0
- llm_code/utils/notebook.py +179 -0
- llm_code/utils/search.py +69 -0
- llm_code/utils/text_normalize.py +28 -0
- llm_code/utils/version_check.py +62 -0
- llm_code/vim/__init__.py +4 -0
- llm_code/vim/engine.py +51 -0
- llm_code/vim/motions.py +172 -0
- llm_code/vim/operators.py +183 -0
- llm_code/vim/text_objects.py +139 -0
- llm_code/vim/transitions.py +279 -0
- llm_code/vim/types.py +68 -0
- llm_code/voice/__init__.py +1 -0
- llm_code/voice/languages.py +43 -0
- llm_code/voice/recorder.py +136 -0
- llm_code/voice/stt.py +36 -0
- llm_code/voice/stt_anthropic.py +66 -0
- llm_code/voice/stt_google.py +32 -0
- llm_code/voice/stt_whisper.py +52 -0
- llmcode_cli-1.0.0.dist-info/METADATA +524 -0
- llmcode_cli-1.0.0.dist-info/RECORD +212 -0
- llmcode_cli-1.0.0.dist-info/WHEEL +4 -0
- llmcode_cli-1.0.0.dist-info/entry_points.txt +2 -0
- 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
|
llm_code/hida/engine.py
ADDED
|
@@ -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
|
+
)
|