hexcli 2.10.0__tar.gz → 2.11.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 (49) hide show
  1. {hexcli-2.10.0 → hexcli-2.11.0}/CHANGELOG.md +35 -0
  2. {hexcli-2.10.0 → hexcli-2.11.0}/PKG-INFO +1 -1
  3. {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/__init__.py +1 -1
  4. {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/agent.py +45 -0
  5. {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/tools.py +188 -5
  6. {hexcli-2.10.0 → hexcli-2.11.0}/.gitignore +0 -0
  7. {hexcli-2.10.0 → hexcli-2.11.0}/Hex CLI.cmd +0 -0
  8. {hexcli-2.10.0 → hexcli-2.11.0}/LICENSE +0 -0
  9. {hexcli-2.10.0 → hexcli-2.11.0}/README.md +0 -0
  10. {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/assets/hexcli.ico +0 -0
  11. {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/assets/hexcli.png +0 -0
  12. {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/cancel.py +0 -0
  13. {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/chatlog.py +0 -0
  14. {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/commands.py +0 -0
  15. {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/compaction.py +0 -0
  16. {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/config.py +0 -0
  17. {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/diffview.py +0 -0
  18. {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/distribution.py +0 -0
  19. {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/doctor.py +0 -0
  20. {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/escalate.py +0 -0
  21. {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/http_client.py +0 -0
  22. {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/launcher.py +0 -0
  23. {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/lineedit.py +0 -0
  24. {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/llm.py +0 -0
  25. {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/local_escalation.py +0 -0
  26. {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/lockfile.py +0 -0
  27. {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/loop_v2.py +0 -0
  28. {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/markdown_stream.py +0 -0
  29. {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/memory.py +0 -0
  30. {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/network.py +0 -0
  31. {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/parsing.py +0 -0
  32. {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/paths.py +0 -0
  33. {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/prompts.py +0 -0
  34. {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/protocol_v2.py +0 -0
  35. {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/repl.py +0 -0
  36. {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/safety.py +0 -0
  37. {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/sessions.py +0 -0
  38. {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/setup_wizard.py +0 -0
  39. {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/shell_session.py +0 -0
  40. {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/statusbar.py +0 -0
  41. {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/stream_render.py +0 -0
  42. {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/telemetry.py +0 -0
  43. {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/ui.py +0 -0
  44. {hexcli-2.10.0 → hexcli-2.11.0}/install.ps1 +0 -0
  45. {hexcli-2.10.0 → hexcli-2.11.0}/launcher.py +0 -0
  46. {hexcli-2.10.0 → hexcli-2.11.0}/pyproject.toml +0 -0
  47. {hexcli-2.10.0 → hexcli-2.11.0}/shellai.cmd +0 -0
  48. {hexcli-2.10.0 → hexcli-2.11.0}/shellai.example.json +0 -0
  49. {hexcli-2.10.0 → hexcli-2.11.0}/shellai.py +0 -0
@@ -6,6 +6,41 @@ the Hexagon NPU, not single-run anecdotes.
6
6
 
7
7
  ## Unreleased
8
8
 
9
+ ## 2.11.0 — 2026-09-14
10
+
11
+ A minor release: tool result text the model reads changed (`verify_syntax`
12
+ reports its rung) and a new nudge was added. Gate: extended suite at 5 runs, seed 20260914, gate PASS after a 6-run recheck (five cases missed once, all 5/6 or better on recheck; run-level 156/205 vs 165/208, p=0.48); multi-turn at 3 runs on a fresh server with 0 invalid runs, no 3/3 case lost, uc3-t7 gained (38/48 vs 33/44, p=0.80); smoke 10/10; stall probe clean; CI green on main and the tag.
13
+
14
+ - Claims need evidence. A finish that says it ran something, that it
15
+ works, that the buttons respond or that the tests pass, needs a run this
16
+ turn (run_code or run_command); reading the file back proves the bytes,
17
+ not the behaviour. The owner's 2026-09-13 calculator session made three
18
+ such claims after read_file, with nothing ever run and nothing wired.
19
+ One nudge names the claim and asks for a run or a plain statement that
20
+ it was not run; a second unbacked claim goes out with a dim "Nothing was
21
+ run this turn." under it. The tests-claim nudge is the same rule for one
22
+ phrase; this is the class.
23
+ - `verify_syntax` is a ladder that reports the rung it reached. A page is
24
+ parsed and cross-referenced: handlers the markup calls must be functions
25
+ a script defines, ids the scripts look up must be elements the markup
26
+ has, buttons must be wired to something, and a script after `</body>` is
27
+ noted (the calculator: no handler on any button, an id no element had, a
28
+ function nothing called). Python is parsed and cross-referenced for
29
+ names it reads but never binds (a star import turns that off). JSON and
30
+ PowerShell say "parsed". A file with no checker is `NOT CHECKED`, never
31
+ `OK: skipped`, and says so, so the claim above cannot rest on it.
32
+ - Two eval cases: `claims-1`, the calculator prompt graded on the page
33
+ being wired and the finish not claiming a run that never happened, and
34
+ `claims-2`, a Python edit-and-claim variant. The 2026-09-13 page is a
35
+ fixture.
36
+ - The eval runner waits for the inference slot after a client timeout.
37
+ An abandoned request keeps the server's one slot until it finishes; the
38
+ next run queued behind it, timed out too, and took a whole scenario
39
+ down as invalid (uc3, 2026-09-13, twice). After a timed-out run the
40
+ runner probes until the backend answers again, up to ten minutes, and
41
+ the timeout no longer counts toward the abort streak once the slot is
42
+ back.
43
+
9
44
  ## 2.10.0 — 2026-09-14
10
45
 
11
46
  A minor release: the retry feedback the model reads changed. Gate: extended
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: hexcli
3
- Version: 2.10.0
3
+ Version: 2.11.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.10.0"
6
+ __version__ = "2.11.0"
@@ -1193,6 +1193,9 @@ def _run_autopilot_turn(
1193
1193
  _tests_requested = _asks_to_run_tests(query)
1194
1194
  _tests_nudge_used = False
1195
1195
  _run_targets: list[str] = []
1196
+ _ran_anything = False # any run_code / run_command this turn: the evidence a behaviour claim needs
1197
+ _claim_nudge_used = False
1198
+ _unbacked_claim = False
1196
1199
  # The last test run's failure output (None once a run passes), so a
1197
1200
  # finish right after a failing run can be sent back once more.
1198
1201
  _last_test_failure: str | None = None
@@ -1338,6 +1341,21 @@ def _run_autopilot_turn(
1338
1341
  messages.append({"role": "assistant", "content": strip_thinking(raw)})
1339
1342
  messages.append({"role": "user", "content": _retest_nudge_text(_last_test_failure)})
1340
1343
  continue
1344
+ # Claims need evidence. "Ran it successfully", "the buttons now
1345
+ # respond", "tests pass": a behaviour claim in the finish needs
1346
+ # a run this turn; reading a file back proves the bytes, not the
1347
+ # behaviour (the calculator session, 2026-09-13: three such
1348
+ # claims, nothing ever run, nothing wired). One nudge: run it, or
1349
+ # say plainly that it was not run. A second unbacked claim goes
1350
+ # out with a one-line notice under it, so the user knows.
1351
+ claim = _behaviour_claim(msg)
1352
+ if (claim and not _ran_anything and config.get("require_verification", True)):
1353
+ if not _claim_nudge_used:
1354
+ _claim_nudge_used = True
1355
+ messages.append({"role": "assistant", "content": strip_thinking(raw)})
1356
+ messages.append({"role": "user", "content": _claim_nudge_text(claim)})
1357
+ continue
1358
+ _unbacked_claim = True
1341
1359
  # Escalation trigger B — the verification nudge was ignored: the
1342
1360
  # model finished a second time without checking its own mutation.
1343
1361
  if (_unverified_mutation and _verify_nudge_used
@@ -1367,6 +1385,8 @@ def _run_autopilot_turn(
1367
1385
  })
1368
1386
  continue
1369
1387
  result = msg or last_tool_output or "Done."
1388
+ if _unbacked_claim:
1389
+ ui.cprint(" Nothing was run this turn.", C.DIM)
1370
1390
  memory.maybe_index_turn(config, query, tools_used, touched_paths, outcome="completed")
1371
1391
  if session:
1372
1392
  _record_undo_snapshots(session, _turn_snapshots)
@@ -1398,6 +1418,7 @@ def _run_autopilot_turn(
1398
1418
  touched_paths.append(str(tool_path))
1399
1419
  if tool_name in ("run_code", "run_command") and isinstance(action.get("args"), dict):
1400
1420
  _run_targets.append(str(action["args"].get("path") or action["args"].get("command") or ""))
1421
+ _ran_anything = True
1401
1422
 
1402
1423
  # Capture file state before first mutation so /undo can restore it.
1403
1424
  if tool_name in {"edit_file", "write_file", "append_file"} and tool_path:
@@ -1685,6 +1706,30 @@ _RUN_TESTS_RE = re.compile(
1685
1706
  )
1686
1707
 
1687
1708
 
1709
+ _BEHAVIOUR_CLAIM_RE = re.compile(
1710
+ r"\b(ran (?:it|the [a-z]+|successfully)(?: successfully)?|runs (?:correctly|fine|successfully|as expected)|"
1711
+ r"(?:it|this|that|everything|the [a-z]+) (?:now |all )?works\b|works (?:as expected|correctly|now|fine)|"
1712
+ r"(?:it|this|that|everything|the [a-z]+) is (?:now )?working(?: correctly| as expected)?|"
1713
+ r"(?:buttons?|it|the app|the page|the script) (?:now )?responds?|"
1714
+ r"calculates? correctly|opens? correctly|executed successfully|"
1715
+ r"tests? (?:pass|passed|passing|succeed)|all tests pass|verified (?:that )?it (?:works|runs)|"
1716
+ r"confirmed (?:that )?it (?:works|runs))\b", re.IGNORECASE)
1717
+
1718
+
1719
+ def _behaviour_claim(message: str) -> str:
1720
+ """The first behaviour claim in a finish message, or ''. Deliberately a
1721
+ list of ways to say "I ran it and it works", not a list of verbs: "the
1722
+ function returns the sum" is a description, "it works" is a claim."""
1723
+ m = _BEHAVIOUR_CLAIM_RE.search(message or "")
1724
+ return m.group(0) if m else ""
1725
+
1726
+
1727
+ def _claim_nudge_text(claim: str) -> str:
1728
+ return (f"You said \"{claim}\" but nothing was run this turn. Run it with run_code or "
1729
+ "run_command and report what you observed, or say plainly that it was not run. "
1730
+ "Respond with JSON only.")
1731
+
1732
+
1688
1733
  def _asks_to_run_tests(query: str) -> bool:
1689
1734
  """True when the request itself asks for the tests to be run."""
1690
1735
  return bool(_RUN_TESTS_RE.search(query or ""))
@@ -581,22 +581,205 @@ def _verify_node_syntax(path: Path) -> tuple[bool, str]:
581
581
  return False, f"FAIL: {result.stderr.strip() or result.stdout.strip()}"
582
582
 
583
583
 
584
+ _PY_IMPLICIT = {"__name__", "__file__", "__doc__", "__builtins__", "__spec__", "__loader__",
585
+ "__package__", "__path__", "__annotations__", "__debug__"}
586
+
587
+
588
+ def undefined_python_names(source: str) -> list[str]:
589
+ """Names the module reads but never binds anywhere and Python does not
590
+ provide: the cross-reference rung for Python. Scopes are collapsed to
591
+ the module (a name bound anywhere counts), so this never flags a name
592
+ that some function defines; it only catches the calculator-class
593
+ mistake of calling something that does not exist. A star import turns
594
+ the check off."""
595
+ import builtins
596
+ try:
597
+ tree = ast.parse(source)
598
+ except SyntaxError:
599
+ return []
600
+ bound: set[str] = set(dir(builtins)) | _PY_IMPLICIT
601
+ loads: dict[str, int] = {}
602
+ for node in ast.walk(tree):
603
+ if isinstance(node, ast.ImportFrom) and any(a.name == "*" for a in node.names):
604
+ return []
605
+ if isinstance(node, (ast.Import, ast.ImportFrom)):
606
+ for a in node.names:
607
+ bound.add((a.asname or a.name).split(".")[0])
608
+ elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
609
+ bound.add(node.name)
610
+ if not isinstance(node, ast.ClassDef):
611
+ args = node.args
612
+ for a in [*args.posonlyargs, *args.args, *args.kwonlyargs]:
613
+ bound.add(a.arg)
614
+ for a in (args.vararg, args.kwarg):
615
+ if a is not None:
616
+ bound.add(a.arg)
617
+ elif isinstance(node, ast.Lambda):
618
+ args = node.args
619
+ for a in [*args.posonlyargs, *args.args, *args.kwonlyargs]:
620
+ bound.add(a.arg)
621
+ for a in (args.vararg, args.kwarg):
622
+ if a is not None:
623
+ bound.add(a.arg)
624
+ elif isinstance(node, ast.ExceptHandler) and node.name:
625
+ bound.add(node.name)
626
+ elif isinstance(node, (ast.Global, ast.Nonlocal)):
627
+ bound.update(node.names)
628
+ elif isinstance(node, ast.MatchAs) and node.name:
629
+ bound.add(node.name)
630
+ elif isinstance(node, ast.MatchStar) and node.name:
631
+ bound.add(node.name)
632
+ elif isinstance(node, ast.MatchMapping) and node.rest:
633
+ bound.add(node.rest)
634
+ elif isinstance(node, ast.Name):
635
+ if isinstance(node.ctx, (ast.Store, ast.Del)):
636
+ bound.add(node.id)
637
+ else:
638
+ loads.setdefault(node.id, node.lineno)
639
+ return [f"{name} (line {line})" for name, line in sorted(loads.items(), key=lambda kv: kv[1])
640
+ if name not in bound]
641
+
642
+
643
+ _JS_BUILTIN_CALLS = {"alert", "confirm", "prompt", "eval", "parseInt", "parseFloat", "String",
644
+ "Number", "Boolean", "Math", "setTimeout", "setInterval", "console",
645
+ "document", "window", "event", "this", "if", "for", "while", "return",
646
+ "function", "new", "Array", "Object", "JSON", "isNaN", "Date", "fetch",
647
+ "requestAnimationFrame", "clearTimeout", "clearInterval", "Promise"}
648
+
649
+
650
+ def html_wiring_report(path: Path) -> tuple[bool, str]:
651
+ """Cross-reference rung for a page: the handlers the markup calls must be
652
+ functions the scripts define, the ids the scripts look up must be
653
+ elements the markup has, and buttons must be wired to something. Reading
654
+ a page back proves the bytes; this proves the parts are connected
655
+ (2026-09-14: a calculator with buttons that called nothing, a script that
656
+ looked up an id no element had, and a function nothing called was
657
+ "verified" three times by read_file)."""
658
+ from html.parser import HTMLParser
659
+
660
+ class _Walk(HTMLParser):
661
+ def __init__(self) -> None:
662
+ super().__init__()
663
+ self.ids: set[str] = set()
664
+ self.handlers: list[str] = []
665
+ self.buttons = 0
666
+ self.scripts: list[str] = []
667
+ self.script_srcs: list[str] = []
668
+ self._in_script = False
669
+ self._body_closed = False
670
+ self.script_after_body = False
671
+ self._buf: list[str] = []
672
+
673
+ def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
674
+ a = {k.lower(): (v or "") for k, v in attrs}
675
+ if a.get("id"):
676
+ self.ids.add(a["id"])
677
+ for k, v in a.items():
678
+ if k.startswith("on") and v:
679
+ self.handlers.extend(m for m in re.findall(r"([A-Za-z_$][\w$]*)\s*\(", v))
680
+ if tag == "button" or (tag == "input" and a.get("type", "").lower() in {"button", "submit"}):
681
+ self.buttons += 1
682
+ if tag == "script":
683
+ self._in_script = True
684
+ self._buf = []
685
+ if a.get("src"):
686
+ self.script_srcs.append(a["src"])
687
+ if self._body_closed:
688
+ self.script_after_body = True
689
+
690
+ def handle_endtag(self, tag: str) -> None:
691
+ if tag == "script" and self._in_script:
692
+ self._in_script = False
693
+ self.scripts.append("".join(self._buf))
694
+ if tag in {"body", "html"}:
695
+ self._body_closed = True
696
+
697
+ def handle_data(self, data: str) -> None:
698
+ if self._in_script:
699
+ self._buf.append(data)
700
+
701
+ source = path.read_text(encoding="utf-8", errors="replace")
702
+ w = _Walk()
703
+ try:
704
+ w.feed(source)
705
+ w.close()
706
+ except Exception as exc: # noqa: BLE001 — html.parser is lenient; anything else is a report
707
+ return False, f"FAIL: could not parse the page: {exc}"
708
+ script = "\n".join(w.scripts)
709
+ for src in w.script_srcs:
710
+ try:
711
+ script += "\n" + (path.parent / src).read_text(encoding="utf-8", errors="replace")
712
+ except OSError:
713
+ pass
714
+ defined = set(re.findall(r"\bfunction\s+([A-Za-z_$][\w$]*)\s*\(", script))
715
+ defined |= set(re.findall(r"\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s+)?(?:function\b|\(|[A-Za-z_$][\w$]*\s*=>)", script))
716
+ defined |= set(re.findall(r"\b(?:window\.)?([A-Za-z_$][\w$]*)\s*=\s*(?:async\s+)?function\b", script))
717
+ looked_up = set(re.findall(r"getElementById\(\s*['\"]([^'\"]+)['\"]", script))
718
+ looked_up |= set(re.findall(r"querySelector(?:All)?\(\s*['\"]#([\w-]+)", script))
719
+ listeners = bool(re.search(r"addEventListener\s*\(|\.on[a-z]+\s*=", script))
720
+ problems: list[str] = []
721
+ undefined = sorted({h for h in w.handlers if h not in defined and h not in _JS_BUILTIN_CALLS})
722
+ if undefined:
723
+ problems.append("handlers the markup calls but no script defines: " + ", ".join(undefined))
724
+ missing = sorted(i for i in looked_up if i not in w.ids)
725
+ if missing:
726
+ problems.append("ids the script looks up but no element has: " + ", ".join(missing))
727
+ if w.buttons and not w.handlers and not listeners:
728
+ problems.append(f"{w.buttons} button(s) and none is wired to a script (no on* attribute, no addEventListener)")
729
+ notes: list[str] = []
730
+ if w.script_after_body:
731
+ notes.append("a <script> sits after </body>")
732
+ if defined and not w.handlers and not listeners:
733
+ notes.append("functions defined but nothing calls them: " + ", ".join(sorted(defined)))
734
+ if problems:
735
+ return False, "FAIL: parsed, cross-referenced: " + "; ".join(problems + notes)
736
+ summary = f"OK: parsed, cross-referenced ({len(w.handlers)} handler call(s), {len(w.ids)} id(s), {w.buttons} button(s))"
737
+ if notes:
738
+ summary += "; " + "; ".join(notes)
739
+ return True, summary
740
+
741
+
584
742
  def verify_syntax_tool(path_text: str, language: str, shell_exe: str) -> str:
743
+ """The verification ladder. Every result says which rung was reached:
744
+ executed (not here: run_code does that), parsed and cross-referenced,
745
+ parsed only, or NOT CHECKED. A file this tool cannot check is reported
746
+ as unchecked, never as OK: the model used to read "OK: skipped" and
747
+ finish with "verified"."""
585
748
  path = resolve_path(path_text)
586
749
  if not path.exists():
587
750
  raise RuntimeError(f"File not found: {path}")
588
- lang = (language or "").strip().lower() or _LANGUAGE_BY_EXT.get(path.suffix.lower(), "")
589
- if lang == "python":
751
+ suffix = path.suffix.lower()
752
+ lang = (language or "").strip().lower() or _LANGUAGE_BY_EXT.get(suffix, "")
753
+ if suffix in {".html", ".htm"} or lang in {"html", "htm"}:
754
+ ok, detail = html_wiring_report(path)
755
+ elif lang == "python":
590
756
  ok, detail = _verify_python_syntax(path)
757
+ if ok and not detail.startswith("OK: skipped"):
758
+ undefined = undefined_python_names(path.read_text(encoding="utf-8", errors="replace"))
759
+ if undefined:
760
+ ok, detail = False, "FAIL: parsed, cross-referenced: references undefined name(s): " + ", ".join(undefined)
761
+ else:
762
+ detail = "OK: parsed, cross-referenced: no syntax errors, every name it reads is defined"
591
763
  elif lang == "json":
592
764
  ok, detail = _verify_json_syntax(path)
765
+ if ok:
766
+ detail = detail.replace("OK: valid JSON", "OK: parsed: valid JSON")
593
767
  elif lang == "powershell":
594
768
  ok, detail = _verify_powershell_syntax(path, shell_exe)
595
- elif lang == "node" or path.suffix.lower() in {".js", ".mjs", ".cjs", ".ts", ".tsx", ".jsx"}:
769
+ if ok and detail.startswith("OK: skipped"):
770
+ ok, detail = True, "NOT CHECKED: " + detail[len("OK: skipped ("):].rstrip(")")
771
+ elif ok:
772
+ detail = "OK: parsed: no syntax errors"
773
+ elif lang == "node" or suffix in {".js", ".mjs", ".cjs", ".ts", ".tsx", ".jsx"}:
596
774
  ok, detail = _verify_node_syntax(path)
775
+ if ok and detail.startswith("OK: skipped"):
776
+ ok, detail = True, "NOT CHECKED: " + detail[len("OK: skipped ("):].rstrip(")")
777
+ elif ok:
778
+ detail = "OK: parsed: no syntax errors"
597
779
  else:
598
- ok, detail = True, f"OK: skipped (no syntax checker for '{path.suffix or language or 'unknown'}')"
599
- ui.tool_event("verify", f"{path} ({'pass' if ok else 'fail'})")
780
+ ok, detail = True, (f"NOT CHECKED: no checker for '{suffix or language or 'this file'}'. "
781
+ "The file was not verified; say so if you report on it.")
782
+ ui.tool_event("verify", f"{path} ({'fail' if not ok else ('unchecked' if detail.startswith('NOT CHECKED') else 'pass')})")
600
783
  return detail
601
784
 
602
785
 
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
File without changes