localcode 0.3.6__py3-none-any.whl → 0.3.8__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 +1 -1
- localcode/agent/loop.py +54 -41
- localcode/agent/recovery.py +22 -19
- localcode/tools/project_check.py +118 -0
- localcode/tools/read_file.py +41 -1
- localcode/tools/syntax_check.py +26 -0
- {localcode-0.3.6.dist-info → localcode-0.3.8.dist-info}/METADATA +1 -1
- {localcode-0.3.6.dist-info → localcode-0.3.8.dist-info}/RECORD +12 -11
- {localcode-0.3.6.dist-info → localcode-0.3.8.dist-info}/WHEEL +0 -0
- {localcode-0.3.6.dist-info → localcode-0.3.8.dist-info}/entry_points.txt +0 -0
- {localcode-0.3.6.dist-info → localcode-0.3.8.dist-info}/licenses/LICENSE +0 -0
- {localcode-0.3.6.dist-info → localcode-0.3.8.dist-info}/top_level.txt +0 -0
localcode/__init__.py
CHANGED
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
|
|
1145
|
-
#
|
|
1146
|
-
#
|
|
1147
|
-
# (
|
|
1148
|
-
#
|
|
1149
|
-
#
|
|
1150
|
-
# (
|
|
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,
|
|
@@ -1258,24 +1240,44 @@ def run_agent_loop(
|
|
|
1258
1240
|
and not ran_build_or_test(bash_history)
|
|
1259
1241
|
):
|
|
1260
1242
|
_build_verify_nudges += 1
|
|
1243
|
+
# TIER-2 VERIFICATION: don't just NUDGE the model to typecheck
|
|
1244
|
+
# (a small model skips it or mishandles output) — RUN the
|
|
1245
|
+
# project's real typecheck/lint ourselves and feed the concrete
|
|
1246
|
+
# errors back. Catches semantic errors (wrong names, missing
|
|
1247
|
+
# imports) the per-write syntax check can't. Light: once per
|
|
1248
|
+
# turn, at completion. See tools.project_check.
|
|
1249
|
+
_proj_errors = None
|
|
1250
|
+
try:
|
|
1251
|
+
from ..tools.project_check import run_project_check
|
|
1252
|
+
out.print_info("Verifying — running the project's typecheck…")
|
|
1253
|
+
_proj_errors = run_project_check(str(app.repo_root))
|
|
1254
|
+
except Exception:
|
|
1255
|
+
_proj_errors = None
|
|
1261
1256
|
try:
|
|
1262
1257
|
from ..events import emit as _emit_bg
|
|
1263
|
-
_emit_bg("auto_nudge", signal="build_unverified",
|
|
1258
|
+
_emit_bg("auto_nudge", signal="build_unverified",
|
|
1259
|
+
round_idx=round_num, had_errors=bool(_proj_errors))
|
|
1264
1260
|
except Exception:
|
|
1265
1261
|
pass
|
|
1266
|
-
|
|
1267
|
-
"
|
|
1268
|
-
"
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1262
|
+
if _proj_errors:
|
|
1263
|
+
out.print_info("Typecheck found errors — sending them back to fix.")
|
|
1264
|
+
messages.append({"role": "user", "content": (
|
|
1265
|
+
"SYSTEM: the project's typecheck/build was run for you and "
|
|
1266
|
+
"reported errors. FIX each one (targeted edit_file changes), "
|
|
1267
|
+
"then finish. Do not claim it works until these are gone:\n\n"
|
|
1268
|
+
+ _proj_errors
|
|
1269
|
+
)})
|
|
1270
|
+
else:
|
|
1271
|
+
# No checker available, or it passed — fall back to advising.
|
|
1272
|
+
out.print_info("Changed code but never built/ran it — nudging to verify.")
|
|
1273
|
+
messages.append({"role": "user", "content": (
|
|
1274
|
+
"SYSTEM: You changed code but never built, type-checked, or ran "
|
|
1275
|
+
"it. Run the project's build/typecheck (npm run build / npx tsc "
|
|
1276
|
+
"--noEmit for TS; tests or an import smoke-check for Python), fix "
|
|
1277
|
+
"every error, then finish. Don't claim it works without building."
|
|
1278
|
+
)})
|
|
1277
1279
|
_ephemeral_nudge_indices.append(len(messages) - 1)
|
|
1278
|
-
continue # don't accept completion — let it
|
|
1280
|
+
continue # don't accept completion — let it fix first
|
|
1279
1281
|
if _goal_state.goal_type == "run_or_launch":
|
|
1280
1282
|
_task_port = int(getattr(_task_state, "active_port", 0) or 0)
|
|
1281
1283
|
content = ground_run_or_launch_text(content, _task_port)
|
|
@@ -1508,11 +1510,22 @@ def run_agent_loop(
|
|
|
1508
1510
|
"smallest targeted edit and verify it."
|
|
1509
1511
|
)
|
|
1510
1512
|
|
|
1513
|
+
# HARD rewrite-stop: the churn NUDGE (limit 3) only advises — logs
|
|
1514
|
+
# showed a model rewrite one file 16x while 25-34 nudges fired.
|
|
1515
|
+
# Past 2x the nudge limit, REJECT further full rewrites (see
|
|
1516
|
+
# recovery.rewrite_hard_stop). Key on the RAW path like the counter.
|
|
1517
|
+
_rewrite_limit_stub = None
|
|
1518
|
+
if tool_name in ("write_file", "append_file", "multi_edit") and isinstance(args, dict):
|
|
1519
|
+
_rw_path = args.get("path") or args.get("file_path") or ""
|
|
1520
|
+
_rewrite_limit_stub = rewrite_hard_stop(_rw_path, _file_write_counts)
|
|
1511
1521
|
# Execute (timed — wall-clock added to _round_tool_exec_ms
|
|
1512
1522
|
# so round_end can show the model what fraction of its
|
|
1513
1523
|
# round time was spent waiting on tools vs LLM).
|
|
1514
1524
|
_tool_started_at = time.monotonic()
|
|
1515
|
-
if
|
|
1525
|
+
if _rewrite_limit_stub is not None:
|
|
1526
|
+
from ..tools import ToolResult as _ToolResult
|
|
1527
|
+
_tool_result_obj = _ToolResult(text=_rewrite_limit_stub, ok=False, facts={"tool": tool_name, "ok": False, "repeated_failed_call": True, "rewrite_hard_stop": True})
|
|
1528
|
+
elif _edit_sequence_stub is not None:
|
|
1516
1529
|
from ..tools import ToolResult as _ToolResult
|
|
1517
1530
|
_tool_result_obj = _ToolResult(text=_edit_sequence_stub, ok=False, facts={"tool": tool_name, "ok": False, "edit_sequence": "missing_context"})
|
|
1518
1531
|
elif _oversize_stub is not None:
|
localcode/agent/recovery.py
CHANGED
|
@@ -127,25 +127,12 @@ def detect_stall(
|
|
|
127
127
|
if not content:
|
|
128
128
|
return StallMode.EMPTY
|
|
129
129
|
|
|
130
|
-
# NARRATION.
|
|
131
|
-
#
|
|
132
|
-
#
|
|
133
|
-
#
|
|
134
|
-
#
|
|
135
|
-
#
|
|
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
|
+
)
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Tier-2 verification: run the project's OWN typecheck/lint once, return errors.
|
|
2
|
+
|
|
3
|
+
The big agents catch semantic errors with a persistent language server (LSP).
|
|
4
|
+
opencode's own docs say that for many projects it's better to run the project's
|
|
5
|
+
diagnostic CLI directly — LSPs "get out of sync, use significant memory … and
|
|
6
|
+
slow down agent workflows." That's decisive for localcode: a 16 GB Mac already
|
|
7
|
+
running a large local model can't spare an always-on tsserver/pyright.
|
|
8
|
+
|
|
9
|
+
So instead we run the project's real typecheck ONCE, when the model is about to
|
|
10
|
+
finish an unverified build, and feed the concrete errors back deterministically
|
|
11
|
+
(the model can't skip it, and gets ground truth like "line 37: 'getCardsForCard'
|
|
12
|
+
does not exist" — the semantic errors a per-write syntax check can't catch).
|
|
13
|
+
|
|
14
|
+
Light: one bounded subprocess at completion, not per-edit. Returns None when the
|
|
15
|
+
project is clean or no checker is available.
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
import re
|
|
22
|
+
import shutil
|
|
23
|
+
import subprocess
|
|
24
|
+
|
|
25
|
+
_TIMEOUT = 60.0
|
|
26
|
+
_MAX_ERR_CHARS = 2500
|
|
27
|
+
_ANSI = re.compile(r"\x1b\[[0-9;]*m")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def run_project_check(repo_root: str) -> str | None:
|
|
31
|
+
"""Run the first available typecheck/lint for the project; return a bounded
|
|
32
|
+
error summary, or None if clean / nothing to run."""
|
|
33
|
+
env = {**os.environ, "NO_COLOR": "1", "FORCE_COLOR": "0"}
|
|
34
|
+
for label, argv, cwd in _detect_commands(repo_root):
|
|
35
|
+
try:
|
|
36
|
+
r = subprocess.run(
|
|
37
|
+
argv, cwd=cwd, capture_output=True, text=True,
|
|
38
|
+
timeout=_TIMEOUT, env=env,
|
|
39
|
+
)
|
|
40
|
+
except Exception:
|
|
41
|
+
continue
|
|
42
|
+
if r.returncode != 0:
|
|
43
|
+
out = _ANSI.sub("", ((r.stdout or "") + "\n" + (r.stderr or "")).strip())
|
|
44
|
+
# Keep only the most useful lines, capped, so we don't flood a
|
|
45
|
+
# small model's context with a wall of output.
|
|
46
|
+
lines = [l for l in out.splitlines() if l.strip()][:40]
|
|
47
|
+
if lines:
|
|
48
|
+
return f"[{label}] reported errors:\n" + "\n".join(lines)[:_MAX_ERR_CHARS]
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _detect_commands(repo_root: str) -> list[tuple[str, list[str], str]]:
|
|
53
|
+
"""Ordered (label, argv, cwd) checkers to try. First that runs wins."""
|
|
54
|
+
cmds: list[tuple[str, list[str], str]] = []
|
|
55
|
+
|
|
56
|
+
# ── JS / TS ── prefer a project "typecheck" script, else tsc --noEmit.
|
|
57
|
+
pj_dir = _nearest_with(repo_root, "package.json")
|
|
58
|
+
if pj_dir:
|
|
59
|
+
node_modules = os.path.join(pj_dir, "node_modules")
|
|
60
|
+
binp = os.path.join(node_modules, ".bin")
|
|
61
|
+
scripts = {}
|
|
62
|
+
try:
|
|
63
|
+
scripts = (json.load(open(os.path.join(pj_dir, "package.json"))) or {}).get("scripts", {})
|
|
64
|
+
except Exception:
|
|
65
|
+
scripts = {}
|
|
66
|
+
# Only run node-based checks once deps are installed (else every import
|
|
67
|
+
# is a false "cannot find module" error that would derail the model).
|
|
68
|
+
if os.path.isdir(node_modules):
|
|
69
|
+
if "typecheck" in scripts:
|
|
70
|
+
cmds.append(("npm run typecheck", ["npm", "run", "--silent", "typecheck"], pj_dir))
|
|
71
|
+
elif os.path.exists(os.path.join(pj_dir, "tsconfig.json")) and os.path.exists(os.path.join(binp, "tsc")):
|
|
72
|
+
cmds.append(("tsc --noEmit", [os.path.join(binp, "tsc"), "--noEmit", "--pretty", "false"], pj_dir))
|
|
73
|
+
elif "lint" in scripts:
|
|
74
|
+
cmds.append(("npm run lint", ["npm", "run", "--silent", "lint"], pj_dir))
|
|
75
|
+
|
|
76
|
+
# ── Python ── ruff for REAL errors only (E9 syntax + F pyflakes: undefined
|
|
77
|
+
# names, bad imports) — not style noise. Falls back to compileall (syntax).
|
|
78
|
+
if _has_ext(repo_root, ".py"):
|
|
79
|
+
if shutil.which("ruff"):
|
|
80
|
+
cmds.append(("ruff", ["ruff", "check", "--select", "E9,F",
|
|
81
|
+
"--no-cache", "--output-format", "concise", "."], repo_root))
|
|
82
|
+
elif shutil.which("python3") or shutil.which("python"):
|
|
83
|
+
py = shutil.which("python3") or shutil.which("python")
|
|
84
|
+
cmds.append(("python -m compileall", [py, "-m", "compileall", "-q", "."], repo_root))
|
|
85
|
+
|
|
86
|
+
# ── Go ── the compiler catches type/undefined errors (like tsc). go.mod.
|
|
87
|
+
go_dir = _nearest_with(repo_root, "go.mod")
|
|
88
|
+
if go_dir and shutil.which("go"):
|
|
89
|
+
cmds.append(("go build", ["go", "build", "./..."], go_dir))
|
|
90
|
+
|
|
91
|
+
# ── Rust ── cargo check is the type-checker (fast, no codegen). Cargo.toml.
|
|
92
|
+
rust_dir = _nearest_with(repo_root, "Cargo.toml")
|
|
93
|
+
if rust_dir and shutil.which("cargo"):
|
|
94
|
+
cmds.append(("cargo check", ["cargo", "check", "--message-format", "short"], rust_dir))
|
|
95
|
+
|
|
96
|
+
# (Interpreted langs — Ruby/PHP/shell — have no whole-project type-check;
|
|
97
|
+
# their per-file syntax linters run in the Tier-1 syntax_check on each write.)
|
|
98
|
+
return cmds
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _nearest_with(root: str, filename: str) -> str | None:
|
|
102
|
+
"""The shallowest directory under root that contains `filename`."""
|
|
103
|
+
root = os.path.abspath(root)
|
|
104
|
+
if os.path.exists(os.path.join(root, filename)):
|
|
105
|
+
return root
|
|
106
|
+
for base, dirs, files in os.walk(root):
|
|
107
|
+
dirs[:] = [d for d in dirs if d not in (".git", "node_modules", "dist", "build", ".venv")]
|
|
108
|
+
if filename in files:
|
|
109
|
+
return base
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _has_ext(root: str, ext: str) -> bool:
|
|
114
|
+
for base, dirs, files in os.walk(os.path.abspath(root)):
|
|
115
|
+
dirs[:] = [d for d in dirs if d not in (".git", "node_modules", "dist", "build", ".venv")]
|
|
116
|
+
if any(f.endswith(ext) for f in files):
|
|
117
|
+
return True
|
|
118
|
+
return False
|
localcode/tools/read_file.py
CHANGED
|
@@ -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
|
-
|
|
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)
|
localcode/tools/syntax_check.py
CHANGED
|
@@ -46,11 +46,37 @@ def check_syntax(path: str, content: str) -> str | None:
|
|
|
46
46
|
return _check_node(path)
|
|
47
47
|
if ext in (".ts", ".tsx", ".jsx", ".mts", ".cts"):
|
|
48
48
|
return _check_esbuild(path)
|
|
49
|
+
# Per-file syntax linters for other languages (best-effort — only if the
|
|
50
|
+
# interpreter/tool is on PATH). Whole-project TYPE checks (go/rust/tsc)
|
|
51
|
+
# run separately at completion in project_check.
|
|
52
|
+
if ext == ".rb":
|
|
53
|
+
return _check_cli(["ruby", "-wc", path], "ruby")
|
|
54
|
+
if ext == ".php":
|
|
55
|
+
return _check_cli(["php", "-l", path], "php")
|
|
56
|
+
if ext in (".sh", ".bash"):
|
|
57
|
+
return _check_cli(["bash", "-n", path], "bash")
|
|
58
|
+
if ext == ".lua":
|
|
59
|
+
return _check_cli(["luac", "-p", path], "luac")
|
|
60
|
+
if ext == ".rs":
|
|
61
|
+
# rustc syntax-only parse (no full build): --emit=metadata is slow;
|
|
62
|
+
# a lightweight parse-only pass isn't stable, so skip unless a fast
|
|
63
|
+
# tool exists. cargo check at completion covers Rust type errors.
|
|
64
|
+
return None
|
|
49
65
|
except Exception:
|
|
50
66
|
return None
|
|
51
67
|
return None
|
|
52
68
|
|
|
53
69
|
|
|
70
|
+
def _check_cli(argv: list, tool: str) -> str | None:
|
|
71
|
+
import shutil
|
|
72
|
+
if not shutil.which(tool):
|
|
73
|
+
return None
|
|
74
|
+
r = subprocess.run(argv, capture_output=True, text=True, timeout=_TIMEOUT)
|
|
75
|
+
if r.returncode != 0:
|
|
76
|
+
return _first_error_line((r.stderr or "") + "\n" + (r.stdout or ""))
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
|
|
54
80
|
def _check_json(content: str) -> str | None:
|
|
55
81
|
try:
|
|
56
82
|
json.loads(content)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
localcode/__init__.py,sha256=
|
|
1
|
+
localcode/__init__.py,sha256=_F9ixyUc6moIe8UvI5HG2cdeyBN6Z_4qdEM2TlPlas8,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,10 +71,10 @@ 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=
|
|
74
|
+
localcode/agent/loop.py,sha256=Gp4Zt0fRpQkE1YLZjrF9xtzM4qlJodyFeGGupx_U16k,101105
|
|
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
|
|
77
|
+
localcode/agent/recovery.py,sha256=-2vnYhhUGZD_reR6CaMOYNsG5XRf6YDsjTqF-1P-St4,16831
|
|
78
78
|
localcode/agent/sections.py,sha256=9hnwaNritQHgn7qIGFBZ3ChZgMQXQcqtmpdbR1wafA0,8998
|
|
79
79
|
localcode/agent/streaming.py,sha256=tW3LxUJlUyh3vRzeUsEo8lQcbc3DBvRtWF_2VLLQ9H8,11331
|
|
80
80
|
localcode/agent/tool_execution.py,sha256=yfWZwOO3f5ICd2eqw6keg8sOKOIa3vFt4peFinmu5nM,14462
|
|
@@ -109,9 +109,10 @@ localcode/tools/launch_app.py,sha256=TVoq1ZQEIm2yQ607LGkJwbIC3TkvaLsBpTJgYIGX1gM
|
|
|
109
109
|
localcode/tools/list_files.py,sha256=wN2dXU01rrKB_HGh6i0GfM9qvBwx7Qoa3S5WZlArma0,1205
|
|
110
110
|
localcode/tools/multi_edit.py,sha256=WkGibiGkr_J-Ld4CtxR24icdy1kGQuPEh6kKDrKe8Ss,4780
|
|
111
111
|
localcode/tools/plan_mode.py,sha256=2RU6QH1IJoOTKuoAr7Ga7uTaebAS8v7OFt5v1zLEXFw,2886
|
|
112
|
-
localcode/tools/
|
|
112
|
+
localcode/tools/project_check.py,sha256=B3Q6IB2XfllW3Nlxwz2pCCQwF_b0VSUBnrpCHS4eOE8,5508
|
|
113
|
+
localcode/tools/read_file.py,sha256=gH852nnPiVD2Go9VcdS_EFbtnqYaGOPyAk5jo1cNbX8,4556
|
|
113
114
|
localcode/tools/skill_tool.py,sha256=vTVPyRWxICtDCVYWMiAMInGnc-wGAk2L1EZn-6e79nE,1478
|
|
114
|
-
localcode/tools/syntax_check.py,sha256=
|
|
115
|
+
localcode/tools/syntax_check.py,sha256=U9niGqS4Ht0_6TyZKEwKBpnoXKatQwmSBtfMdekikNM,6050
|
|
115
116
|
localcode/tools/todo_write.py,sha256=8A4k9MzoqOtzJ6y2Gd3lEGkINcpV3JVCd2lYBmdd6qI,5801
|
|
116
117
|
localcode/tools/web_fetch.py,sha256=UPhMrLipwN5xJx5-zr634gL4p6nIRqYI2_7tbUw_8Go,2770
|
|
117
118
|
localcode/tools/web_search.py,sha256=ILfBBx50p00OjA939HzdDmdao4QLL8sRuxlBoUDjmC8,1122
|
|
@@ -132,9 +133,9 @@ localcode/tui/widgets/chat_log.py,sha256=D5DUkrQXL_ilxyC8N1xIPLw4Pqeh0jSWt1mPgho
|
|
|
132
133
|
localcode/tui/widgets/voice_visualizer.py,sha256=zOhrIxaMcg-F2Y8LvHJTddq227phmiTjtEyRbkx1XxU,4709
|
|
133
134
|
localcode/tui/widgets/messages/__init__.py,sha256=952AZ1qVMGUIqPIry7mwxnYbMkgPlcY5FAwZtEa5YPg,967
|
|
134
135
|
localcode/tui/widgets/messages/diff.py,sha256=khDf_MThrQz0S62-t-m7i3OU5cdNolUk1TCeKMRPDQI,10910
|
|
135
|
-
localcode-0.3.
|
|
136
|
-
localcode-0.3.
|
|
137
|
-
localcode-0.3.
|
|
138
|
-
localcode-0.3.
|
|
139
|
-
localcode-0.3.
|
|
140
|
-
localcode-0.3.
|
|
136
|
+
localcode-0.3.8.dist-info/licenses/LICENSE,sha256=Ahg0cteZ-6bIRB7KC15WEtdqWnzy8HY_yzsZBoVRql0,11294
|
|
137
|
+
localcode-0.3.8.dist-info/METADATA,sha256=-O_cA0e66WTvuIJyuJEQG6Z2ummHGDjFwe0atQyQRsc,7813
|
|
138
|
+
localcode-0.3.8.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
139
|
+
localcode-0.3.8.dist-info/entry_points.txt,sha256=rE7elTGUvMsBd-Quy22cuTVJ-zGVOmx0nEuC6ni9Tg4,87
|
|
140
|
+
localcode-0.3.8.dist-info/top_level.txt,sha256=58eL3Rw8v0OGmYNxjx8uS4ERWatrzUbyCL5inlukkTo,10
|
|
141
|
+
localcode-0.3.8.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|