lcode-cli 0.31.0__tar.gz → 0.32.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 (167) hide show
  1. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/CHANGELOG.md +14 -1
  2. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/PKG-INFO +1 -1
  3. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/docs/automation.md +1 -0
  4. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/docs/configuration.md +1 -0
  5. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/docs/quickstart.md +1 -1
  6. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/docs/usage.md +26 -0
  7. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/pyproject.toml +1 -0
  8. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/src/lcode/__init__.py +1 -1
  9. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/src/lcode/agent.py +54 -0
  10. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/src/lcode/api.py +3 -0
  11. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/src/lcode/cli.py +4 -0
  12. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/src/lcode/config.py +1 -0
  13. lcode_cli-0.32.0/src/lcode/review.py +103 -0
  14. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/src/lcode/tools.py +3 -0
  15. lcode_cli-0.32.0/tests/test_review.py +94 -0
  16. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/.editorconfig +0 -0
  17. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/.github/CODEOWNERS +0 -0
  18. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  19. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  20. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  21. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/.github/ISSUE_TEMPLATE/model_request.yml +0 -0
  22. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  23. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/.github/dependabot.yml +0 -0
  24. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/.github/workflows/ci.yml +0 -0
  25. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/.github/workflows/docs.yml +0 -0
  26. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/.github/workflows/release.yml +0 -0
  27. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/.gitignore +0 -0
  28. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/.pre-commit-config.yaml +0 -0
  29. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/AGENTS.md +0 -0
  30. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/CODE_OF_CONDUCT.md +0 -0
  31. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/CONTRIBUTING.md +0 -0
  32. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/LICENSE +0 -0
  33. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/README.md +0 -0
  34. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/SECURITY.md +0 -0
  35. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/action.yml +0 -0
  36. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/docs/agents.md +0 -0
  37. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/docs/assets/demo.svg +0 -0
  38. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/docs/assets/extra.css +0 -0
  39. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/docs/assets/logo.svg +0 -0
  40. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/docs/assets/social-preview.png +0 -0
  41. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/docs/bench.md +0 -0
  42. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/docs/changelog.md +0 -0
  43. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/docs/commands.md +0 -0
  44. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/docs/contributing.md +0 -0
  45. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/docs/editors.md +0 -0
  46. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/docs/git.md +0 -0
  47. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/docs/hooks.md +0 -0
  48. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/docs/how-it-works.md +0 -0
  49. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/docs/index.md +0 -0
  50. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/docs/installation.md +0 -0
  51. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/docs/lsp.md +0 -0
  52. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/docs/mcp.md +0 -0
  53. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/docs/memory.md +0 -0
  54. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/docs/models.md +0 -0
  55. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/docs/sandbox.md +0 -0
  56. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/docs/search.md +0 -0
  57. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/docs/servers.md +0 -0
  58. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/docs/troubleshooting.md +0 -0
  59. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/examples/README.md +0 -0
  60. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/examples/api/fix_tests.py +0 -0
  61. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/examples/commands/review.md +0 -0
  62. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/examples/skills/commit-message/SKILL.md +0 -0
  63. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/examples/skills/commit-message/scripts/staged.sh +0 -0
  64. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/install.sh +0 -0
  65. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/mkdocs.yml +0 -0
  66. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/overrides/main.html +0 -0
  67. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/scripts/record_demo.py +0 -0
  68. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/scripts/social-preview.html +0 -0
  69. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/src/lcode/__main__.py +0 -0
  70. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/src/lcode/acp.py +0 -0
  71. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/src/lcode/action.py +0 -0
  72. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/src/lcode/backends.py +0 -0
  73. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/src/lcode/bench.py +0 -0
  74. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/src/lcode/bench_backend.py +0 -0
  75. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/src/lcode/bench_runtime.py +0 -0
  76. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/src/lcode/catalog.py +0 -0
  77. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/src/lcode/checkpoints.py +0 -0
  78. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/src/lcode/checks.py +0 -0
  79. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/src/lcode/clipboard.py +0 -0
  80. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/src/lcode/codesearch.py +0 -0
  81. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/src/lcode/context.py +0 -0
  82. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/src/lcode/extensions.py +0 -0
  83. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/src/lcode/frontmatter.py +0 -0
  84. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/src/lcode/gitflow.py +0 -0
  85. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/src/lcode/hardware.py +0 -0
  86. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/src/lcode/hooks.py +0 -0
  87. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/src/lcode/jobs.py +0 -0
  88. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/src/lcode/limits.py +0 -0
  89. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/src/lcode/lsp.py +0 -0
  90. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/src/lcode/matching.py +0 -0
  91. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/src/lcode/mcp/__init__.py +0 -0
  92. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/src/lcode/mcp/auth.py +0 -0
  93. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/src/lcode/mcp/catalog.py +0 -0
  94. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/src/lcode/mcp/catalog.toml +0 -0
  95. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/src/lcode/mcp/client.py +0 -0
  96. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/src/lcode/mcp/commands.py +0 -0
  97. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/src/lcode/mcp/config.py +0 -0
  98. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/src/lcode/mcp/http.py +0 -0
  99. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/src/lcode/mcp/manager.py +0 -0
  100. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/src/lcode/mcp/protocol.py +0 -0
  101. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/src/lcode/mcp/server.py +0 -0
  102. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/src/lcode/mcp/stdio.py +0 -0
  103. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/src/lcode/memory.py +0 -0
  104. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/src/lcode/models.toml +0 -0
  105. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/src/lcode/notify.py +0 -0
  106. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/src/lcode/ollama.py +0 -0
  107. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/src/lcode/permissions.py +0 -0
  108. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/src/lcode/planning.py +0 -0
  109. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/src/lcode/render.py +0 -0
  110. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/src/lcode/repeats.py +0 -0
  111. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/src/lcode/repl.py +0 -0
  112. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/src/lcode/repomap.py +0 -0
  113. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/src/lcode/residency.py +0 -0
  114. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/src/lcode/sandbox.py +0 -0
  115. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/src/lcode/secretscan.py +0 -0
  116. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/src/lcode/servers/__init__.py +0 -0
  117. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/src/lcode/servers/encord.py +0 -0
  118. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/src/lcode/servers/valohai.py +0 -0
  119. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/src/lcode/servers/xai.py +0 -0
  120. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/src/lcode/sessions.py +0 -0
  121. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/src/lcode/subagents.py +0 -0
  122. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/src/lcode/update.py +0 -0
  123. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/src/lcode/vision.py +0 -0
  124. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/src/lcode/web.py +0 -0
  125. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/tests/conftest.py +0 -0
  126. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/tests/fake_lsp_server.py +0 -0
  127. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/tests/fake_mcp_server.py +0 -0
  128. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/tests/test_acp.py +0 -0
  129. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/tests/test_action.py +0 -0
  130. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/tests/test_agent.py +0 -0
  131. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/tests/test_api.py +0 -0
  132. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/tests/test_backends.py +0 -0
  133. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/tests/test_bench.py +0 -0
  134. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/tests/test_catalog.py +0 -0
  135. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/tests/test_checkpoints.py +0 -0
  136. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/tests/test_checks.py +0 -0
  137. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/tests/test_cli.py +0 -0
  138. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/tests/test_clipboard.py +0 -0
  139. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/tests/test_codesearch.py +0 -0
  140. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/tests/test_config.py +0 -0
  141. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/tests/test_context.py +0 -0
  142. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/tests/test_context_picker.py +0 -0
  143. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/tests/test_extensions.py +0 -0
  144. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/tests/test_gitflow.py +0 -0
  145. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/tests/test_hooks.py +0 -0
  146. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/tests/test_jobs.py +0 -0
  147. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/tests/test_lsp.py +0 -0
  148. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/tests/test_matching.py +0 -0
  149. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/tests/test_mcp.py +0 -0
  150. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/tests/test_memory.py +0 -0
  151. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/tests/test_permissions.py +0 -0
  152. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/tests/test_plan_mode.py +0 -0
  153. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/tests/test_repeats.py +0 -0
  154. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/tests/test_repomap.py +0 -0
  155. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/tests/test_residency.py +0 -0
  156. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/tests/test_robustness.py +0 -0
  157. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/tests/test_sandbox.py +0 -0
  158. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/tests/test_secretscan.py +0 -0
  159. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/tests/test_servers.py +0 -0
  160. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/tests/test_sessions.py +0 -0
  161. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/tests/test_subagents.py +0 -0
  162. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/tests/test_tools.py +0 -0
  163. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/tests/test_update.py +0 -0
  164. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/tests/test_vision.py +0 -0
  165. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/tests/test_web.py +0 -0
  166. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/tests/test_xai.py +0 -0
  167. {lcode_cli-0.31.0 → lcode_cli-0.32.0}/uv.lock +0 -0
@@ -6,6 +6,18 @@ All notable changes to lcode are documented here. The format follows
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.32.0] - 2026-10-14
10
+
11
+ ### Added
12
+
13
+ - A review before the answer. After a request that changed files, the model checks its change
14
+ against your request, from the diff and its own answer, and real problems (part of the request
15
+ not done, an untested claim, a leftover debug print) go back to it once. One line says what the
16
+ review found. It's off by default: measured with qwen3.6-35b on a 12 GB laptop, it made requests
17
+ 73% slower for one real catch. Turn it on with `lcode config set review auto`. `lcode -p --output
18
+ json` reports it in `review`, and `lcode bench --review` compares. See
19
+ [A review before the answer](https://nasser1941.github.io/lcode/usage/#a-review-before-the-answer).
20
+
9
21
  ## [0.31.0] - 2026-10-14
10
22
 
11
23
  ### Added
@@ -679,7 +691,8 @@ First public release.
679
691
  - `AGENTS.md` project instructions and `/init` to generate them.
680
692
  - One-line installer for Linux and macOS.
681
693
 
682
- [Unreleased]: https://github.com/nasser1941/lcode/compare/v0.31.0...HEAD
694
+ [Unreleased]: https://github.com/nasser1941/lcode/compare/v0.32.0...HEAD
695
+ [0.32.0]: https://github.com/nasser1941/lcode/compare/v0.31.0...v0.32.0
683
696
  [0.31.0]: https://github.com/nasser1941/lcode/compare/v0.30.0...v0.31.0
684
697
  [0.30.0]: https://github.com/nasser1941/lcode/compare/v0.29.0...v0.30.0
685
698
  [0.29.0]: https://github.com/nasser1941/lcode/compare/v0.28.1...v0.29.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: lcode-cli
3
- Version: 0.31.0
3
+ Version: 0.32.0
4
4
  Summary: A local-first terminal coding agent powered by open-weight models on your own GPU or Mac, via Ollama.
5
5
  Project-URL: Homepage, https://nasser1941.github.io/lcode/
6
6
  Project-URL: Documentation, https://nasser1941.github.io/lcode/
@@ -55,6 +55,7 @@ went: `0` success, `1` an error, `3` stopped at `--max-steps`, `4` stopped becau
55
55
  | `tool_calls` | Every tool call, with its arguments and output (the first 4,000 characters). `error` is true when the call failed or wasn't allowed |
56
56
  | `files_changed` | Files the request added (`A`), modified (`M`) or deleted (`D`), shell commands included |
57
57
  | `usage` | Model requests and tokens; `prompt_tokens` includes tokens served from the prompt cache |
58
+ | `review` | What the [review](usage.md#a-review-before-the-answer) sent back to the model (`[]` when it found nothing; `null` when no review ran) |
58
59
  | `checks` | The [checks](usage.md#checks-before-done) that ran after the change: `command`, `exit_code` (`null` when they weren't allowed to run) and `by` (`lcode`, or `model` when the model ran them itself) |
59
60
  | `error` | What went wrong, when `status` is `error` |
60
61
  | `session_id` | Continue the conversation later with `lcode --resume <id>` |
@@ -43,6 +43,7 @@ lcode config path # print the file location
43
43
  | `max_parallel_agents` | `1` | Subagents that run at the same time; more needs `OLLAMA_NUM_PARALLEL` on the Ollama server ([details](agents.md#several-at-once)) |
44
44
  | `notify` | `true` | A desktop [notification](usage.md#notifications) when a long request is done or waits for your answer |
45
45
  | `notify_after` | `30` | Seconds a request runs before it notifies |
46
+ | `review` | `off` | Before the answer, [review a change](usage.md#a-review-before-the-answer) against the request and send problems back to the model: `auto` or `off` |
46
47
  | `checks` | `auto` | Run the project's [checks](usage.md#checks-before-done) after a request changed files: `auto` (found from the project), `off`, or a command |
47
48
  | `repeat_limit` | `3` | Identical tool calls with identical results before lcode [steps in](how-it-works.md#when-the-model-repeats-itself); `0` turns it off |
48
49
  | `secret_check` | `true` | Before `git commit` and `git push`, look for passwords, keys and tokens they'd add ([details](usage.md#secrets-in-commits)) |
@@ -12,7 +12,7 @@ lcode
12
12
 
13
13
  ```text
14
14
  ╭──────────────────────────────────────────────╮
15
- │ lcode v0.31.0 — local coding agent │
15
+ │ lcode v0.32.0 — local coding agent │
16
16
  │ │
17
17
  │ model lcode-qwen3.6-35b │
18
18
  │ context 256K tokens │
@@ -179,6 +179,32 @@ command = "uv run pytest -q && uv run ruff check ."
179
179
  Or set it for yourself with `lcode config set checks "make check"`; `lcode config set checks off`
180
180
  turns checks off.
181
181
 
182
+ ## A review before the answer
183
+
184
+ Models claim more than they did: half of a two-part request done, "it works now" without having
185
+ checked, a debug print left behind. So after a request that changed files (and after the
186
+ [checks](#checks-before-done)), lcode asks the model to step back and review its change against your
187
+ request. It's told to judge only from what you asked, the diff, and its own answer:
188
+
189
+ ```text
190
+ ◆ Review: the change does what was asked.
191
+ ⚠ Review: 1 point(s) sent back to the model. First: README.md isn't changed, though …
192
+ ```
193
+
194
+ Problems it can point to go back to the model once; it fixes them, or says why a point is wrong.
195
+ `lcode -p --output json` reports what the review found in `review`.
196
+
197
+ **It's off by default,** because it's an extra model answer after every change. On the 12 GB laptop
198
+ with qwen3.6-35b, the 8 `lcode bench` tasks took 416 s with the review against 240 s without
199
+ (8/8 against 7/8, within run-to-run noise). It found a real gap in one run of `runtime-bug` ("it can't
200
+ be made smaller" left undone), which the model then argued away. Without reasoning, the review was
201
+ no faster and missed that gap every time, so it reasons like the model does. Turn it on with
202
+ `lcode config set review auto` when a stronger or faster model makes the extra look worth it.
203
+
204
+ The review is asked on top of the conversation rather than in a fresh one. The model server keeps
205
+ the processed conversation cached, and a separate review would push it out, so the next request
206
+ would have to read the whole conversation again. On a small GPU that's tens of seconds.
207
+
182
208
  ## Background commands
183
209
 
184
210
  Some commands keep running: a dev server, a test watcher, a build in watch mode. The model starts
@@ -78,6 +78,7 @@ ignore = [
78
78
  "src/lcode/memory.py" = ["E501"]
79
79
  "src/lcode/subagents.py" = ["E501"]
80
80
  "src/lcode/planning.py" = ["E501"]
81
+ "src/lcode/review.py" = ["E501"] # the review prompt is prose
81
82
  "src/lcode/extensions.py" = ["E501"]
82
83
  "src/lcode/context.py" = ["E501"]
83
84
  "src/lcode/lsp.py" = ["E501"]
@@ -3,7 +3,7 @@
3
3
  From a program: `from lcode import Session` (see lcode.api).
4
4
  """
5
5
 
6
- __version__ = "0.31.0"
6
+ __version__ = "0.32.0"
7
7
 
8
8
  API = ("Session", "Result", "Options", "SetupError")
9
9
 
@@ -35,6 +35,7 @@ from lcode import (
35
35
  planning,
36
36
  repomap,
37
37
  residency,
38
+ review,
38
39
  sessions,
39
40
  subagents,
40
41
  vision,
@@ -276,6 +277,7 @@ class Settings:
276
277
  compact_at: int = 0 # tokens: make room at this size even when the window is larger (0: only at 85%)
277
278
  repeat_limit: int = 3 # identical calls with identical results before the model is told (lcode.repeats)
278
279
  checks: str = "off" # the project's checks after a change (lcode.checks): auto, off or a command
280
+ review: str = "off" # check a change against the request before the answer (lcode.review): auto or off
279
281
 
280
282
 
281
283
  class Agent:
@@ -334,6 +336,7 @@ class Agent:
334
336
  self.autosave = False # save the session after every step (sessions people resume; not the bench's)
335
337
  self._check: tuple[Path, checks.Check | None] | None = None
336
338
  self.check_results: list[dict] = [] # the checks of the last request: command, exit code, who ran them
339
+ self.review_results: list[str] | None = None # what the last request's review found (None: no review)
337
340
  self.sent_chars = 0 # characters of the conversation the model had already read
338
341
  self.counted_chars: int | None = None # characters that ctx_used covers (None: all of them)
339
342
  self.turn_status = "" # how the last request ended: success, max_steps, loop
@@ -831,6 +834,48 @@ class Agent:
831
834
  self.last_speed = final.get("eval_count", 0) / seconds if seconds else 0.0
832
835
  return message
833
836
 
837
+ def reviewing(self) -> bool:
838
+ return (
839
+ self.settings.review != "off" and self.allowed_tools is None and not self.planning() and not self.no_changes
840
+ )
841
+
842
+ def self_review(self, answer: str) -> list[str]:
843
+ """Check the request's change against the request before the answer counts; the problems found."""
844
+ budget = int(self.settings.context * 3 * 0.25)
845
+ diff, changed = review.diff_of(self.tools.before, self.current_text, budget)
846
+ if changed < review.MIN_CHANGED_LINES:
847
+ return []
848
+ request = next(
849
+ (
850
+ context_tools.request_text(str(m.get("content", "")))
851
+ for m in reversed(self.messages)
852
+ if m.get("role") == "user"
853
+ and not str(m.get("content", "")).startswith(("[lcode]", sessions.SUMMARY_PREFIX))
854
+ ),
855
+ "",
856
+ )
857
+ try:
858
+ with self.console.status("Reviewing the change against your request…", spinner="dots"):
859
+ problems = review.run(self, request, answer, diff)
860
+ except OllamaError as e:
861
+ self.say(f"The review didn't run: {e}", warning=True)
862
+ return []
863
+ self.review_results = problems
864
+ if problems:
865
+ self.say(
866
+ f"Review: {len(problems)} point(s) sent back to the model. First: {problems[0][:160]}", warning=True
867
+ )
868
+ else:
869
+ self.say("Review: the change does what was asked.")
870
+ return problems
871
+
872
+ def current_text(self, path: str) -> str | None:
873
+ p = self.tools.resolve(path)
874
+ try:
875
+ return self.tools.read_text(p) if p.is_file() else None
876
+ except OSError:
877
+ return None
878
+
834
879
  def checks_prompt(self) -> str:
835
880
  check = self.project_check()
836
881
  if check is None:
@@ -1026,6 +1071,9 @@ class Agent:
1026
1071
  self.messages.append({"role": "user", "content": self.expand_mentions(user_text) + note})
1027
1072
  self.repeats.reset()
1028
1073
  self.check_results = []
1074
+ self.review_results = None
1075
+ self.tools.before = {}
1076
+ reviewed = False
1029
1077
  changed = verified = False # files changed in this request; the checks passed since
1030
1078
  check_rounds = 0
1031
1079
  malformed = 0
@@ -1087,6 +1135,12 @@ class Agent:
1087
1135
  changed = False # run them again only if the model changes something more
1088
1136
  continue
1089
1137
  verified = True
1138
+ if not reviewed and self.tools.before and self.reviewing():
1139
+ reviewed = True
1140
+ problems = self.self_review(reply.get("content") or "")
1141
+ if problems:
1142
+ self.messages.append({"role": "user", "content": review.feedback(problems)})
1143
+ continue
1090
1144
  break
1091
1145
  done: dict[int, str] = {} # results of subagents that ran together
1092
1146
  together = [
@@ -77,6 +77,7 @@ class Result:
77
77
  files_changed: list[dict] = field(default_factory=list) # {"status": "A" | "M" | "D", "path"}
78
78
  usage: dict = field(default_factory=dict) # requests, prompt_tokens, output_tokens
79
79
  checks: list[dict] = field(default_factory=list) # {"command", "exit_code", "by": "lcode" | "model"}
80
+ review: list[str] | None = None # problems the review sent back ([] when it found none; None: no review)
80
81
  seconds: float = 0.0
81
82
  error: str = ""
82
83
 
@@ -198,6 +199,7 @@ def open_agent(cwd: Path, options: Options, console: Console, hw: Hardware | Non
198
199
  secret_check=cfg["secret_check"],
199
200
  repeat_limit=cfg["repeat_limit"],
200
201
  checks=cfg["checks"],
202
+ review=cfg["review"],
201
203
  compact_at=cfg["compact_at"],
202
204
  )
203
205
  agent = Agent(ollama, settings, cwd, console=console)
@@ -306,6 +308,7 @@ def run_request(agent: Agent, prompt: str) -> Result:
306
308
  files_changed=files,
307
309
  usage=usage,
308
310
  checks=list(agent.check_results),
311
+ review=agent.review_results,
309
312
  seconds=round(time.monotonic() - started, 2),
310
313
  error=error,
311
314
  )
@@ -581,6 +581,7 @@ def cmd_bench(args) -> None:
581
581
  prune=not args.no_prune,
582
582
  compact_at=cfg["compact_at"] if args.compact_at is None else args.compact_at,
583
583
  checks=cfg["checks"] if args.checks is None else args.checks,
584
+ review=cfg["review"] if args.review is None else args.review,
584
585
  repo_map=args.repo_map,
585
586
  embed_model=cfg["embed_model"] if args.code_search else "off",
586
587
  )
@@ -775,6 +776,9 @@ def build_subparsers() -> dict[str, argparse.ArgumentParser]:
775
776
  p.add_argument("--repo-map", action="store_true", help="give the model the repository map (off by default)")
776
777
  p.add_argument("--code-search", action="store_true", help="index each task's folder and offer semantic code search")
777
778
  p.add_argument("--no-prune", action="store_true", help="don't remove old tool output (to compare, with --session)")
779
+ p.add_argument(
780
+ "--review", choices=["auto", "off"], help="review each change before it counts (default: the setting)"
781
+ )
778
782
  p.add_argument(
779
783
  "--checks",
780
784
  metavar="auto|off|COMMAND",
@@ -59,6 +59,7 @@ SETTINGS: dict[str, tuple[object, type, str]] = {
59
59
  "subagents": (True, bool, "let the model hand tasks to subagents that have their own context"),
60
60
  "notify": (True, bool, "desktop notification when a long request is done or waits for you"),
61
61
  "notify_after": (30, int, "seconds a request runs before it notifies (with notify on)"),
62
+ "review": ("off", str, "before the answer, check a change against the request and send problems back: auto | off"),
62
63
  "checks": ("auto", str, "run the project's checks before calling work done: auto | off | a command"),
63
64
  "repeat_limit": (3, int, "identical tool calls with identical results before lcode steps in (0: never)"),
64
65
  "secret_check": (True, bool, "before git commit and git push, look for passwords, keys and tokens they'd add"),
@@ -0,0 +1,103 @@
1
+ """A second look at a change before the answer goes to the user.
2
+
3
+ Models claim more than they did: "users can now shrink the window" without having checked, half of a
4
+ two-part request done, a debug print left behind. So when a request changed files, lcode asks the model to
5
+ step back and check its change against the request (what was asked, what the diff shows, what the answer
6
+ claims) before the answer counts. Real problems go back to the model once; the user sees a line about it.
7
+
8
+ The review is asked on top of the conversation, not in a fresh one: the model server keeps the processed
9
+ conversation cached, and a separate review prompt would push it out, so the next step would have to read the
10
+ whole conversation again, tens of seconds on a small GPU. The question tells the model to judge only from the
11
+ request and the diff.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import difflib
17
+ from typing import TYPE_CHECKING
18
+
19
+ from lcode.gitflow import parse_json
20
+
21
+ if TYPE_CHECKING:
22
+ from lcode.agent import Agent
23
+
24
+ MIN_CHANGED_LINES = 1 # even one line: a one-line bug fix is where a wrong fix hides
25
+ MAX_PROBLEMS = 5
26
+ SCHEMA = {
27
+ "type": "object",
28
+ "properties": {"problems": {"type": "array", "items": {"type": "string"}}},
29
+ "required": ["problems"],
30
+ }
31
+ PROMPT = """[lcode] Before your answer goes to the user, review your change as a strict reviewer would: judge it only by what the user asked and what the diff shows, not by what you meant to do.
32
+
33
+ The user's request:
34
+ <request>
35
+ {request}
36
+ </request>
37
+
38
+ Your answer to them:
39
+ <answer>
40
+ {answer}
41
+ </answer>
42
+
43
+ {checks}The change (unified diff of the files changed in this request):
44
+ <diff>
45
+ {diff}
46
+ </diff>
47
+
48
+ Find real problems only, each one you can point to in the request or the diff:
49
+ 1. Something the user asked for that the change doesn't do, or only partly does.
50
+ 2. A claim in your answer that the diff doesn't support, or something called tested or working that nothing shows was run.
51
+ 3. A mistake in the diff: a bug, a broken edge case the request mentions, leftover debugging (prints, commented-out code), an unfinished TODO.
52
+ No style preferences, no suggestions beyond the request. Answer in JSON: {{"problems": ["…"]}}, with an empty list when the change does what was asked."""
53
+
54
+ FEEDBACK = """[lcode] A review of your change against the user's request found:
55
+ {problems}
56
+
57
+ Fix what's real; if a point is wrong, say why in your answer. Then finish."""
58
+
59
+
60
+ def diff_of(before: dict[str, str | None], read, budget: int) -> tuple[str, int]:
61
+ """The unified diff of the files changed in the request, and how many lines it changes."""
62
+ parts, changed = [], 0
63
+ for path, old in before.items():
64
+ new = read(path)
65
+ if new is None or new == old:
66
+ continue
67
+ lines = list(
68
+ difflib.unified_diff((old or "").splitlines(True), new.splitlines(True), f"a/{path}", f"b/{path}", n=2)
69
+ )
70
+ changed += sum(1 for line in lines if line[:1] in "+-" and not line.startswith(("+++", "---")))
71
+ parts.append("".join(lines))
72
+ text = "".join(parts)
73
+ if len(text) > budget:
74
+ text = text[:budget] + f"\n[… the rest of the diff ({len(text) - budget} characters) is cut]"
75
+ return text, changed
76
+
77
+
78
+ def run(agent: Agent, request: str, answer: str, diff: str) -> list[str]:
79
+ """Ask for the review; the problems it found (empty when none, or when the answer can't be read)."""
80
+ checks = ""
81
+ if agent.check_results:
82
+ last = agent.check_results[-1]
83
+ code = last.get("exit_code")
84
+ state = "passed" if code == 0 else "didn't run" if code is None else f"failed (exit code {code})"
85
+ checks = f"The project's checks (`{last.get('command')}`) {state} after the change.\n\n"
86
+ prompt = PROMPT.format(request=request[:4000], answer=answer[:3000] or "(none)", checks=checks, diff=diff)
87
+ payload = {
88
+ "model": agent.settings.model,
89
+ "messages": [*agent.messages, {"role": "user", "content": prompt}],
90
+ "tools": agent.tool_schemas(), # as in the session, so the cached conversation is reused
91
+ "think": agent.settings.think, # without reasoning it missed every gap it caught with it, and wasn't faster
92
+ "format": SCHEMA,
93
+ "keep_alive": agent.settings.keep_alive,
94
+ "options": agent.options(),
95
+ }
96
+ reply = agent.ollama.chat(payload)
97
+ data = parse_json((reply.get("message") or {}).get("content") or "")
98
+ problems = data.get("problems") if isinstance(data.get("problems"), list) else []
99
+ return [str(p).strip()[:500] for p in problems if str(p).strip()][:MAX_PROBLEMS]
100
+
101
+
102
+ def feedback(problems: list[str]) -> str:
103
+ return FEEDBACK.format(problems="\n".join(f"- {p}" for p in problems))
@@ -448,6 +448,7 @@ class Toolbox:
448
448
  self.formats: dict[str, TextFormat] = {} # encoding and line endings of the files read
449
449
  self.notes: list[str] = [] # the model's notes on the task (todo_write): decisions, findings, questions
450
450
  self.changed: dict[str, None] = {} # files lcode wrote in this session, in order
451
+ self.before: dict[str, str | None] = {} # files changed in this request: their text before (None: new)
451
452
  self.todos: list[dict] = []
452
453
  # An editor's buffers (lcode acp): read unsaved changes and write through the editor.
453
454
  self.reader: Callable[[Path], str | None] | None = None
@@ -870,6 +871,8 @@ class Toolbox:
870
871
  p.parent.mkdir(parents=True, exist_ok=True)
871
872
  fmt = self.formats.get(str(p)) or (decode(p.read_bytes())[1] if p.exists() else TextFormat())
872
873
  data = encode(content, fmt) # before anything is written: it may not be possible
874
+ if self.rel(p) not in self.before:
875
+ self.before[self.rel(p)] = decode(p.read_bytes())[0] if p.exists() else None
873
876
  if self.writer is not None:
874
877
  self.writer(p, content) # the editor updates its buffer (and the file)
875
878
  if not p.exists() or p.read_bytes() != data:
@@ -0,0 +1,94 @@
1
+ """Self-review (#114): a change is checked against the request before the answer counts."""
2
+
3
+ import json
4
+
5
+ from conftest import call, output, reply
6
+ from lcode import api, review
7
+ from lcode.ollama import OllamaError
8
+
9
+ NEW = (
10
+ "import sys\n\n\ndef main(argv):\n verbose = '--verbose' in argv\n"
11
+ " print(sum(int(a) for a in argv if a != '--verbose'))\n"
12
+ )
13
+
14
+
15
+ def text(agent):
16
+ return " ".join(output(agent).split())
17
+
18
+
19
+ def changing(make_agent, *after, **settings):
20
+ return make_agent(
21
+ [
22
+ reply(tool_calls=[call("write_file", path="app.py", content=NEW)]),
23
+ reply("Added --verbose and documented it."),
24
+ *after,
25
+ ],
26
+ **{"review": "auto", **settings},
27
+ )
28
+
29
+
30
+ def test_problems_go_back_to_the_model_once(make_agent):
31
+ agent = changing(make_agent, reply("Fixed: the flag now prints each number, and README.md documents it."))
32
+ agent.ollama.chat_replies = [
33
+ json.dumps({"problems": ["README.md isn't changed, though the user asked to document the flag."]})
34
+ ]
35
+ agent.run_turn("add --verbose and document it in README.md")
36
+ note = next(
37
+ m["content"] for m in agent.messages if m["role"] == "user" and m["content"].startswith("[lcode] A review")
38
+ )
39
+ assert "- README.md isn't changed" in note and "Fix what's real" in note
40
+ assert agent.messages[-1]["content"].startswith("Fixed:")
41
+ assert "Review: 1 point(s) sent back to the model" in text(agent)
42
+ assert agent.review_results == ["README.md isn't changed, though the user asked to document the flag."]
43
+ asked = agent.ollama.chats[0]
44
+ assert "add --verbose and document it in README.md" in asked["messages"][-1]["content"]
45
+ assert "+ verbose = '--verbose' in argv" in asked["messages"][-1]["content"] # the diff
46
+ assert asked["format"] == review.SCHEMA
47
+
48
+
49
+ def test_a_clean_review_adds_no_step(make_agent):
50
+ agent = changing(make_agent)
51
+ agent.ollama.chat_replies = [json.dumps({"problems": []})]
52
+ agent.run_turn("add --verbose")
53
+ assert agent.messages[-1]["content"] == "Added --verbose and documented it."
54
+ assert "Review: the change does what was asked." in text(agent) and agent.review_results == []
55
+
56
+
57
+ def test_no_review_without_a_change_or_when_off(make_agent):
58
+ reading = make_agent(
59
+ [reply(tool_calls=[call("read_file", path="README.md")]), reply("It's a demo.")], review="auto"
60
+ )
61
+ reading.run_turn("what is this?")
62
+ assert reading.ollama.chats == [] and reading.review_results is None
63
+ off = changing(make_agent, review="off")
64
+ off.run_turn("add --verbose")
65
+ assert off.ollama.chats == []
66
+ planner = make_agent(mode="plan", review="auto")
67
+ assert not planner.reviewing()
68
+
69
+
70
+ def test_a_review_that_fails_doesnt_stop_the_answer(make_agent):
71
+ agent = changing(make_agent)
72
+
73
+ def broken(payload):
74
+ raise OllamaError("model crashed")
75
+
76
+ agent.ollama.chat = broken
77
+ agent.run_turn("add --verbose")
78
+ assert agent.messages[-1]["content"] == "Added --verbose and documented it."
79
+ assert "The review didn't run: model crashed" in text(agent)
80
+
81
+
82
+ def test_the_diff_covers_only_this_request(tmp_path):
83
+ files = {"a.py": "x = 1\ny = 2\nz = 3\n", "new.py": "print('hi')\n"}
84
+ diff, changed = review.diff_of(
85
+ {"a.py": "x = 1\ny = 0\nz = 3\n", "new.py": None, "same.py": "q\n"}, files.get, 10_000
86
+ )
87
+ assert "-y = 0\n+y = 2" in diff and "+print('hi')" in diff and "same.py" not in diff and changed == 3
88
+ cut, _ = review.diff_of({"a.py": ""}, lambda p: "x\n" * 5000, 100)
89
+ assert cut.endswith("is cut]")
90
+
91
+
92
+ def test_programs_see_the_review(make_agent):
93
+ result = api.Result(status="success", text="", session_id="s", model="m", review=["README.md not updated"])
94
+ assert result.to_json()["review"] == ["README.md not updated"]
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