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.
Files changed (44) hide show
  1. {hexcli-2.17.0 → hexcli-2.18.0}/CHANGELOG.md +32 -0
  2. {hexcli-2.17.0 → hexcli-2.18.0}/PKG-INFO +1 -1
  3. {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/__init__.py +1 -1
  4. {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/editing.py +40 -0
  5. {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/tools.py +1 -1
  6. {hexcli-2.17.0 → hexcli-2.18.0}/.gitignore +0 -0
  7. {hexcli-2.17.0 → hexcli-2.18.0}/Hex CLI.cmd +0 -0
  8. {hexcli-2.17.0 → hexcli-2.18.0}/LICENSE +0 -0
  9. {hexcli-2.17.0 → hexcli-2.18.0}/README.md +0 -0
  10. {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/agent.py +0 -0
  11. {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/assets/hexcli.ico +0 -0
  12. {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/assets/hexcli.png +0 -0
  13. {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/cancel.py +0 -0
  14. {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/chatlog.py +0 -0
  15. {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/commands.py +0 -0
  16. {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/compaction.py +0 -0
  17. {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/config.py +0 -0
  18. {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/diffview.py +0 -0
  19. {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/distribution.py +0 -0
  20. {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/doctor.py +0 -0
  21. {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/http_client.py +0 -0
  22. {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/launcher.py +0 -0
  23. {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/lineedit.py +0 -0
  24. {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/llm.py +0 -0
  25. {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/lockfile.py +0 -0
  26. {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/markdown_stream.py +0 -0
  27. {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/memory.py +0 -0
  28. {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/network.py +0 -0
  29. {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/parsing.py +0 -0
  30. {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/paths.py +0 -0
  31. {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/prompts.py +0 -0
  32. {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/psparse.py +0 -0
  33. {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/repl.py +0 -0
  34. {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/safety.py +0 -0
  35. {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/sessions.py +0 -0
  36. {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/setup_wizard.py +0 -0
  37. {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/statusbar.py +0 -0
  38. {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/stream_render.py +0 -0
  39. {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/telemetry.py +0 -0
  40. {hexcli-2.17.0 → hexcli-2.18.0}/hexcli/ui.py +0 -0
  41. {hexcli-2.17.0 → hexcli-2.18.0}/install.ps1 +0 -0
  42. {hexcli-2.17.0 → hexcli-2.18.0}/launcher.py +0 -0
  43. {hexcli-2.17.0 → hexcli-2.18.0}/pyproject.toml +0 -0
  44. {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.17.0
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
@@ -3,4 +3,4 @@
3
3
  # The one place the version is written. pyproject.toml reads it (hatch
4
4
  # dynamic version), agent.VERSION re-exports it, and CI refuses a release
5
5
  # tag that does not match it.
6
- __version__ = "2.17.0"
6
+ __version__ = "2.18.0"
@@ -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.unescape_json(content)
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