localcode 0.3.5__py3-none-any.whl → 0.3.7__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.
localcode/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  __all__ = ["__version__"]
2
2
 
3
- __version__ = "0.3.5"
3
+ __version__ = "0.3.7"
localcode/agent/loop.py CHANGED
@@ -40,6 +40,7 @@ __all__ = ["run_agent_loop"]
40
40
 
41
41
  from .constants import (
42
42
  MAX_ROUNDS,
43
+ CHURN_FILE_WRITE_LIMIT,
43
44
  MAX_OUTPUT_TOKENS,
44
45
  MAX_THINKING_SECONDS,
45
46
  MAX_THINKING_CHARS,
@@ -70,6 +71,7 @@ from .prompts import (
70
71
  )
71
72
  from .prompt_context import build_agent_system_prompt
72
73
  from .recovery import (
74
+ rewrite_hard_stop,
73
75
  StallMode,
74
76
  detect_stall,
75
77
  nudge_for,
@@ -1141,33 +1143,13 @@ def run_agent_loop(
1141
1143
  _ephemeral_nudge_indices.append(len(messages) - 1)
1142
1144
 
1143
1145
  # ── Stalled-round recovery ──
1144
- # Three failure modes end a round with the model NOT actually
1145
- # done, but the user watching a frozen screen:
1146
- #
1147
- # (A) Empty round — long reasoning, then server closes the
1148
- # stream with NO content and NO tool_calls. Images 107/108.
1149
- #
1150
- # (B) Intent-without-action model emits ONE sentence of
1151
- # forward-looking narration ("I'll build the web app now.
1152
- # Let me create this.") and stops, with no tool call. Image
1153
- # 112. Happens because IQ2/IQ3 sometimes exits the reasoning
1154
- # channel with just a sign-off instead of an action.
1155
- #
1156
- # (C) Gave-up-after-rejection — most recent tool call returned
1157
- # a "REJECTED" / "Error" string AND the round ended with no
1158
- # follow-up tool call. The model hit a snag and bailed
1159
- # instead of reading the rejection's actionable feedback
1160
- # and retrying. This was image 123: write_file got
1161
- # REJECTED for syntax error, model stopped completely.
1162
- #
1163
- # Stall detection + nudge. Logic lives in agent/recovery.py
1164
- # (T0.1-d split). `stall` is None for a productive round,
1165
- # otherwise a `StallMode` enum that drives both the telemetry
1166
- # label and the per-mode nudge text.
1167
- # Gated on Feature.AUTO_NUDGE_RECOVERY. Disabled → stalls end
1168
- # the turn silently (no synthetic SYSTEM: nudge), which is the
1169
- # pre-recovery behaviour and what eval wants when A/B-ing the
1170
- # nudge feature's actual contribution to task completion.
1146
+ # Three failure modes end a round with the model NOT done: (A) empty
1147
+ # round (long reasoning, stream closes with no content/tools); (B)
1148
+ # intent-without-action (one narration sentence, no tool call); (C)
1149
+ # gave-up-after-rejection (last tool returned REJECTED/Error, round
1150
+ # ended with no retry). Detection + nudge live in agent/recovery.py;
1151
+ # `stall` is None for a productive round else a StallMode. Gated on
1152
+ # Feature.AUTO_NUDGE_RECOVERY (off stalls end the turn silently).
1171
1153
  from ..features import Feature, is_enabled as _is_enabled
1172
1154
  stall = detect_stall(
1173
1155
  tool_calls=tool_calls,
@@ -1508,11 +1490,22 @@ def run_agent_loop(
1508
1490
  "smallest targeted edit and verify it."
1509
1491
  )
1510
1492
 
1493
+ # HARD rewrite-stop: the churn NUDGE (limit 3) only advises — logs
1494
+ # showed a model rewrite one file 16x while 25-34 nudges fired.
1495
+ # Past 2x the nudge limit, REJECT further full rewrites (see
1496
+ # recovery.rewrite_hard_stop). Key on the RAW path like the counter.
1497
+ _rewrite_limit_stub = None
1498
+ if tool_name in ("write_file", "append_file", "multi_edit") and isinstance(args, dict):
1499
+ _rw_path = args.get("path") or args.get("file_path") or ""
1500
+ _rewrite_limit_stub = rewrite_hard_stop(_rw_path, _file_write_counts)
1511
1501
  # Execute (timed — wall-clock added to _round_tool_exec_ms
1512
1502
  # so round_end can show the model what fraction of its
1513
1503
  # round time was spent waiting on tools vs LLM).
1514
1504
  _tool_started_at = time.monotonic()
1515
- if _edit_sequence_stub is not None:
1505
+ if _rewrite_limit_stub is not None:
1506
+ from ..tools import ToolResult as _ToolResult
1507
+ _tool_result_obj = _ToolResult(text=_rewrite_limit_stub, ok=False, facts={"tool": tool_name, "ok": False, "repeated_failed_call": True, "rewrite_hard_stop": True})
1508
+ elif _edit_sequence_stub is not None:
1516
1509
  from ..tools import ToolResult as _ToolResult
1517
1510
  _tool_result_obj = _ToolResult(text=_edit_sequence_stub, ok=False, facts={"tool": tool_name, "ok": False, "edit_sequence": "missing_context"})
1518
1511
  elif _oversize_stub is not None:
@@ -127,25 +127,12 @@ def detect_stall(
127
127
  if not content:
128
128
  return StallMode.EMPTY
129
129
 
130
- # NARRATION. Real failures showed the model
131
- # emitting hundreds of chars describing the edit it was about to
132
- # make, then ending the round with zero tool calls.
133
- #
134
- # Important: do NOT classify every short answer after a tool call as
135
- # narration. A question like "weather in ny?" legitimately uses one
136
- # tool, then answers in ~200 chars. The old broad `<400 chars after
137
- # tools` rule deleted that answer, injected an auto-nudge, and made
138
- # the model call the same API repeatedly. Only intent phrases count
139
- # as narration.
140
- #
141
- # Two complementary detectors:
142
- # • _NARRATION_INTENT_RE — "let me X" / "I'll Y" / "going to Z"
143
- # anywhere in the content (full scan, not just tail) catches
144
- # case (1) and similar variants where the commit phrase may
145
- # be mid-content rather than at the end.
146
- # • _NARRATION_PRESENT_PARTICIPLE_RE — last sentence starts with
147
- # "Updating X / Changing Y / Now writing Z" + target. Past
148
- # tense "Updated X" / "Changed Y" deliberately excluded.
130
+ # NARRATION detection. Only INTENT phrases count — not every short answer
131
+ # after a tool call (a "weather in ny?" answer legitimately runs one tool
132
+ # then replies in ~200 chars; the old broad rule deleted such answers and
133
+ # looped). Two detectors: _NARRATION_INTENT_RE ("let me X"/"I'll Y" anywhere)
134
+ # and _NARRATION_PRESENT_PARTICIPLE_RE (last sentence "Updating X/Now writing
135
+ # Z" + target; past tense "Updated X" excluded).
149
136
  stripped = content.strip()
150
137
  if bool(tools_called_prior) and stripped:
151
138
  if _NARRATION_INTENT_RE.search(stripped):
@@ -398,3 +385,19 @@ def churn_nudge_for(signal: ChurnSignal) -> str:
398
385
  "disk that you did not create are not part of your task. If you truly "
399
386
  "lack information only the user has, ask ONE focused question."
400
387
  )
388
+
389
+
390
+ def rewrite_hard_stop(path, file_write_counts) -> str | None:
391
+ """Circuit-breaker for rewrite drift: reject once a path is rewritten past
392
+ 2x the nudge limit (the nudge only advises; logs showed 16x on one file)."""
393
+ if not path or not isinstance(path, str):
394
+ return None
395
+ n = file_write_counts.get(path, 0)
396
+ if n < CHURN_FILE_WRITE_LIMIT * 2:
397
+ return None
398
+ return (
399
+ f"REJECTED — HARD STOP: you have rewritten {path} {n} times this turn "
400
+ "and it's still not right — full rewrites are drifting. Make ONE "
401
+ "targeted edit_file change to the broken line, or leave this file and "
402
+ "move on. Do NOT call write_file on this path again."
403
+ )
@@ -89,58 +89,6 @@ def _decode_streaming_content(args: str, start_pos: int) -> tuple[str, int]:
89
89
  return ("".join(out), i)
90
90
 
91
91
 
92
- def _maybe_incremental_write(
93
- app: "LocalCodeApp",
94
- result: "StreamRoundResult",
95
- *,
96
- tool_name: str,
97
- tool_idx: int,
98
- snippet: str,
99
- ) -> None:
100
- """Write the streaming `content` to disk incrementally for write_file
101
- / append_file. Tracks per-tool-index state on `result.live_writes`
102
- so we only write NEW bytes each tick.
103
-
104
- Best-effort: any failure (path not yet decoded, decode error, FS
105
- error) silently no-ops — the round-end atomic write_file.execute()
106
- is still authoritative."""
107
- if tool_name not in {"write_file", "append_file"}:
108
- return
109
- if not snippet:
110
- return
111
- state = result.live_writes.get(tool_idx)
112
- if state is None:
113
- # Need a path before we can write anywhere. Extract it from the
114
- # leading args; tool schemas put it FIRST so it shows up in the
115
- # first ~256 chars.
116
- m = _PATH_FIELD_RE.search(snippet)
117
- if not m:
118
- return
119
- path_str = m.group(1)
120
- try:
121
- target = Path(path_str)
122
- if not target.is_absolute():
123
- target = Path(app.repo_root) / target
124
- target.parent.mkdir(parents=True, exist_ok=True)
125
- # write_file truncates; append_file extends an existing file.
126
- mode = "w" if tool_name == "write_file" else "a"
127
- target.open(mode).close() # truncate / touch
128
- except Exception:
129
- return
130
- state = {"path": str(target), "decoded_pos": 0}
131
- result.live_writes[tool_idx] = state
132
- decoded, new_pos = _decode_streaming_content(snippet, state["decoded_pos"])
133
- if not decoded and new_pos == state["decoded_pos"]:
134
- return
135
- if decoded:
136
- try:
137
- with open(state["path"], "a", encoding="utf-8") as f:
138
- f.write(decoded)
139
- except Exception:
140
- return
141
- state["decoded_pos"] = new_pos
142
-
143
-
144
92
  @dataclass
145
93
  class StreamRoundResult:
146
94
  content_parts: list[str] = field(default_factory=list)
@@ -253,18 +201,19 @@ def stream_model_round(
253
201
  out.stream(chunk)
254
202
  return
255
203
  if typ == "tool_preview":
204
+ # Live file preview is driven from THIS event (the TUI decodes
205
+ # args_snippet). We deliberately do NOT write to the target file
206
+ # incrementally: if the round was cut short (token/context cap, a
207
+ # mid-stream server SIGKILL, Ctrl-C), the partial bytes orphaned on
208
+ # disk while the loop discarded the tool call — leaving a truncated
209
+ # file (e.g. "autop") that the model then read back and churned on
210
+ # forever. The authoritative full-overwrite write_file.execute() at
211
+ # round-commit is the ONLY thing that touches disk.
256
212
  out.tool_preview(
257
213
  event.get("name", ""),
258
214
  int(event.get("args_chars", 0) or 0),
259
215
  event.get("args_snippet", "") or "",
260
216
  )
261
- _maybe_incremental_write(
262
- app,
263
- result,
264
- tool_name=event.get("name", "") or "",
265
- tool_idx=int(event.get("index", -1) or -1),
266
- snippet=event.get("args_snippet", "") or "",
267
- )
268
217
  return
269
218
  if typ == "tool_calls":
270
219
  result.tool_calls = event.get("tool_calls") or []
@@ -258,14 +258,22 @@ def execute(ctx: ToolContext, args: dict) -> str:
258
258
  f"file may already be in the state you want."
259
259
  )
260
260
  path.write_text(new_content)
261
+ _sw = ""
262
+ try:
263
+ from .syntax_check import check_syntax
264
+ _e = check_syntax(str(path), new_content)
265
+ if _e:
266
+ _sw = f"\n\n⚠ SYNTAX ERROR after this edit — fix it now: {_e}"
267
+ except Exception:
268
+ _sw = ""
261
269
  diff = list(difflib.unified_diff(
262
270
  old_content.splitlines(keepends=True),
263
271
  new_content.splitlines(keepends=True),
264
272
  fromfile=args["path"], tofile=args["path"], lineterm="",
265
273
  ))
266
274
  if diff:
267
- return "\n".join(diff[:60])
268
- return f"Edited {args['path']} ({count} replacement{'s' if count != 1 else ''})"
275
+ return "\n".join(diff[:60]) + _sw
276
+ return f"Edited {args['path']} ({count} replacement{'s' if count != 1 else ''}){_sw}"
269
277
 
270
278
  # ── Tier 2: strip Read-output line-number prefixes from old_string ──
271
279
  stripped = _strip_read_prefix(old)
@@ -98,6 +98,14 @@ def execute(ctx: ToolContext, args: dict) -> str:
98
98
  return f"Error: no-op multi_edit on {args['path']}; applied 0/{len(edits)}."
99
99
 
100
100
  path.write_text(content)
101
+ _sw = ""
102
+ try:
103
+ from .syntax_check import check_syntax
104
+ _e = check_syntax(str(path), content)
105
+ if _e:
106
+ _sw = f"\n\n⚠ SYNTAX ERROR after these edits — fix it now: {_e}"
107
+ except Exception:
108
+ _sw = ""
101
109
  diff = list(difflib.unified_diff(
102
110
  original.splitlines(keepends=True),
103
111
  content.splitlines(keepends=True),
@@ -105,8 +113,8 @@ def execute(ctx: ToolContext, args: dict) -> str:
105
113
  ))
106
114
  head = f"Applied {len(prepared)}/{len(edits)} edits to {args['path']}"
107
115
  if diff:
108
- return head + "\n" + "\n".join(diff[:120])
109
- return head
116
+ return head + "\n" + "\n".join(diff[:120]) + _sw
117
+ return head + _sw
110
118
 
111
119
 
112
120
  def _find_all(text: str, needle: str) -> list[int]:
@@ -32,12 +32,52 @@ SCHEMA = {
32
32
  }
33
33
 
34
34
 
35
+ def _suggest_path(missing) -> str:
36
+ """Suggest the real file when the model gave a typo'd/wrong-case path.
37
+
38
+ Walk up to the nearest existing ancestor dir and fuzzy-match the missing
39
+ filename (and its path segments) against what's actually there.
40
+ """
41
+ import difflib
42
+ from pathlib import Path
43
+ try:
44
+ missing = Path(missing)
45
+ name = missing.name
46
+ # Nearest existing ancestor to search in.
47
+ base = missing.parent
48
+ for _ in range(6):
49
+ if base.exists():
50
+ break
51
+ base = base.parent
52
+ if not base.exists():
53
+ return ""
54
+ candidates = []
55
+ for p in base.rglob("*"):
56
+ if p.is_file():
57
+ candidates.append(p)
58
+ if len(candidates) > 4000:
59
+ break
60
+ names = [p.name for p in candidates]
61
+ close = difflib.get_close_matches(name, names, n=1, cutoff=0.6)
62
+ if close:
63
+ match = next(p for p in candidates if p.name == close[0])
64
+ return f"Did you mean: {match} — read THAT exact path (don't guess variants)."
65
+ except Exception:
66
+ pass
67
+ return ""
68
+
69
+
35
70
  def execute(ctx: ToolContext, args: dict) -> str:
36
71
  if "path" not in args:
37
72
  return "Error: 'path' argument is required for read_file."
38
73
  path = ctx.repo / args["path"]
39
74
  if not path.exists():
40
- return f"File not found: {args['path']}"
75
+ # A small model invents misspelled/wrong-case paths ("Aki" for "Anki",
76
+ # gitHub/github/Github) and, getting a bare "not found", retries with
77
+ # ANOTHER wrong variant forever (dedup can't collapse differing typos).
78
+ # Point it at the real nearby file so it corrects instead of guessing.
79
+ hint = _suggest_path(path)
80
+ return f"File not found: {args['path']}" + (f"\n{hint}" if hint else "")
41
81
  content = path.read_text(errors="replace")
42
82
  lines = content.splitlines()
43
83
  offset = args.get("offset", 0)
@@ -0,0 +1,133 @@
1
+ """One-shot syntax validation for files the agent writes.
2
+
3
+ Poor-man's LSP. The big agents (Claude Code, opencode) run a persistent
4
+ language server and feed its diagnostics back after each edit; codex uses
5
+ verified structured patches. A persistent LSP is too heavy for localcode's
6
+ target — a 16 GB Mac already running a large local model. So instead we run a
7
+ FAST, ONE-SHOT syntax check after each write and, if it fails, append the exact
8
+ error to the tool result.
9
+
10
+ Why this matters for a LOCAL model specifically: a small quantized model drifts
11
+ into structural typos (`useState(0]`, an unterminated string, a missing bracket,
12
+ `getCardsForCard`). With no verification the model has to NOTICE its own typo —
13
+ which weak models are bad at — so it re-reads the broken file, the broken code
14
+ enters its context, and it reproduces the error (self-conditioning loop). A
15
+ deterministic check gives it GROUND TRUTH ("line 37: unexpected token") so it
16
+ fixes the exact line instead of guessing.
17
+
18
+ Best-effort and non-blocking: the write ALWAYS succeeds; this only appends a
19
+ warning. Any checker failure/absence returns None (no false alarms). Disable
20
+ with LOCALCODE_SYNTAX_CHECK=0.
21
+ """
22
+ from __future__ import annotations
23
+
24
+ import json
25
+ import os
26
+ import subprocess
27
+
28
+ _TIMEOUT = 5.0
29
+
30
+
31
+ def check_syntax(path: str, content: str) -> str | None:
32
+ """Return a one-line syntax-error string, or None if OK / not checkable.
33
+
34
+ In-process for JSON/Python (instant, zero deps); a fast one-shot subprocess
35
+ for JS (node --check) and TS/JSX (esbuild, when the project has it).
36
+ """
37
+ if os.environ.get("LOCALCODE_SYNTAX_CHECK") == "0":
38
+ return None
39
+ ext = os.path.splitext(path)[1].lower()
40
+ try:
41
+ if ext == ".json":
42
+ return _check_json(content)
43
+ if ext in (".py", ".pyi"):
44
+ return _check_python(content, path)
45
+ if ext in (".js", ".mjs", ".cjs"):
46
+ return _check_node(path)
47
+ if ext in (".ts", ".tsx", ".jsx", ".mts", ".cts"):
48
+ return _check_esbuild(path)
49
+ except Exception:
50
+ return None
51
+ return None
52
+
53
+
54
+ def _check_json(content: str) -> str | None:
55
+ try:
56
+ json.loads(content)
57
+ return None
58
+ except json.JSONDecodeError as e:
59
+ return f"JSON syntax error at line {e.lineno} col {e.colno}: {e.msg}"
60
+
61
+
62
+ def _check_python(content: str, path: str) -> str | None:
63
+ try:
64
+ compile(content, path, "exec")
65
+ return None
66
+ except SyntaxError as e:
67
+ loc = f"line {e.lineno}" + (f" col {e.offset}" if e.offset else "")
68
+ return f"Python syntax error at {loc}: {e.msg}"
69
+
70
+
71
+ def _check_node(path: str) -> str | None:
72
+ import shutil
73
+ if not shutil.which("node"):
74
+ return None
75
+ r = subprocess.run(
76
+ ["node", "--check", path],
77
+ capture_output=True, text=True, timeout=_TIMEOUT,
78
+ )
79
+ if r.returncode != 0:
80
+ return _first_error_line(r.stderr)
81
+ return None
82
+
83
+
84
+ def _find_bin(start: str, name: str) -> str | None:
85
+ """Walk up from the file's dir to find node_modules/.bin/<name>."""
86
+ d = os.path.dirname(os.path.abspath(start))
87
+ for _ in range(25):
88
+ cand = os.path.join(d, "node_modules", ".bin", name)
89
+ if os.path.exists(cand):
90
+ return cand
91
+ parent = os.path.dirname(d)
92
+ if parent == d:
93
+ break
94
+ d = parent
95
+ return None
96
+
97
+
98
+ def _check_esbuild(path: str) -> str | None:
99
+ # esbuild parses TS/TSX/JSX and reports syntax errors instantly. Only used
100
+ # when the project has it (Vite/most JS projects do post-install); if not
101
+ # present we skip rather than risk a false alarm from a wrong parser.
102
+ esbuild = _find_bin(path, "esbuild")
103
+ if not esbuild:
104
+ return None
105
+ loader = "tsx" if path.endswith((".tsx", ".jsx")) else "ts"
106
+ r = subprocess.run(
107
+ [esbuild, path, f"--loader={loader}", "--log-level=error"],
108
+ capture_output=True, text=True, timeout=_TIMEOUT,
109
+ )
110
+ if r.returncode != 0 and r.stderr.strip():
111
+ return _first_error_line(r.stderr)
112
+ return None
113
+
114
+
115
+ def _first_error_line(stderr: str) -> str | None:
116
+ """Pull the human-meaningful error out of a node/esbuild stderr dump.
117
+
118
+ Prefer the actual `SyntaxError: …` / `error: …` message, skipping node's
119
+ internal loader/stack frames (`node:internal/…`, ` at …`).
120
+ """
121
+ lines = [l.rstrip() for l in stderr.splitlines()]
122
+ # First choice: the real error message line.
123
+ for s in lines:
124
+ t = s.strip()
125
+ if t.startswith(("SyntaxError", "TypeError", "ReferenceError")) or " error:" in t.lower() or t.lower().startswith("error"):
126
+ return t[:200]
127
+ # Fallback: the "file:line" location node prints first (skip internals/stack).
128
+ for s in lines:
129
+ t = s.strip()
130
+ if not t or t.startswith(("node:internal", "at ", "^")):
131
+ continue
132
+ return t[:200]
133
+ return None
@@ -199,6 +199,18 @@ def execute(ctx: ToolContext, args: dict) -> str:
199
199
  lines = content.count("\n") + 1
200
200
  verb = "Rewrote" if existed else "Created"
201
201
 
202
+ # Deterministic syntax check (poor-man's LSP): if the just-written file has
203
+ # a syntax error, surface the EXACT error so the model fixes that line next
204
+ # round instead of re-reading the broken file and drifting. See syntax_check.
205
+ _syntax_warning = ""
206
+ try:
207
+ from .syntax_check import check_syntax
208
+ _err = check_syntax(str(path), content)
209
+ if _err:
210
+ _syntax_warning = f"\n\n⚠ SYNTAX ERROR in this file — fix it now: {_err}"
211
+ except Exception:
212
+ _syntax_warning = ""
213
+
202
214
  # Emit a unified diff for rewrites of existing files so the TUI draws the
203
215
  # diff card. New files (or unchanged rewrites) keep the concise summary —
204
216
  # a full new-file dump as "+" lines is noise, and the summary reads fine.
@@ -210,5 +222,5 @@ def execute(ctx: ToolContext, args: dict) -> str:
210
222
  fromfile=args["path"], tofile=args["path"], lineterm="",
211
223
  ))
212
224
  if diff:
213
- return "\n".join(diff[:120])
214
- return f"{verb} {args['path']} ({lines} lines)"
225
+ return "\n".join(diff[:120]) + _syntax_warning
226
+ return f"{verb} {args['path']} ({lines} lines){_syntax_warning}"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: localcode
3
- Version: 0.3.5
3
+ Version: 0.3.7
4
4
  Summary: High-performance AI coding on consumer hardware.
5
5
  Author: LocalCode contributors
6
6
  License-Expression: Apache-2.0
@@ -1,4 +1,4 @@
1
- localcode/__init__.py,sha256=1BYETer-qr6n0hQRQuRG08uTZrtp262X5k87-OyEhp0,49
1
+ localcode/__init__.py,sha256=7scw5ecMd3F-dCMkmY63HI0FKsdj4ANkgybevu64WuQ,49
2
2
  localcode/__main__.py,sha256=_4Tblte9F1KOpB-oGBQuTlaSN-IncLmhihqhBkpH3pU,69
3
3
  localcode/_subproc_env.py,sha256=eltq_kKvqGj22pIFivB5BZft9-s5fziBW0lsV9FhEV4,2383
4
4
  localcode/app.py,sha256=FSwpce6Jfn7zJmhL1ipaqTq0xKh0MxCMnEnMQuFV8L8,67820
@@ -71,12 +71,12 @@ localcode/agent/context.py,sha256=adV_WlFBHMDHsWdndRgrkZqRweDD9plyqMP_2X74WAw,37
71
71
  localcode/agent/goal.py,sha256=X87Q3rsdp70wOfkpbh35M4qFWSNyklX3RuRWjv-pCXs,6264
72
72
  localcode/agent/helpers.py,sha256=vutFyCtnca0UnF0wgk7LdsqVWdiqyYm4YnFOxtnCJ_A,15004
73
73
  localcode/agent/hooks.py,sha256=XadEbSM7gHtKlLrqSKnvcQFcoWoNUXdlMdIPS-EwBGA,15841
74
- localcode/agent/loop.py,sha256=2O2iKFsCQmr1CoQq40Ls_NeBs7tFgcDVfkU8YhqXXdM,99864
74
+ localcode/agent/loop.py,sha256=VcD7fjkoJtx8pECsL2tp-pMSom5qOkec1fy9nDgj3EE,99760
75
75
  localcode/agent/prompt_context.py,sha256=abJPLMhTzuVcsW4AfMBIW-SEqmrU69DhI-0vGZllZVU,13707
76
76
  localcode/agent/prompts.py,sha256=LCJBM2mV_SKc1OXtH0-ih9OPQnhrTh3vTlrOgLUiVDA,14845
77
- localcode/agent/recovery.py,sha256=oCghDm9BdHhvl98Kx8EikNuxSk4Geh-0cgAEhTpH-sw,16738
77
+ localcode/agent/recovery.py,sha256=-2vnYhhUGZD_reR6CaMOYNsG5XRf6YDsjTqF-1P-St4,16831
78
78
  localcode/agent/sections.py,sha256=9hnwaNritQHgn7qIGFBZ3ChZgMQXQcqtmpdbR1wafA0,8998
79
- localcode/agent/streaming.py,sha256=TEcKbmKfDGV1razrV-ceVrvIIxvS-GmLWGNDnF0gSTA,12872
79
+ localcode/agent/streaming.py,sha256=tW3LxUJlUyh3vRzeUsEo8lQcbc3DBvRtWF_2VLLQ9H8,11331
80
80
  localcode/agent/tool_execution.py,sha256=yfWZwOO3f5ICd2eqw6keg8sOKOIa3vFt4peFinmu5nM,14462
81
81
  localcode/agent/tool_orchestration.py,sha256=LVKNNHg59XLBlakz8J3zCwubXoWDiaXum8AS7HXB0sE,1711
82
82
  localcode/agent/turn_finalization.py,sha256=n5h0aA_EPDS8ZY6TBvbUo7qu2IkgEwnwcO0PKBd6930,4884
@@ -101,20 +101,21 @@ localcode/tools/append_file.py,sha256=GNjPBnnV7hpD32u0Jx6YxRmsOSr4uwvJgIcInH1qhS
101
101
  localcode/tools/base.py,sha256=2QV-q1DzXWM4KL9mFKNPzlBiLOS2uKLXhlgGyeWEKCI,2064
102
102
  localcode/tools/bash.py,sha256=Yuuxo0lsX0JuFbHQX8OflmgZgq7uqBc1FUSGJRHlsB8,43380
103
103
  localcode/tools/edit_diff.py,sha256=eJ1FyK3HecAmJNiM-XdHOQ7WCkyOuD6hwAlTVgYQGGk,3017
104
- localcode/tools/edit_file.py,sha256=DhhQSi6wnD4_IwLEYoC-zBLUPaxhihZxBatNoerFLe4,13455
104
+ localcode/tools/edit_file.py,sha256=YiUuw08kVw1qiClgsTmfi8W-RkH42cFqV9BvYfvzVcA,13750
105
105
  localcode/tools/facts.py,sha256=OqTLYOclKy6lpPQiU3aDrzY-Ho-jhWc8mvzLx6TFRCk,5576
106
106
  localcode/tools/glob_tool.py,sha256=8Gilud4U6lDZ_bASRbBeVndWzNImtf-jydNl3KQkpmY,1084
107
107
  localcode/tools/grep.py,sha256=OP2P_J8fRz3qIJWp3--t1avq2K4HXjVENsOVXJDhLEU,1695
108
108
  localcode/tools/launch_app.py,sha256=TVoq1ZQEIm2yQ607LGkJwbIC3TkvaLsBpTJgYIGX1gM,3357
109
109
  localcode/tools/list_files.py,sha256=wN2dXU01rrKB_HGh6i0GfM9qvBwx7Qoa3S5WZlArma0,1205
110
- localcode/tools/multi_edit.py,sha256=bT9OROgwTAZ1gKHE6DnpOU6_EBzLcDiisxXDnkwlyEg,4518
110
+ localcode/tools/multi_edit.py,sha256=WkGibiGkr_J-Ld4CtxR24icdy1kGQuPEh6kKDrKe8Ss,4780
111
111
  localcode/tools/plan_mode.py,sha256=2RU6QH1IJoOTKuoAr7Ga7uTaebAS8v7OFt5v1zLEXFw,2886
112
- localcode/tools/read_file.py,sha256=u8ZySkvSj-ZzPVSdi1ybZ6yTgZ-yXiqAW54ZVdZWf-g,3017
112
+ localcode/tools/read_file.py,sha256=gH852nnPiVD2Go9VcdS_EFbtnqYaGOPyAk5jo1cNbX8,4556
113
113
  localcode/tools/skill_tool.py,sha256=vTVPyRWxICtDCVYWMiAMInGnc-wGAk2L1EZn-6e79nE,1478
114
+ localcode/tools/syntax_check.py,sha256=wojE6Ke6nMe3ptyh9T7zLp5roGKYaez3TwSpnp8nE8A,4869
114
115
  localcode/tools/todo_write.py,sha256=8A4k9MzoqOtzJ6y2Gd3lEGkINcpV3JVCd2lYBmdd6qI,5801
115
116
  localcode/tools/web_fetch.py,sha256=UPhMrLipwN5xJx5-zr634gL4p6nIRqYI2_7tbUw_8Go,2770
116
117
  localcode/tools/web_search.py,sha256=ILfBBx50p00OjA939HzdDmdao4QLL8sRuxlBoUDjmC8,1122
117
- localcode/tools/write_file.py,sha256=2rfbp0j_ApHuk8fUAopMl6H1BIyEMKWKXjZ__xrY6oI,9743
118
+ localcode/tools/write_file.py,sha256=gjlNkVy9xw9q9eGWc5jY0l3iFN3zciKTztk8W25EHa0,10308
118
119
  localcode/tui/__init__.py,sha256=Yfy9bvw3l1iY4FDhRX8qZVgnZLuYiq8lc9SDYl4cWwc,61
119
120
  localcode/tui/app.py,sha256=KjQzpGKnxkgh-0kVc3zcb63Q6a5LBuGreBqTUrasiqM,17700
120
121
  localcode/tui/bridge.py,sha256=urPPCqC_nisAYawWk3juvMI8IaoJfv4g7USpGOZOqQc,2803
@@ -131,9 +132,9 @@ localcode/tui/widgets/chat_log.py,sha256=D5DUkrQXL_ilxyC8N1xIPLw4Pqeh0jSWt1mPgho
131
132
  localcode/tui/widgets/voice_visualizer.py,sha256=zOhrIxaMcg-F2Y8LvHJTddq227phmiTjtEyRbkx1XxU,4709
132
133
  localcode/tui/widgets/messages/__init__.py,sha256=952AZ1qVMGUIqPIry7mwxnYbMkgPlcY5FAwZtEa5YPg,967
133
134
  localcode/tui/widgets/messages/diff.py,sha256=khDf_MThrQz0S62-t-m7i3OU5cdNolUk1TCeKMRPDQI,10910
134
- localcode-0.3.5.dist-info/licenses/LICENSE,sha256=Ahg0cteZ-6bIRB7KC15WEtdqWnzy8HY_yzsZBoVRql0,11294
135
- localcode-0.3.5.dist-info/METADATA,sha256=F80qNJ73o16hDwmaZzV6e9B-EGuc3ARhiHgwg_P0HDo,7813
136
- localcode-0.3.5.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
137
- localcode-0.3.5.dist-info/entry_points.txt,sha256=rE7elTGUvMsBd-Quy22cuTVJ-zGVOmx0nEuC6ni9Tg4,87
138
- localcode-0.3.5.dist-info/top_level.txt,sha256=58eL3Rw8v0OGmYNxjx8uS4ERWatrzUbyCL5inlukkTo,10
139
- localcode-0.3.5.dist-info/RECORD,,
135
+ localcode-0.3.7.dist-info/licenses/LICENSE,sha256=Ahg0cteZ-6bIRB7KC15WEtdqWnzy8HY_yzsZBoVRql0,11294
136
+ localcode-0.3.7.dist-info/METADATA,sha256=Qg7wa61uxkFvD8SkVHWHQ1kEzZmnlK6H8e_SFlzm-0o,7813
137
+ localcode-0.3.7.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
138
+ localcode-0.3.7.dist-info/entry_points.txt,sha256=rE7elTGUvMsBd-Quy22cuTVJ-zGVOmx0nEuC6ni9Tg4,87
139
+ localcode-0.3.7.dist-info/top_level.txt,sha256=58eL3Rw8v0OGmYNxjx8uS4ERWatrzUbyCL5inlukkTo,10
140
+ localcode-0.3.7.dist-info/RECORD,,