local-coding-agent 1.1.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 (101) hide show
  1. local_coding_agent/__init__.py +314 -0
  2. local_coding_agent/__main__.py +5 -0
  3. local_coding_agent/acp_server/__init__.py +38 -0
  4. local_coding_agent/acp_server/_codec.py +95 -0
  5. local_coding_agent/acp_server/_protocol.py +13 -0
  6. local_coding_agent/acp_server/_server.py +663 -0
  7. local_coding_agent/acp_server/_session.py +24 -0
  8. local_coding_agent/acp_server/_tools.py +126 -0
  9. local_coding_agent/ast_compactor.py +130 -0
  10. local_coding_agent/atomizer.py +77 -0
  11. local_coding_agent/benchmark/__init__.py +57 -0
  12. local_coding_agent/benchmark/_cases.py +307 -0
  13. local_coding_agent/benchmark/_judge.py +175 -0
  14. local_coding_agent/benchmark/_oracles.py +157 -0
  15. local_coding_agent/benchmark/_runner.py +259 -0
  16. local_coding_agent/benchmark/_stats.py +22 -0
  17. local_coding_agent/benchmark/_summary.py +143 -0
  18. local_coding_agent/benchmark_oracle_worker.py +169 -0
  19. local_coding_agent/calibration.py +154 -0
  20. local_coding_agent/capability.py +450 -0
  21. local_coding_agent/chat_compaction.py +276 -0
  22. local_coding_agent/cli/__init__.py +12 -0
  23. local_coding_agent/cli/_dispatch.py +377 -0
  24. local_coding_agent/cli/_handlers.py +320 -0
  25. local_coding_agent/cli/_handlers2.py +725 -0
  26. local_coding_agent/cli/_input.py +61 -0
  27. local_coding_agent/cli/_main.py +159 -0
  28. local_coding_agent/cli/_parser.py +327 -0
  29. local_coding_agent/context_manager.py +155 -0
  30. local_coding_agent/controller/__init__.py +18 -0
  31. local_coding_agent/controller/_constants.py +108 -0
  32. local_coding_agent/controller/_controller.py +1008 -0
  33. local_coding_agent/controller/_post_apply.py +216 -0
  34. local_coding_agent/delegator.py +180 -0
  35. local_coding_agent/desktop/__init__.py +8 -0
  36. local_coding_agent/desktop/app.py +100 -0
  37. local_coding_agent/desktop/assets/lucide.min.js +12 -0
  38. local_coding_agent/desktop/assets/tailwind.css +1 -0
  39. local_coding_agent/desktop/client_js.py +1271 -0
  40. local_coding_agent/desktop/components.py +555 -0
  41. local_coding_agent/desktop/server/__init__.py +37 -0
  42. local_coding_agent/desktop/server/_handlers.py +2178 -0
  43. local_coding_agent/desktop/server/_models.py +204 -0
  44. local_coding_agent/desktop/server/_server.py +347 -0
  45. local_coding_agent/desktop/server/_telemetry.py +87 -0
  46. local_coding_agent/desktop/styles.py +76 -0
  47. local_coding_agent/desktop/ui.py +53 -0
  48. local_coding_agent/doctor.py +413 -0
  49. local_coding_agent/hooks/__init__.py +19 -0
  50. local_coding_agent/hooks/_adapters.py +458 -0
  51. local_coding_agent/hooks/_bridge.py +346 -0
  52. local_coding_agent/hooks/_decision.py +26 -0
  53. local_coding_agent/lsp/__init__.py +60 -0
  54. local_coding_agent/lsp/_client.py +322 -0
  55. local_coding_agent/lsp/_fallback.py +297 -0
  56. local_coding_agent/lsp/_manager.py +173 -0
  57. local_coding_agent/lsp/_mock.py +200 -0
  58. local_coding_agent/lsp/_types.py +398 -0
  59. local_coding_agent/mcp_config.py +398 -0
  60. local_coding_agent/mcp_server.py +377 -0
  61. local_coding_agent/memory.py +140 -0
  62. local_coding_agent/mode_router.py +200 -0
  63. local_coding_agent/model_scanner.py +525 -0
  64. local_coding_agent/monitor.py +635 -0
  65. local_coding_agent/observation_policy.py +184 -0
  66. local_coding_agent/ollama_adapter.py +736 -0
  67. local_coding_agent/plan_mode.py +653 -0
  68. local_coding_agent/prescriptions.py +278 -0
  69. local_coding_agent/profiles.py +239 -0
  70. local_coding_agent/repository_tools.py +782 -0
  71. local_coding_agent/ripgrep.py +326 -0
  72. local_coding_agent/semantic_linter.py +122 -0
  73. local_coding_agent/service.py +405 -0
  74. local_coding_agent/session_events.py +684 -0
  75. local_coding_agent/session_query.py +578 -0
  76. local_coding_agent/skill_config.py +426 -0
  77. local_coding_agent/smoke.py +268 -0
  78. local_coding_agent/speculative_racing.py +85 -0
  79. local_coding_agent/spill.py +258 -0
  80. local_coding_agent/stats.py +255 -0
  81. local_coding_agent/stdio.py +132 -0
  82. local_coding_agent/subagent.py +516 -0
  83. local_coding_agent/task.py +90 -0
  84. local_coding_agent/task_store.py +142 -0
  85. local_coding_agent/tasks.py +240 -0
  86. local_coding_agent/terminal/__init__.py +35 -0
  87. local_coding_agent/terminal/_manager.py +156 -0
  88. local_coding_agent/terminal/_process.py +148 -0
  89. local_coding_agent/terminal/_session.py +452 -0
  90. local_coding_agent/terminal/_tools.py +233 -0
  91. local_coding_agent/terminal/_types.py +60 -0
  92. local_coding_agent/tool_call_parser.py +178 -0
  93. local_coding_agent/validators.py +563 -0
  94. local_coding_agent/vram_fit.py +240 -0
  95. local_coding_agent/worker_pool.py +516 -0
  96. local_coding_agent-1.1.0.dist-info/METADATA +202 -0
  97. local_coding_agent-1.1.0.dist-info/RECORD +101 -0
  98. local_coding_agent-1.1.0.dist-info/WHEEL +5 -0
  99. local_coding_agent-1.1.0.dist-info/entry_points.txt +3 -0
  100. local_coding_agent-1.1.0.dist-info/licenses/LICENSE +21 -0
  101. local_coding_agent-1.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,184 @@
1
+ """Filesystem Observation Policy and Prior-Observation Gate.
2
+
3
+ Adapted from DeepSeek Harness @deepseek-ai/dsh-fs-observation-policy.
4
+ Enforces the safety invariant that files must be authoritatively observed (read)
5
+ within the active session before any mutation (edit, patch, replace) is allowed.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import hashlib
11
+ import os
12
+ from pathlib import Path
13
+
14
+
15
+ class FsObservationError(Exception):
16
+ """Raised when a file system mutation violates the observation policy."""
17
+
18
+
19
+ class FsObservationGate:
20
+ """Tracks observed files and content hashes per session.
21
+
22
+ Enforces that local models must read/observe a file before attempting
23
+ to propose or apply edits to it.
24
+ """
25
+
26
+ def __init__(self) -> None:
27
+ # Maps session_id -> {canonical_path: sha256_hex_hash}
28
+ self._sessions: dict[str, dict[str, str]] = {}
29
+
30
+ def _canonical_key(self, file_path: str | Path) -> str:
31
+ """Derive a canonical key for cross-platform matching."""
32
+ resolved = Path(file_path).resolve()
33
+ posix = resolved.as_posix()
34
+ return posix.casefold() if os.name == "nt" else posix
35
+
36
+ def observe_file(
37
+ self,
38
+ session_id: str,
39
+ file_path: str | Path,
40
+ content: str | bytes | None = None,
41
+ ) -> str:
42
+ """Record an authoritative observation of a file and return its SHA-256 hash.
43
+
44
+ If content is provided, it is hashed directly.
45
+ If content is None, the file is read from disk.
46
+ """
47
+ path_obj = Path(file_path).resolve()
48
+
49
+ if content is not None:
50
+ if isinstance(content, str):
51
+ raw_bytes = content.encode("utf-8")
52
+ elif isinstance(content, bytes):
53
+ raw_bytes = content
54
+ else:
55
+ raise TypeError(f"Expected str, bytes, or None for content, got {type(content).__name__}")
56
+ else:
57
+ if not path_obj.is_file():
58
+ raise FileNotFoundError(f"File not found for observation: {file_path}")
59
+ raw_bytes = path_obj.read_bytes()
60
+
61
+ sha256_hash = hashlib.sha256(raw_bytes).hexdigest()
62
+ canonical_key = self._canonical_key(path_obj)
63
+
64
+ if session_id not in self._sessions:
65
+ self._sessions[session_id] = {}
66
+
67
+ self._sessions[session_id][canonical_key] = sha256_hash
68
+ return sha256_hash
69
+
70
+ def is_observed(self, session_id: str, file_path: str | Path) -> bool:
71
+ """Check if a file has been observed in the given session."""
72
+ session_map = self._sessions.get(session_id)
73
+ if not session_map:
74
+ return False
75
+ canonical_key = self._canonical_key(file_path)
76
+ return canonical_key in session_map
77
+
78
+ def get_observed_hash(self, session_id: str, file_path: str | Path) -> str | None:
79
+ """Get the recorded SHA-256 hash for an observed file, or None if unobserved."""
80
+ session_map = self._sessions.get(session_id)
81
+ if not session_map:
82
+ return None
83
+ canonical_key = self._canonical_key(file_path)
84
+ return session_map.get(canonical_key)
85
+
86
+ def verify_edit_intent(
87
+ self,
88
+ session_id: str,
89
+ file_path: str | Path,
90
+ ) -> tuple[bool, str | None]:
91
+ """Verify whether an edit can proceed under the observation policy.
92
+
93
+ Returns:
94
+ (True, None) if observed, or
95
+ (False, "FS_NOT_OBSERVED: edit requires reading '<file>' first") if not observed.
96
+ """
97
+ if self.is_observed(session_id, file_path):
98
+ return True, None
99
+
100
+ display_name = str(file_path)
101
+ return False, f"FS_NOT_OBSERVED: edit requires reading '{display_name}' first"
102
+
103
+ def verify_freshness(
104
+ self,
105
+ session_id: str,
106
+ file_path: str | Path,
107
+ ) -> tuple[bool, str | None]:
108
+ """Verify that the file on disk matches the hash recorded when observed."""
109
+ if not self.is_observed(session_id, file_path):
110
+ display_name = str(file_path)
111
+ return False, f"FS_NOT_OBSERVED: edit requires reading '{display_name}' first"
112
+
113
+ path_obj = Path(file_path).resolve()
114
+ if not path_obj.is_file():
115
+ return False, f"FS_DELETED: file '{file_path}' was deleted after observation"
116
+
117
+ current_hash = hashlib.sha256(path_obj.read_bytes()).hexdigest()
118
+ observed_hash = self.get_observed_hash(session_id, file_path)
119
+
120
+ if current_hash != observed_hash:
121
+ return False, f"FS_STALE: file '{file_path}' was modified on disk since last observed"
122
+
123
+ return True, None
124
+
125
+ def reset_session(self, session_id: str) -> None:
126
+ """Clear all observation records for a session."""
127
+ self._sessions.pop(session_id, None)
128
+
129
+ def list_observed(self, session_id: str) -> list[str]:
130
+ """List all canonical paths observed in a session."""
131
+ return list(self._sessions.get(session_id, {}).keys())
132
+
133
+ def clear(self) -> None:
134
+ """Clear all session observations."""
135
+ self._sessions.clear()
136
+
137
+
138
+ # Global default instance & convenience helpers
139
+ _DEFAULT_OBSERVATION_GATE: FsObservationGate | None = None
140
+
141
+
142
+ def get_default_observation_gate() -> FsObservationGate:
143
+ """Retrieve or lazily initialize the default FsObservationGate."""
144
+ global _DEFAULT_OBSERVATION_GATE
145
+ if _DEFAULT_OBSERVATION_GATE is None:
146
+ _DEFAULT_OBSERVATION_GATE = FsObservationGate()
147
+ return _DEFAULT_OBSERVATION_GATE
148
+
149
+
150
+ def observe_file(
151
+ session_id: str,
152
+ file_path: str | Path,
153
+ content: str | bytes | None = None,
154
+ ) -> str:
155
+ """Record file observation in the default gate."""
156
+ return get_default_observation_gate().observe_file(
157
+ session_id=session_id,
158
+ file_path=file_path,
159
+ content=content,
160
+ )
161
+
162
+
163
+ def is_observed(session_id: str, file_path: str | Path) -> bool:
164
+ """Check if file is observed in the default gate."""
165
+ return get_default_observation_gate().is_observed(
166
+ session_id=session_id,
167
+ file_path=file_path,
168
+ )
169
+
170
+
171
+ def verify_edit_intent(
172
+ session_id: str,
173
+ file_path: str | Path,
174
+ ) -> tuple[bool, str | None]:
175
+ """Verify edit intent against the default gate."""
176
+ return get_default_observation_gate().verify_edit_intent(
177
+ session_id=session_id,
178
+ file_path=file_path,
179
+ )
180
+
181
+
182
+ def reset_session(session_id: str) -> None:
183
+ """Reset session observations in the default gate."""
184
+ get_default_observation_gate().reset_session(session_id)