lcode-cli 0.30.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.
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/CHANGELOG.md +26 -1
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/PKG-INFO +1 -1
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/docs/automation.md +1 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/docs/configuration.md +1 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/docs/how-it-works.md +10 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/docs/quickstart.md +1 -1
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/docs/usage.md +28 -1
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/pyproject.toml +1 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/src/lcode/__init__.py +1 -1
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/src/lcode/agent.py +68 -2
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/src/lcode/api.py +3 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/src/lcode/cli.py +4 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/src/lcode/config.py +1 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/src/lcode/context.py +48 -1
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/src/lcode/repl.py +28 -0
- lcode_cli-0.32.0/src/lcode/review.py +103 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/src/lcode/tools.py +37 -4
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/tests/test_context.py +54 -1
- lcode_cli-0.32.0/tests/test_review.py +94 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/tests/test_tools.py +8 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/.editorconfig +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/.github/CODEOWNERS +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/.github/ISSUE_TEMPLATE/model_request.yml +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/.github/dependabot.yml +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/.github/workflows/ci.yml +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/.github/workflows/docs.yml +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/.github/workflows/release.yml +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/.gitignore +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/.pre-commit-config.yaml +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/AGENTS.md +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/CODE_OF_CONDUCT.md +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/CONTRIBUTING.md +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/LICENSE +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/README.md +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/SECURITY.md +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/action.yml +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/docs/agents.md +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/docs/assets/demo.svg +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/docs/assets/extra.css +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/docs/assets/logo.svg +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/docs/assets/social-preview.png +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/docs/bench.md +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/docs/changelog.md +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/docs/commands.md +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/docs/contributing.md +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/docs/editors.md +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/docs/git.md +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/docs/hooks.md +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/docs/index.md +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/docs/installation.md +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/docs/lsp.md +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/docs/mcp.md +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/docs/memory.md +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/docs/models.md +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/docs/sandbox.md +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/docs/search.md +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/docs/servers.md +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/docs/troubleshooting.md +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/examples/README.md +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/examples/api/fix_tests.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/examples/commands/review.md +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/examples/skills/commit-message/SKILL.md +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/examples/skills/commit-message/scripts/staged.sh +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/install.sh +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/mkdocs.yml +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/overrides/main.html +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/scripts/record_demo.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/scripts/social-preview.html +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/src/lcode/__main__.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/src/lcode/acp.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/src/lcode/action.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/src/lcode/backends.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/src/lcode/bench.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/src/lcode/bench_backend.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/src/lcode/bench_runtime.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/src/lcode/catalog.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/src/lcode/checkpoints.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/src/lcode/checks.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/src/lcode/clipboard.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/src/lcode/codesearch.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/src/lcode/extensions.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/src/lcode/frontmatter.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/src/lcode/gitflow.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/src/lcode/hardware.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/src/lcode/hooks.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/src/lcode/jobs.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/src/lcode/limits.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/src/lcode/lsp.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/src/lcode/matching.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/src/lcode/mcp/__init__.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/src/lcode/mcp/auth.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/src/lcode/mcp/catalog.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/src/lcode/mcp/catalog.toml +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/src/lcode/mcp/client.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/src/lcode/mcp/commands.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/src/lcode/mcp/config.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/src/lcode/mcp/http.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/src/lcode/mcp/manager.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/src/lcode/mcp/protocol.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/src/lcode/mcp/server.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/src/lcode/mcp/stdio.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/src/lcode/memory.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/src/lcode/models.toml +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/src/lcode/notify.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/src/lcode/ollama.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/src/lcode/permissions.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/src/lcode/planning.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/src/lcode/render.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/src/lcode/repeats.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/src/lcode/repomap.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/src/lcode/residency.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/src/lcode/sandbox.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/src/lcode/secretscan.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/src/lcode/servers/__init__.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/src/lcode/servers/encord.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/src/lcode/servers/valohai.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/src/lcode/servers/xai.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/src/lcode/sessions.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/src/lcode/subagents.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/src/lcode/update.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/src/lcode/vision.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/src/lcode/web.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/tests/conftest.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/tests/fake_lsp_server.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/tests/fake_mcp_server.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/tests/test_acp.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/tests/test_action.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/tests/test_agent.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/tests/test_api.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/tests/test_backends.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/tests/test_bench.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/tests/test_catalog.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/tests/test_checkpoints.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/tests/test_checks.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/tests/test_cli.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/tests/test_clipboard.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/tests/test_codesearch.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/tests/test_config.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/tests/test_context_picker.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/tests/test_extensions.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/tests/test_gitflow.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/tests/test_hooks.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/tests/test_jobs.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/tests/test_lsp.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/tests/test_matching.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/tests/test_mcp.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/tests/test_memory.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/tests/test_permissions.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/tests/test_plan_mode.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/tests/test_repeats.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/tests/test_repomap.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/tests/test_residency.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/tests/test_robustness.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/tests/test_sandbox.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/tests/test_secretscan.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/tests/test_servers.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/tests/test_sessions.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/tests/test_subagents.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/tests/test_update.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/tests/test_vision.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/tests/test_web.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/tests/test_xai.py +0 -0
- {lcode_cli-0.30.0 → lcode_cli-0.32.0}/uv.lock +0 -0
|
@@ -6,6 +6,29 @@ 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
|
+
|
|
21
|
+
## [0.31.0] - 2026-10-14
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- A task sheet that survives long sessions. The plan (the todo list), the model's notes (decisions,
|
|
26
|
+
findings, open questions, kept in `todo_write`'s new `notes`, or inside a todo item, where small
|
|
27
|
+
models tend to put them), the files changed and the last check are pinned next to the summary and
|
|
28
|
+
next to your request when old output is removed. It's saved with the session, so `lcode -c`
|
|
29
|
+
brings it back (the todo list used to be lost), and `/status` shows it. See
|
|
30
|
+
[Context management](https://nasser1941.github.io/lcode/how-it-works/#context-management).
|
|
31
|
+
|
|
9
32
|
## [0.30.0] - 2026-10-13
|
|
10
33
|
|
|
11
34
|
### Added
|
|
@@ -668,7 +691,9 @@ First public release.
|
|
|
668
691
|
- `AGENTS.md` project instructions and `/init` to generate them.
|
|
669
692
|
- One-line installer for Linux and macOS.
|
|
670
693
|
|
|
671
|
-
[Unreleased]: https://github.com/nasser1941/lcode/compare/v0.
|
|
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
|
|
696
|
+
[0.31.0]: https://github.com/nasser1941/lcode/compare/v0.30.0...v0.31.0
|
|
672
697
|
[0.30.0]: https://github.com/nasser1941/lcode/compare/v0.29.0...v0.30.0
|
|
673
698
|
[0.29.0]: https://github.com/nasser1941/lcode/compare/v0.28.1...v0.29.0
|
|
674
699
|
[0.28.1]: https://github.com/nasser1941/lcode/compare/v0.28.0...v0.28.1
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: lcode-cli
|
|
3
|
-
Version: 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)) |
|
|
@@ -97,6 +97,16 @@ context is old tool output, so:
|
|
|
97
97
|
findings, files changed, next steps) and continues from it. Your latest requests (up to eight)
|
|
98
98
|
are kept word for word under the summary, so the goal isn't lost in the retelling.
|
|
99
99
|
|
|
100
|
+
**The task sheet.** Next to the summary, and next to your request when old tool output is removed,
|
|
101
|
+
lcode pins a short sheet, so the state of the work survives:
|
|
102
|
+
|
|
103
|
+
- the plan (the model's todo list);
|
|
104
|
+
- the notes the model keeps with it (decisions and why, findings, open questions);
|
|
105
|
+
- the files changed in the session;
|
|
106
|
+
- the last check.
|
|
107
|
+
|
|
108
|
+
It's saved with the session, so `lcode -c` brings it back, and `/status` shows it to you.
|
|
109
|
+
|
|
100
110
|
`lcode config set compact_at 128k` moves the ceiling, and `0` turns it off, leaving only the 85%
|
|
101
111
|
rule. `/context` shows where the current session makes room.
|
|
102
112
|
|
|
@@ -80,6 +80,7 @@ reasoning is on.
|
|
|
80
80
|
| `/mode [ask\|plan\|auto-edit\|yolo]` | Set the permission mode |
|
|
81
81
|
| `/cd DIR` | Change the working directory |
|
|
82
82
|
| `/todos` | Show the model's task list |
|
|
83
|
+
| `/status` | The task sheet: your latest requests, the plan and the model's notes, files changed, the last check |
|
|
83
84
|
| `/jobs [stop ID]` | The [background commands](#background-commands) the model started; stop one |
|
|
84
85
|
| `/commit [notes]` | [Commit](git.md#commit) the changes with a message in the repository's style, once you approve it |
|
|
85
86
|
| `/review [base]` | [Review](git.md#review) the uncommitted changes, or the branch against a base |
|
|
@@ -99,7 +100,7 @@ reasoning is on.
|
|
|
99
100
|
| `grep` | Regex search with ripgrep (falls back to Python if ripgrep is missing) |
|
|
100
101
|
| `bash` | Run a shell command with live output, a timeout and a persistent working directory; with `background`, keep it running ([details](#background-commands)) |
|
|
101
102
|
| `bash_output`, `bash_stop` | Read what a background command printed since last time; stop it |
|
|
102
|
-
| `todo_write` | Keep a visible task list for multi-step work |
|
|
103
|
+
| `todo_write` | Keep a visible task list for multi-step work, with notes (decisions, findings) that survive when old messages are removed; `/status` shows them |
|
|
103
104
|
| `web_search` | Search the web for current information (needs a [search provider](#web-search)) |
|
|
104
105
|
| `web_fetch` | Read a web page or text file by URL as clean text |
|
|
105
106
|
| `view_image` | Look at an image: a model that can see describes it ([Images](#images)) |
|
|
@@ -178,6 +179,32 @@ command = "uv run pytest -q && uv run ruff check ."
|
|
|
178
179
|
Or set it for yourself with `lcode config set checks "make check"`; `lcode config set checks off`
|
|
179
180
|
turns checks off.
|
|
180
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
|
+
|
|
181
208
|
## Background commands
|
|
182
209
|
|
|
183
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"]
|
|
@@ -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,
|
|
@@ -112,7 +113,7 @@ SYSTEM_PROMPT = """You are lcode, an autonomous software-engineering agent runni
|
|
|
112
113
|
- To understand a repository: look at the layout, README/docs, entry points and config, then grep and read the relevant files. Cite code as `path:line`.
|
|
113
114
|
- Read a file before editing it. Use edit_file for targeted changes and write_file for new files or full rewrites. old_string must be copied exactly from the file (without the line-number prefixes).
|
|
114
115
|
- Match the existing code style, naming and structure. Don't add unrequested features.
|
|
115
|
-
- For multi-step tasks, plan with todo_write and keep it updated.
|
|
116
|
+
- For multi-step tasks, plan with todo_write and keep it updated; put decisions and findings you must not lose in its notes.
|
|
116
117
|
- Work autonomously until the task is done. Only stop to ask the user when you are genuinely blocked or the decision is theirs.
|
|
117
118
|
- Don't run destructive or irreversible commands (rm -rf, git reset --hard, git push --force, dropping data) unless the user explicitly asked.
|
|
118
119
|
- If the user denies a tool call, don't retry the same thing; adjust or ask.
|
|
@@ -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
|
|
@@ -613,6 +616,9 @@ class Agent:
|
|
|
613
616
|
"title": self.session_title,
|
|
614
617
|
"messages": self.messages,
|
|
615
618
|
"checkpoints": self.checkpoints.to_json(),
|
|
619
|
+
"todos": self.tools.todos,
|
|
620
|
+
"notes": self.tools.notes,
|
|
621
|
+
"changed": list(self.tools.changed),
|
|
616
622
|
}
|
|
617
623
|
# Written next to it and then renamed: a crash halfway leaves the previous version, not a broken file.
|
|
618
624
|
partial = f.with_name(f".{f.name}.partial")
|
|
@@ -626,6 +632,8 @@ class Agent:
|
|
|
626
632
|
self.session_name = ""
|
|
627
633
|
self.session_title = ""
|
|
628
634
|
self.checkpoints.reset(self.session_id)
|
|
635
|
+
self.tools.todos, self.tools.notes, self.tools.changed = [], [], {}
|
|
636
|
+
self.check_results = []
|
|
629
637
|
|
|
630
638
|
def rename(self, name: str) -> None:
|
|
631
639
|
self.session_name = " ".join(name.split())
|
|
@@ -652,6 +660,9 @@ class Agent:
|
|
|
652
660
|
self.session_name = data.get("name", "")
|
|
653
661
|
self.session_title = data.get("title") or sessions.title_from(self.messages)
|
|
654
662
|
self.tools.read_mtimes.clear() # files may have changed since; the model must read them again
|
|
663
|
+
self.tools.todos = list(data.get("todos") or [])
|
|
664
|
+
self.tools.notes = [str(n) for n in data.get("notes") or []]
|
|
665
|
+
self.tools.changed = dict.fromkeys(str(f) for f in data.get("changed") or [])
|
|
655
666
|
self.reflected = len(self.messages) # checked for notes to remember when that session ended
|
|
656
667
|
self.ctx_used = sum(len(json.dumps(m)) for m in self.messages) // 3
|
|
657
668
|
self.counted_chars = None
|
|
@@ -823,6 +834,48 @@ class Agent:
|
|
|
823
834
|
self.last_speed = final.get("eval_count", 0) / seconds if seconds else 0.0
|
|
824
835
|
return message
|
|
825
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
|
+
|
|
826
879
|
def checks_prompt(self) -> str:
|
|
827
880
|
check = self.project_check()
|
|
828
881
|
if check is None:
|
|
@@ -1018,6 +1071,9 @@ class Agent:
|
|
|
1018
1071
|
self.messages.append({"role": "user", "content": self.expand_mentions(user_text) + note})
|
|
1019
1072
|
self.repeats.reset()
|
|
1020
1073
|
self.check_results = []
|
|
1074
|
+
self.review_results = None
|
|
1075
|
+
self.tools.before = {}
|
|
1076
|
+
reviewed = False
|
|
1021
1077
|
changed = verified = False # files changed in this request; the checks passed since
|
|
1022
1078
|
check_rounds = 0
|
|
1023
1079
|
malformed = 0
|
|
@@ -1079,6 +1135,12 @@ class Agent:
|
|
|
1079
1135
|
changed = False # run them again only if the model changes something more
|
|
1080
1136
|
continue
|
|
1081
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
|
|
1082
1144
|
break
|
|
1083
1145
|
done: dict[int, str] = {} # results of subagents that ran together
|
|
1084
1146
|
together = [
|
|
@@ -1289,6 +1351,8 @@ class Agent:
|
|
|
1289
1351
|
self.messages, keep=1, removed=removed
|
|
1290
1352
|
) # not enough: before the last request
|
|
1291
1353
|
context_tools.note_removed(self.messages, removed)
|
|
1354
|
+
if saved:
|
|
1355
|
+
context_tools.pin(self.messages, context_tools.task_sheet(self))
|
|
1292
1356
|
if saved:
|
|
1293
1357
|
self.sent_chars = 0 # the conversation changed: the model reads it again
|
|
1294
1358
|
self.pruned += 1
|
|
@@ -1333,7 +1397,9 @@ class Agent:
|
|
|
1333
1397
|
{"role": "system", "content": self.system_prompt()},
|
|
1334
1398
|
{
|
|
1335
1399
|
"role": "user",
|
|
1336
|
-
"content": f"{sessions.SUMMARY_PREFIX}\n\n{summary}"
|
|
1400
|
+
"content": f"{sessions.SUMMARY_PREFIX}\n\n{summary}"
|
|
1401
|
+
+ context_tools.requests_block(requests)
|
|
1402
|
+
+ (f"\n\n{sheet}" if (sheet := context_tools.task_sheet(self)) else ""),
|
|
1337
1403
|
},
|
|
1338
1404
|
{"role": "assistant", "content": "Got it — I have the context from the summary and will continue."},
|
|
1339
1405
|
]
|
|
@@ -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"),
|
|
@@ -49,7 +49,9 @@ REQUESTS_HEADER = "[The user's requests in this conversation, word for word, old
|
|
|
49
49
|
MAX_REQUESTS = 8 # the most recent ones are kept through a summary
|
|
50
50
|
MAX_REQUEST_CHARS = 1500
|
|
51
51
|
# What lcode appends to the user's words: attached files, images and folders, and its own notes.
|
|
52
|
-
ATTACHED = re.compile(
|
|
52
|
+
ATTACHED = re.compile(
|
|
53
|
+
r"\n\n(?:<file path=|<image path=|<directory path=|\[lcode[:\]] |\[The user wants the )"
|
|
54
|
+
) # also the sheet
|
|
53
55
|
|
|
54
56
|
|
|
55
57
|
def request_text(content: str) -> str:
|
|
@@ -187,6 +189,51 @@ def prune(messages: list[dict], keep: int = KEEP_RECENT_REQUESTS, removed: list[
|
|
|
187
189
|
return saved
|
|
188
190
|
|
|
189
191
|
|
|
192
|
+
SHEET_HEADER = "[lcode] Task sheet: the plan, notes and changes so far, kept when old messages are removed"
|
|
193
|
+
MAX_SHEET = 3000 # characters
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def task_sheet(agent: Agent) -> str:
|
|
197
|
+
"""The state of the work, from lcode's own records: the todo list and the model's notes (todo_write), the
|
|
198
|
+
files changed in the session and the last check. Pinned where old messages go: in summaries and next to the
|
|
199
|
+
request when tool output is removed."""
|
|
200
|
+
tools = agent.tools
|
|
201
|
+
parts = []
|
|
202
|
+
if tools.todos:
|
|
203
|
+
marks = {"completed": "[x]", "in_progress": "[>]", "pending": "[ ]"}
|
|
204
|
+
plan = "\n".join(f"{marks.get(t.get('status', ''), '[ ]')} {t.get('content', '')}" for t in tools.todos)
|
|
205
|
+
parts.append(f"Plan:\n{plan}")
|
|
206
|
+
if tools.notes:
|
|
207
|
+
parts.append("Notes:\n" + "\n".join(f"- {note}" for note in tools.notes))
|
|
208
|
+
if tools.changed:
|
|
209
|
+
files = list(tools.changed)
|
|
210
|
+
more = f" and {len(files) - 30} more" if len(files) > 30 else ""
|
|
211
|
+
parts.append("Files changed in this session: " + ", ".join(files[-30:]) + more)
|
|
212
|
+
if agent.check_results:
|
|
213
|
+
last = agent.check_results[-1]
|
|
214
|
+
code = last.get("exit_code")
|
|
215
|
+
state = "passed" if code == 0 else "didn't run" if code is None else f"failed (exit code {code})"
|
|
216
|
+
parts.append(f"Last check: `{last.get('command')}` {state}")
|
|
217
|
+
if not parts:
|
|
218
|
+
return ""
|
|
219
|
+
sheet = "\n\n".join(parts)
|
|
220
|
+
if len(sheet) > MAX_SHEET:
|
|
221
|
+
sheet = sheet[: MAX_SHEET - 20] + "\n[…cut]"
|
|
222
|
+
return f"{SHEET_HEADER}\n{sheet}"
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def pin(messages: list[dict], text: str) -> None:
|
|
226
|
+
"""Put a note next to the request the model is working on, replacing an earlier sheet there."""
|
|
227
|
+
if not text:
|
|
228
|
+
return
|
|
229
|
+
for message in reversed(messages):
|
|
230
|
+
content = str(message.get("content", ""))
|
|
231
|
+
if message.get("role") == "user" and not content.startswith(("[lcode]", SUMMARY_PREFIX)):
|
|
232
|
+
content = content.split("\n\n" + SHEET_HEADER, 1)[0]
|
|
233
|
+
message["content"] = f"{content}\n\n{text}"
|
|
234
|
+
return
|
|
235
|
+
|
|
236
|
+
|
|
190
237
|
def note_removed(messages: list[dict], removed: list[str]) -> None:
|
|
191
238
|
"""Tell the model, next to the request it's working on, which files it no longer has: a stub deep in the
|
|
192
239
|
conversation is easy to overlook, and models then answer from memory instead of reading again."""
|
|
@@ -63,6 +63,7 @@ COMMANDS = {
|
|
|
63
63
|
"/mode": "Permission mode: ask | plan | auto-edit | yolo (Shift+Tab cycles)",
|
|
64
64
|
"/cd": "Change the working directory",
|
|
65
65
|
"/todos": "Show the current todo list",
|
|
66
|
+
"/status": "The task sheet: your requests, the plan and notes, files changed, the last check",
|
|
66
67
|
"/jobs": "Background commands the model started: /jobs, /jobs stop <id>",
|
|
67
68
|
"/commit": "Commit the changes with a message in the repository's style, after you approve it",
|
|
68
69
|
"/review": "Review the uncommitted changes, or the branch against a base: /review [base]",
|
|
@@ -109,6 +110,31 @@ def mention(path: Path) -> str:
|
|
|
109
110
|
return f'@"{shown}"' if " " in shown else f"@{shown}"
|
|
110
111
|
|
|
111
112
|
|
|
113
|
+
def status_command(agent: Agent) -> None:
|
|
114
|
+
"""What the session is doing, as lcode keeps it for the model (the task sheet)."""
|
|
115
|
+
c = agent.console
|
|
116
|
+
requests = context_tools.user_requests(agent.messages)
|
|
117
|
+
if not requests and not agent.tools.todos and not agent.tools.changed:
|
|
118
|
+
c.print("Nothing yet: no requests in this session.")
|
|
119
|
+
return
|
|
120
|
+
if requests:
|
|
121
|
+
latest = requests[-3:]
|
|
122
|
+
lines = [f"[dim]{len(requests) - len(latest) + i + 1}.[/] {escape(r[:300])}" for i, r in enumerate(latest)]
|
|
123
|
+
c.print(Panel("\n".join(lines), title="Requests (latest)", title_align="left", border_style="cyan"))
|
|
124
|
+
if agent.tools.todos or agent.tools.notes:
|
|
125
|
+
agent.tools.show_todos()
|
|
126
|
+
if agent.tools.changed:
|
|
127
|
+
c.print(f"[bold]Files changed:[/] {escape(', '.join(agent.tools.changed))}")
|
|
128
|
+
if agent.check_results:
|
|
129
|
+
last = agent.check_results[-1]
|
|
130
|
+
code = last.get("exit_code")
|
|
131
|
+
state = (
|
|
132
|
+
"[green]passed[/]" if code == 0 else "didn't run" if code is None else f"[red]failed[/] (exit code {code})"
|
|
133
|
+
)
|
|
134
|
+
c.print(f"[bold]Last check:[/] {escape(str(last.get('command')))} {state}")
|
|
135
|
+
c.print("[dim]The model keeps this too, even when old messages are removed or summarized.[/]")
|
|
136
|
+
|
|
137
|
+
|
|
112
138
|
def build_session(agent: Agent) -> PromptSession:
|
|
113
139
|
kb = KeyBindings()
|
|
114
140
|
|
|
@@ -978,6 +1004,8 @@ def handle_command(agent: Agent, line: str, hardware: Hardware) -> bool:
|
|
|
978
1004
|
c.print(f"Working directory: {p}")
|
|
979
1005
|
else:
|
|
980
1006
|
c.print(f"[red]Not a directory: {p}[/]")
|
|
1007
|
+
elif cmd == "/status":
|
|
1008
|
+
status_command(agent)
|
|
981
1009
|
elif cmd == "/todos":
|
|
982
1010
|
agent.tools.show_todos()
|
|
983
1011
|
elif cmd == "/jobs":
|
|
@@ -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))
|