hexcli 2.15.0__tar.gz → 2.17.0__tar.gz
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.
- {hexcli-2.15.0 → hexcli-2.17.0}/CHANGELOG.md +62 -0
- {hexcli-2.15.0 → hexcli-2.17.0}/PKG-INFO +1 -1
- {hexcli-2.15.0 → hexcli-2.17.0}/hexcli/__init__.py +1 -1
- {hexcli-2.15.0 → hexcli-2.17.0}/hexcli/agent.py +6 -1
- {hexcli-2.15.0 → hexcli-2.17.0}/hexcli/parsing.py +49 -4
- {hexcli-2.15.0 → hexcli-2.17.0}/.gitignore +0 -0
- {hexcli-2.15.0 → hexcli-2.17.0}/Hex CLI.cmd +0 -0
- {hexcli-2.15.0 → hexcli-2.17.0}/LICENSE +0 -0
- {hexcli-2.15.0 → hexcli-2.17.0}/README.md +0 -0
- {hexcli-2.15.0 → hexcli-2.17.0}/hexcli/assets/hexcli.ico +0 -0
- {hexcli-2.15.0 → hexcli-2.17.0}/hexcli/assets/hexcli.png +0 -0
- {hexcli-2.15.0 → hexcli-2.17.0}/hexcli/cancel.py +0 -0
- {hexcli-2.15.0 → hexcli-2.17.0}/hexcli/chatlog.py +0 -0
- {hexcli-2.15.0 → hexcli-2.17.0}/hexcli/commands.py +0 -0
- {hexcli-2.15.0 → hexcli-2.17.0}/hexcli/compaction.py +0 -0
- {hexcli-2.15.0 → hexcli-2.17.0}/hexcli/config.py +0 -0
- {hexcli-2.15.0 → hexcli-2.17.0}/hexcli/diffview.py +0 -0
- {hexcli-2.15.0 → hexcli-2.17.0}/hexcli/distribution.py +0 -0
- {hexcli-2.15.0 → hexcli-2.17.0}/hexcli/doctor.py +0 -0
- {hexcli-2.15.0 → hexcli-2.17.0}/hexcli/editing.py +0 -0
- {hexcli-2.15.0 → hexcli-2.17.0}/hexcli/http_client.py +0 -0
- {hexcli-2.15.0 → hexcli-2.17.0}/hexcli/launcher.py +0 -0
- {hexcli-2.15.0 → hexcli-2.17.0}/hexcli/lineedit.py +0 -0
- {hexcli-2.15.0 → hexcli-2.17.0}/hexcli/llm.py +0 -0
- {hexcli-2.15.0 → hexcli-2.17.0}/hexcli/lockfile.py +0 -0
- {hexcli-2.15.0 → hexcli-2.17.0}/hexcli/markdown_stream.py +0 -0
- {hexcli-2.15.0 → hexcli-2.17.0}/hexcli/memory.py +0 -0
- {hexcli-2.15.0 → hexcli-2.17.0}/hexcli/network.py +0 -0
- {hexcli-2.15.0 → hexcli-2.17.0}/hexcli/paths.py +0 -0
- {hexcli-2.15.0 → hexcli-2.17.0}/hexcli/prompts.py +0 -0
- {hexcli-2.15.0 → hexcli-2.17.0}/hexcli/psparse.py +0 -0
- {hexcli-2.15.0 → hexcli-2.17.0}/hexcli/repl.py +0 -0
- {hexcli-2.15.0 → hexcli-2.17.0}/hexcli/safety.py +0 -0
- {hexcli-2.15.0 → hexcli-2.17.0}/hexcli/sessions.py +0 -0
- {hexcli-2.15.0 → hexcli-2.17.0}/hexcli/setup_wizard.py +0 -0
- {hexcli-2.15.0 → hexcli-2.17.0}/hexcli/statusbar.py +0 -0
- {hexcli-2.15.0 → hexcli-2.17.0}/hexcli/stream_render.py +0 -0
- {hexcli-2.15.0 → hexcli-2.17.0}/hexcli/telemetry.py +0 -0
- {hexcli-2.15.0 → hexcli-2.17.0}/hexcli/tools.py +0 -0
- {hexcli-2.15.0 → hexcli-2.17.0}/hexcli/ui.py +0 -0
- {hexcli-2.15.0 → hexcli-2.17.0}/install.ps1 +0 -0
- {hexcli-2.15.0 → hexcli-2.17.0}/launcher.py +0 -0
- {hexcli-2.15.0 → hexcli-2.17.0}/pyproject.toml +0 -0
- {hexcli-2.15.0 → hexcli-2.17.0}/shellai.example.json +0 -0
|
@@ -4,6 +4,68 @@ Full evidence for every claim below — including the experiments that failed
|
|
|
4
4
|
lives in `docs/V2_PLAN.md` §14. Numbers are pass^k over repeated live runs on
|
|
5
5
|
the Hexagon NPU, not single-run anecdotes.
|
|
6
6
|
|
|
7
|
+
## 2.17.0 — 2026-09-18
|
|
8
|
+
|
|
9
|
+
A minor release: the parser accepts a reply it used to send back for a
|
|
10
|
+
retry, so what the model sees after such a reply changes.
|
|
11
|
+
|
|
12
|
+
> **Gate: PASS** on the pinned 24-case set, all 24 held at 5 runs with no
|
|
13
|
+
> recheck. Own arm, fresh server, seed 20260918, 6 invalid of 245 runs.
|
|
14
|
+
> Against the v2.11.1 12-run baseline: run-level 402/514 vs 174/224, −0.5 %,
|
|
15
|
+
> Fisher p = 0.92; pass^k 26 → 31 of 46 shared cases, McNemar p = 0.13. The
|
|
16
|
+
> close fired on five replies in the arm (`claims-1`, `claims-2`, two in
|
|
17
|
+
> `tests-claim-1`, one in `trap-3`); `claims-1` 3/5 against 3/11, `trap-3`
|
|
18
|
+
> 0/5 as before. Smoke 9/10 on a fresh server; the miss was `factual-1`,
|
|
19
|
+
> re-measured 7/10 against its recorded 79 % (p = 0.67).
|
|
20
|
+
|
|
21
|
+
- A reply that stops before its last closing brace is closed and decoded.
|
|
22
|
+
In the owner's 2026-09-18 session two `write_file` replies, 2,239 and 782
|
|
23
|
+
characters, ended at `"}` with the outer object never closed. The parser's
|
|
24
|
+
stray-quote repair took the decoder's "Expecting ',' delimiter" at the end
|
|
25
|
+
of the text as a quote that had ended a string early, escaped the
|
|
26
|
+
content's own closing quote, and the decoder then reported an unterminated
|
|
27
|
+
string; the retry feedback told the model to fix its quoting, the model
|
|
28
|
+
escaped everything twice, and the file it wrote had `\n` and `\"` in it as
|
|
29
|
+
text. Every step after that in the session was the model failing to repair
|
|
30
|
+
that file. When the decoder's error sits at the very end of the reply,
|
|
31
|
+
every string is closed and the brace depth is still positive, the missing
|
|
32
|
+
braces are appended and the object decodes. Measured over the 10,772
|
|
33
|
+
replies on record: 185 fail to decode, and 27 of them now decode (all one
|
|
34
|
+
brace short; 23 in `claims-1`, `claims-2` and `tests-claim-1`, four in the
|
|
35
|
+
owner's sessions). The close is only tried on the reply as sent: three
|
|
36
|
+
more replies are a brace short *after* a stray quote, and a reply whose
|
|
37
|
+
content string was cut off and then followed by a second object would
|
|
38
|
+
otherwise be glued into one, so those stay retries. A reply cut off inside
|
|
39
|
+
a string is still reported as cut off, and a stray quote in the middle is
|
|
40
|
+
still repaired as before; the other 158 undecodable replies are unchanged.
|
|
41
|
+
|
|
42
|
+
## 2.16.0 — 2026-09-18
|
|
43
|
+
|
|
44
|
+
A minor release: a tool result the model reads changes (`find_files` returns
|
|
45
|
+
a listing where it returned an argument error).
|
|
46
|
+
|
|
47
|
+
> **Gate: PASS** on the pinned 24-case set after one recheck (`agentic-4`
|
|
48
|
+
> and `agentic-5`, neither of which calls `find_files`, missed once at 5
|
|
49
|
+
> runs and then held at 6). Own arm, fresh server, seed 20260918, 8 invalid
|
|
50
|
+
> of 245 runs. Against the v2.11.1 12-run baseline: run-level 402/514 vs
|
|
51
|
+
> 174/222, +0.2 %, Fisher p = 1.00; pass^k 26 → 30 of 46 shared cases,
|
|
52
|
+
> McNemar p = 0.29. **The alias did not fire in this arm**: all ten
|
|
53
|
+
> `find_files` calls carried `glob` (two also sent `pattern`, and `glob`
|
|
54
|
+
> won as designed), so the arm is evidence of no regression, and the case
|
|
55
|
+
> for the change rests on the 21 of 170 recorded calls that would have
|
|
56
|
+
> hit it. `findfile-1` 2/5, all three misses the decoy answer, which is
|
|
57
|
+
> the separate finding still open. Smoke 10/10 on a fresh server.
|
|
58
|
+
|
|
59
|
+
- `find_files` takes `pattern` as its glob when no `glob` is sent. The
|
|
60
|
+
model names the file under the key `search_files` uses in 21 of the 170
|
|
61
|
+
`find_files` calls on record, across eight cases and one of the owner's
|
|
62
|
+
sessions, and each one came back "find_files requires 'glob'". In the
|
|
63
|
+
2.12.0 arm three of `findfile-1`'s twelve runs spent two to three steps
|
|
64
|
+
on it and one reached the step limit that way. The value is what the
|
|
65
|
+
model meant to look for either way, and a bare name is a valid glob for
|
|
66
|
+
that exact name; `glob` still wins when both are sent, and a call with
|
|
67
|
+
neither is still an error.
|
|
68
|
+
|
|
7
69
|
## 2.15.0 — 2026-09-18
|
|
8
70
|
|
|
9
71
|
A minor release: one tool error the model reads is new (`edit_file` on
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: hexcli
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.17.0
|
|
4
4
|
Summary: Local Hexagon NPU terminal agent for Snapdragon X Elite Windows ARM64
|
|
5
5
|
Project-URL: Homepage, https://github.com/NathanL15/Hex-CLI
|
|
6
6
|
Project-URL: Repository, https://github.com/NathanL15/Hex-CLI
|
|
@@ -826,7 +826,12 @@ def execute_tool_call(config: dict[str, Any], action: dict[str, Any], shell_exe:
|
|
|
826
826
|
raise RuntimeError("search_files requires 'pattern'.")
|
|
827
827
|
return search_files_tool(pattern, path, glob_pat, limit)
|
|
828
828
|
if tool == "find_files":
|
|
829
|
-
|
|
829
|
+
# 21 of the 170 find_files calls on record (2026-09-17) sent the
|
|
830
|
+
# name under "pattern", the key search_files uses, and got "requires
|
|
831
|
+
# 'glob'" -- three of findfile-1's twelve runs burned steps on it,
|
|
832
|
+
# one to the step limit. The value is what the model meant to look
|
|
833
|
+
# for either way; a bare name is a valid glob for that exact name.
|
|
834
|
+
glob_pat = str(args.get("glob") or args.get("pattern") or "").strip()
|
|
830
835
|
path = str(args.get("path") or ".").strip() or "."
|
|
831
836
|
if not glob_pat:
|
|
832
837
|
raise RuntimeError("find_files requires 'glob'.")
|
|
@@ -126,6 +126,49 @@ def _repair_stray_quote(text: str, err: json.JSONDecodeError) -> str | None:
|
|
|
126
126
|
return text[:q] + "\\" + text[q:]
|
|
127
127
|
|
|
128
128
|
|
|
129
|
+
def _close_unfinished_object(text: str, start: int, err: json.JSONDecodeError) -> str | None:
|
|
130
|
+
"""The reply ended before its last closing brace(s): `{"action":"write_file",
|
|
131
|
+
"args":{...,"content":"..."}` and then nothing. The decoder reports
|
|
132
|
+
"Expecting ',' delimiter" AT THE END of the text; every string is closed
|
|
133
|
+
and the brace depth is still positive. Appending the missing braces is
|
|
134
|
+
the whole repair. 30 of the 185 undecodable replies on record are this
|
|
135
|
+
shape, all one brace short, four of them in the owner's sessions
|
|
136
|
+
(2026-09-13 and 2026-09-18), and the stray-quote repair below used to
|
|
137
|
+
make them worse: it escaped the content's own closing quote, the decoder
|
|
138
|
+
then said "Unterminated string", and the retry feedback told the model
|
|
139
|
+
to fix its quoting -- which it did by escaping everything twice, and the
|
|
140
|
+
file it then wrote had backslashes in it. None when the error is not at
|
|
141
|
+
the end, a string is still open, or the braces already balance."""
|
|
142
|
+
if err.pos < len(text.rstrip()):
|
|
143
|
+
return None
|
|
144
|
+
depth, in_string, escaped = 0, False, False
|
|
145
|
+
for ch in text[start:]:
|
|
146
|
+
if in_string:
|
|
147
|
+
if escaped:
|
|
148
|
+
escaped = False
|
|
149
|
+
elif ch == "\\":
|
|
150
|
+
escaped = True
|
|
151
|
+
elif ch == '"':
|
|
152
|
+
in_string = False
|
|
153
|
+
elif ch == '"':
|
|
154
|
+
in_string = True
|
|
155
|
+
elif ch == "{":
|
|
156
|
+
depth += 1
|
|
157
|
+
elif ch == "}":
|
|
158
|
+
depth -= 1
|
|
159
|
+
if in_string or depth <= 0:
|
|
160
|
+
return None
|
|
161
|
+
return text.rstrip() + "}" * depth
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
# Only the reply AS SENT is closed this way. Once a stray-quote repair has
|
|
165
|
+
# changed the text, a positive depth at the end can be the repair's own
|
|
166
|
+
# doing (a write whose content string was cut off, followed by a second
|
|
167
|
+
# object: the repairs walk the quotes back until the text "balances" and
|
|
168
|
+
# the close would then glue the two objects into one), and that reply must
|
|
169
|
+
# stay a retry -- evals/test_backports.py pins it.
|
|
170
|
+
|
|
171
|
+
|
|
129
172
|
def _loads_object(text: str) -> dict[str, Any] | None:
|
|
130
173
|
"""The first JSON object in `text`, decoded from its first brace and
|
|
131
174
|
ignoring whatever follows it (a second batched action, prose). Stray
|
|
@@ -133,12 +176,13 @@ def _loads_object(text: str) -> dict[str, Any] | None:
|
|
|
133
176
|
start = text.find("{")
|
|
134
177
|
if start < 0:
|
|
135
178
|
return None
|
|
136
|
-
for
|
|
179
|
+
for attempt in range(_MAX_QUOTE_REPAIRS + 1):
|
|
137
180
|
try:
|
|
138
181
|
parsed, _end = _DECODER.raw_decode(text, start)
|
|
139
182
|
return parsed if isinstance(parsed, dict) else None
|
|
140
183
|
except json.JSONDecodeError as err:
|
|
141
|
-
fixed =
|
|
184
|
+
fixed = (_close_unfinished_object(text, start, err) if attempt == 0 else None) \
|
|
185
|
+
or _repair_stray_quote(text, err)
|
|
142
186
|
if fixed is None:
|
|
143
187
|
return None
|
|
144
188
|
text = fixed
|
|
@@ -155,13 +199,14 @@ def _decode_failure(text: str) -> tuple[json.JSONDecodeError | None, str]:
|
|
|
155
199
|
if start < 0:
|
|
156
200
|
return None, text
|
|
157
201
|
last: json.JSONDecodeError | None = None
|
|
158
|
-
for
|
|
202
|
+
for attempt in range(_MAX_QUOTE_REPAIRS + 1):
|
|
159
203
|
try:
|
|
160
204
|
_DECODER.raw_decode(text, start)
|
|
161
205
|
return None, text
|
|
162
206
|
except json.JSONDecodeError as err:
|
|
163
207
|
last = err
|
|
164
|
-
fixed =
|
|
208
|
+
fixed = (_close_unfinished_object(text, start, err) if attempt == 0 else None) \
|
|
209
|
+
or _repair_stray_quote(text, err)
|
|
165
210
|
if fixed is None:
|
|
166
211
|
return err, text
|
|
167
212
|
text = fixed
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|