hexcli 2.17.0__tar.gz → 2.18.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.17.0 → hexcli-2.18.0}/CHANGELOG.md +32 -0
- {hexcli-2.17.0 → hexcli-2.18.0}/PKG-INFO +1 -1
- {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/__init__.py +1 -1
- {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/editing.py +40 -0
- {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/tools.py +1 -1
- {hexcli-2.17.0 → hexcli-2.18.0}/.gitignore +0 -0
- {hexcli-2.17.0 → hexcli-2.18.0}/Hex CLI.cmd +0 -0
- {hexcli-2.17.0 → hexcli-2.18.0}/LICENSE +0 -0
- {hexcli-2.17.0 → hexcli-2.18.0}/README.md +0 -0
- {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/agent.py +0 -0
- {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/assets/hexcli.ico +0 -0
- {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/assets/hexcli.png +0 -0
- {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/cancel.py +0 -0
- {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/chatlog.py +0 -0
- {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/commands.py +0 -0
- {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/compaction.py +0 -0
- {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/config.py +0 -0
- {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/diffview.py +0 -0
- {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/distribution.py +0 -0
- {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/doctor.py +0 -0
- {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/http_client.py +0 -0
- {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/launcher.py +0 -0
- {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/lineedit.py +0 -0
- {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/llm.py +0 -0
- {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/lockfile.py +0 -0
- {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/markdown_stream.py +0 -0
- {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/memory.py +0 -0
- {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/network.py +0 -0
- {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/parsing.py +0 -0
- {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/paths.py +0 -0
- {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/prompts.py +0 -0
- {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/psparse.py +0 -0
- {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/repl.py +0 -0
- {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/safety.py +0 -0
- {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/sessions.py +0 -0
- {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/setup_wizard.py +0 -0
- {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/statusbar.py +0 -0
- {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/stream_render.py +0 -0
- {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/telemetry.py +0 -0
- {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/ui.py +0 -0
- {hexcli-2.17.0 → hexcli-2.18.0}/install.ps1 +0 -0
- {hexcli-2.17.0 → hexcli-2.18.0}/launcher.py +0 -0
- {hexcli-2.17.0 → hexcli-2.18.0}/pyproject.toml +0 -0
- {hexcli-2.17.0 → hexcli-2.18.0}/shellai.example.json +0 -0
|
@@ -4,6 +4,38 @@ 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.18.0 — 2026-09-18
|
|
8
|
+
|
|
9
|
+
A minor release: what `write_file` writes for one shape of body changes,
|
|
10
|
+
so the run that follows it changes.
|
|
11
|
+
|
|
12
|
+
> **Gate: PASS** on the pinned 24-case set after rechecks. Own arm, fresh
|
|
13
|
+
> server, seed 20260918, 2 invalid of 245 runs. `agentic-5` and
|
|
14
|
+
> `error-recovery-3` (a counting answer and a denied search, no write in
|
|
15
|
+
> either) missed once at 5 runs; `agentic-5` held 6/6, `error-recovery-3`
|
|
16
|
+
> read 3/4 with two invalid runs on a server 70 minutes into its traffic and
|
|
17
|
+
> 6/6 on a fresh one. Against the v2.11.1 12-run baseline: run-level
|
|
18
|
+
> 402/514 vs 182/228, +1.6 %, Fisher p = 0.70; pass^k 26 → 29 of 46 shared
|
|
19
|
+
> cases, McNemar p = 0.51. The unescape fired on three writes in the arm
|
|
20
|
+
> (a `trap-1` poem and two `runit-1` bodies, all of which decode the same
|
|
21
|
+
> under both rules), so the arm is evidence of no regression and the effect
|
|
22
|
+
> rests on the fixture. Smoke 10/10 on a fresh server.
|
|
23
|
+
|
|
24
|
+
- A double-escaped Python body is decoded the way that parses. The model
|
|
25
|
+
writes its line breaks as literal `\n` when it escapes a reply twice, and
|
|
26
|
+
the `\n` it means as a string escape inside the code looks exactly the
|
|
27
|
+
same: `print(\"\nWelcome\")` and the line break after it are the same two
|
|
28
|
+
characters. The 2.9.0 rule decoded all of them, so on 2026-09-18 the owner
|
|
29
|
+
got a file that read `print("` on one line and `Welcome to the Calculator
|
|
30
|
+
App!")` on the next, and the model spent the rest of the session not
|
|
31
|
+
fixing it. For a `.py` path the body is now decoded in full and parsed; if
|
|
32
|
+
that fails, every `\n` except one sitting right after an opening quote or
|
|
33
|
+
right before a closing one is decoded and the result parsed again, and
|
|
34
|
+
whichever parses is written (the full decode when neither does, as
|
|
35
|
+
before). Other file types are unchanged. Of the 511 `write_file` bodies on
|
|
36
|
+
record five are double-escaped, two have a quote-adjacent `\n`, and one of
|
|
37
|
+
those, the session above, parses only this way.
|
|
38
|
+
|
|
7
39
|
## 2.17.0 — 2026-09-18
|
|
8
40
|
|
|
9
41
|
A minor release: the parser accepts a reply it used to send back for a
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: hexcli
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.18.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
|
|
@@ -12,6 +12,7 @@ audit (2026-07) through the merge tier (2026-09-13).
|
|
|
12
12
|
"""
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
|
|
15
|
+
import ast
|
|
15
16
|
import difflib
|
|
16
17
|
import re
|
|
17
18
|
|
|
@@ -140,6 +141,45 @@ def unescape_json(text: str) -> str:
|
|
|
140
141
|
return _unescape_json(text)
|
|
141
142
|
|
|
142
143
|
|
|
144
|
+
_QUOTE_ADJACENT_NL = re.compile(r'(["\'])\\n|\\n(["\'])')
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def unescape_body(text: str, path: str) -> str:
|
|
148
|
+
"""Decode a double-escaped file body, keeping a Python source parseable.
|
|
149
|
+
|
|
150
|
+
A body the model escaped twice carries its line breaks as literal \\n --
|
|
151
|
+
and so does every \\n it meant as a string escape inside the code:
|
|
152
|
+
`print(\\"\\nWelcome\\")` and the line break after it are the same two
|
|
153
|
+
characters. Decoding all of them (the 2.9.0 rule) turns the escape into a
|
|
154
|
+
real newline inside the string literal, and the file the owner got on
|
|
155
|
+
2026-09-18 read `print("` / `Welcome to the Calculator App!")` -- a
|
|
156
|
+
SyntaxError the model then spent eleven steps not fixing.
|
|
157
|
+
|
|
158
|
+
For a .py path, decode everything and parse; if that fails, decode
|
|
159
|
+
everything EXCEPT a \\n that sits right after an opening quote or right
|
|
160
|
+
before a closing one, and parse again; keep whichever parses, and the
|
|
161
|
+
full decode when neither does (the caller's checker will say so). Other
|
|
162
|
+
file types get the full decode as before. Measured over the 511
|
|
163
|
+
write_file bodies on record: five are double-escaped, two of those have
|
|
164
|
+
a quote-adjacent \\n, and one of the two (the session above) parses only
|
|
165
|
+
this way."""
|
|
166
|
+
full = _unescape_json(text)
|
|
167
|
+
if not path.lower().endswith(".py"):
|
|
168
|
+
return full
|
|
169
|
+
try:
|
|
170
|
+
ast.parse(full)
|
|
171
|
+
return full
|
|
172
|
+
except SyntaxError:
|
|
173
|
+
pass
|
|
174
|
+
kept = _QUOTE_ADJACENT_NL.sub(lambda m: (m.group(1) or "") + "\x00" + (m.group(2) or ""), text)
|
|
175
|
+
candidate = _unescape_json(kept).replace("\x00", "\\n")
|
|
176
|
+
try:
|
|
177
|
+
ast.parse(candidate)
|
|
178
|
+
return candidate
|
|
179
|
+
except SyntaxError:
|
|
180
|
+
return full
|
|
181
|
+
|
|
182
|
+
|
|
143
183
|
_DELTA_TOKEN_RE = re.compile(r"\w+|\s+|[^\w\s]")
|
|
144
184
|
_ESCAPED_RE = re.compile(r'\\["nt\\]')
|
|
145
185
|
_BARE_QUOTE_RE = re.compile(r'(?<!\\)"')
|
|
@@ -461,7 +461,7 @@ def write_file_tool(path_text: str, content: str) -> str:
|
|
|
461
461
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
462
462
|
note = ""
|
|
463
463
|
if p2.looks_double_escaped(content):
|
|
464
|
-
content = p2.
|
|
464
|
+
content = p2.unescape_body(content, str(path))
|
|
465
465
|
note = ", unescaped"
|
|
466
466
|
tmp = path.parent / (path.name + ".tmp")
|
|
467
467
|
tmp.write_text(content, encoding="utf-8")
|
|
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
|