lcode-cli 0.24.0__tar.gz → 0.26.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 (158) hide show
  1. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/CHANGELOG.md +32 -1
  2. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/PKG-INFO +1 -1
  3. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/docs/automation.md +2 -0
  4. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/docs/bench.md +4 -2
  5. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/docs/configuration.md +1 -0
  6. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/docs/how-it-works.md +13 -0
  7. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/docs/quickstart.md +1 -1
  8. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/docs/usage.md +33 -0
  9. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/pyproject.toml +1 -0
  10. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/src/lcode/__init__.py +1 -1
  11. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/src/lcode/action.py +8 -1
  12. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/src/lcode/agent.py +8 -2
  13. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/src/lcode/api.py +1 -0
  14. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/src/lcode/bench.py +9 -2
  15. lcode_cli-0.26.0/src/lcode/bench_runtime.py +156 -0
  16. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/src/lcode/config.py +1 -0
  17. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/src/lcode/gitflow.py +15 -0
  18. lcode_cli-0.26.0/src/lcode/secretscan.py +315 -0
  19. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/src/lcode/tools.py +25 -2
  20. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/tests/conftest.py +40 -0
  21. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/tests/test_bench.py +25 -0
  22. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/tests/test_gitflow.py +1 -40
  23. lcode_cli-0.26.0/tests/test_secretscan.py +163 -0
  24. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/tests/test_vision.py +9 -0
  25. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/.editorconfig +0 -0
  26. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/.github/CODEOWNERS +0 -0
  27. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  28. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  29. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  30. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/.github/ISSUE_TEMPLATE/model_request.yml +0 -0
  31. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  32. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/.github/dependabot.yml +0 -0
  33. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/.github/workflows/ci.yml +0 -0
  34. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/.github/workflows/docs.yml +0 -0
  35. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/.github/workflows/release.yml +0 -0
  36. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/.gitignore +0 -0
  37. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/.pre-commit-config.yaml +0 -0
  38. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/AGENTS.md +0 -0
  39. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/CODE_OF_CONDUCT.md +0 -0
  40. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/CONTRIBUTING.md +0 -0
  41. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/LICENSE +0 -0
  42. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/README.md +0 -0
  43. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/SECURITY.md +0 -0
  44. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/action.yml +0 -0
  45. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/docs/agents.md +0 -0
  46. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/docs/assets/demo.svg +0 -0
  47. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/docs/assets/extra.css +0 -0
  48. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/docs/assets/logo.svg +0 -0
  49. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/docs/assets/social-preview.png +0 -0
  50. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/docs/changelog.md +0 -0
  51. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/docs/commands.md +0 -0
  52. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/docs/contributing.md +0 -0
  53. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/docs/editors.md +0 -0
  54. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/docs/git.md +0 -0
  55. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/docs/hooks.md +0 -0
  56. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/docs/index.md +0 -0
  57. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/docs/installation.md +0 -0
  58. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/docs/lsp.md +0 -0
  59. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/docs/mcp.md +0 -0
  60. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/docs/memory.md +0 -0
  61. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/docs/models.md +0 -0
  62. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/docs/sandbox.md +0 -0
  63. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/docs/search.md +0 -0
  64. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/docs/servers.md +0 -0
  65. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/docs/troubleshooting.md +0 -0
  66. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/examples/README.md +0 -0
  67. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/examples/api/fix_tests.py +0 -0
  68. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/examples/commands/review.md +0 -0
  69. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/examples/skills/commit-message/SKILL.md +0 -0
  70. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/examples/skills/commit-message/scripts/staged.sh +0 -0
  71. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/install.sh +0 -0
  72. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/mkdocs.yml +0 -0
  73. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/overrides/main.html +0 -0
  74. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/scripts/record_demo.py +0 -0
  75. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/scripts/social-preview.html +0 -0
  76. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/src/lcode/__main__.py +0 -0
  77. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/src/lcode/acp.py +0 -0
  78. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/src/lcode/backends.py +0 -0
  79. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/src/lcode/bench_backend.py +0 -0
  80. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/src/lcode/catalog.py +0 -0
  81. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/src/lcode/checkpoints.py +0 -0
  82. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/src/lcode/cli.py +0 -0
  83. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/src/lcode/clipboard.py +0 -0
  84. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/src/lcode/codesearch.py +0 -0
  85. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/src/lcode/context.py +0 -0
  86. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/src/lcode/extensions.py +0 -0
  87. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/src/lcode/frontmatter.py +0 -0
  88. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/src/lcode/hardware.py +0 -0
  89. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/src/lcode/hooks.py +0 -0
  90. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/src/lcode/jobs.py +0 -0
  91. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/src/lcode/limits.py +0 -0
  92. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/src/lcode/lsp.py +0 -0
  93. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/src/lcode/mcp/__init__.py +0 -0
  94. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/src/lcode/mcp/auth.py +0 -0
  95. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/src/lcode/mcp/catalog.py +0 -0
  96. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/src/lcode/mcp/catalog.toml +0 -0
  97. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/src/lcode/mcp/client.py +0 -0
  98. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/src/lcode/mcp/commands.py +0 -0
  99. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/src/lcode/mcp/config.py +0 -0
  100. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/src/lcode/mcp/http.py +0 -0
  101. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/src/lcode/mcp/manager.py +0 -0
  102. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/src/lcode/mcp/protocol.py +0 -0
  103. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/src/lcode/mcp/server.py +0 -0
  104. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/src/lcode/mcp/stdio.py +0 -0
  105. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/src/lcode/memory.py +0 -0
  106. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/src/lcode/models.toml +0 -0
  107. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/src/lcode/notify.py +0 -0
  108. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/src/lcode/ollama.py +0 -0
  109. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/src/lcode/permissions.py +0 -0
  110. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/src/lcode/planning.py +0 -0
  111. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/src/lcode/render.py +0 -0
  112. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/src/lcode/repeats.py +0 -0
  113. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/src/lcode/repl.py +0 -0
  114. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/src/lcode/repomap.py +0 -0
  115. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/src/lcode/sandbox.py +0 -0
  116. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/src/lcode/servers/__init__.py +0 -0
  117. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/src/lcode/servers/encord.py +0 -0
  118. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/src/lcode/servers/valohai.py +0 -0
  119. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/src/lcode/servers/xai.py +0 -0
  120. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/src/lcode/sessions.py +0 -0
  121. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/src/lcode/subagents.py +0 -0
  122. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/src/lcode/update.py +0 -0
  123. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/src/lcode/vision.py +0 -0
  124. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/src/lcode/web.py +0 -0
  125. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/tests/fake_lsp_server.py +0 -0
  126. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/tests/fake_mcp_server.py +0 -0
  127. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/tests/test_acp.py +0 -0
  128. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/tests/test_action.py +0 -0
  129. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/tests/test_agent.py +0 -0
  130. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/tests/test_api.py +0 -0
  131. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/tests/test_backends.py +0 -0
  132. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/tests/test_catalog.py +0 -0
  133. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/tests/test_checkpoints.py +0 -0
  134. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/tests/test_cli.py +0 -0
  135. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/tests/test_clipboard.py +0 -0
  136. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/tests/test_codesearch.py +0 -0
  137. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/tests/test_config.py +0 -0
  138. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/tests/test_context.py +0 -0
  139. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/tests/test_context_picker.py +0 -0
  140. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/tests/test_extensions.py +0 -0
  141. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/tests/test_hooks.py +0 -0
  142. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/tests/test_jobs.py +0 -0
  143. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/tests/test_lsp.py +0 -0
  144. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/tests/test_mcp.py +0 -0
  145. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/tests/test_memory.py +0 -0
  146. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/tests/test_permissions.py +0 -0
  147. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/tests/test_plan_mode.py +0 -0
  148. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/tests/test_repeats.py +0 -0
  149. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/tests/test_repomap.py +0 -0
  150. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/tests/test_sandbox.py +0 -0
  151. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/tests/test_servers.py +0 -0
  152. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/tests/test_sessions.py +0 -0
  153. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/tests/test_subagents.py +0 -0
  154. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/tests/test_tools.py +0 -0
  155. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/tests/test_update.py +0 -0
  156. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/tests/test_web.py +0 -0
  157. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/tests/test_xai.py +0 -0
  158. {lcode_cli-0.24.0 → lcode_cli-0.26.0}/uv.lock +0 -0
@@ -6,6 +6,35 @@ All notable changes to lcode are documented here. The format follows
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.26.0] - 2026-10-10
10
+
11
+ ### Added
12
+
13
+ - lcode checks for secrets before the model runs `git commit` or `git push`: passwords, keys and
14
+ tokens in code or configuration, credentials in URLs (`rtsp://user:pass@…`), private keys,
15
+ tokens with a known form, and `.env` files, in the lines a commit adds or the commits a push
16
+ sends. In `ask` mode you decide; in `auto-edit` and `yolo` the command doesn't run, and the model
17
+ is told to move the secret out of the code. `/commit` and `/pr` show the findings before you
18
+ confirm, and the GitHub Action doesn't commit at all. `lcode: allow-secret` in a comment allows a
19
+ line; `secret_check = false` turns the check off. See
20
+ [Secrets in commits](https://nasser1941.github.io/lcode/usage/#secrets-in-commits).
21
+
22
+ ## [0.25.0] - 2026-10-10
23
+
24
+ ### Changed
25
+
26
+ - The model is told what counts as checking its work: run what it changed the way it will be used,
27
+ reproduce each reported problem before fixing it, check what it can't see (GUIs, web pages,
28
+ layouts) without a screen, for example with Qt's `offscreen` platform, a headless browser or a
29
+ rendered image, and never call something tested or done without having run it. See
30
+ [Checking its own work](https://nasser1941.github.io/lcode/how-it-works/#checking-its-own-work).
31
+
32
+ ### Added
33
+
34
+ - `lcode bench --tasks runtime-bug`: a bug that shows only when the code runs, a camera wall whose
35
+ window grows with every video frame. Reading the code suggests band-aids that fail its hidden
36
+ test. It isn't in the default set.
37
+
9
38
  ## [0.24.0] - 2026-10-10
10
39
 
11
40
  ### Added
@@ -524,7 +553,9 @@ First public release.
524
553
  - `AGENTS.md` project instructions and `/init` to generate them.
525
554
  - One-line installer for Linux and macOS.
526
555
 
527
- [Unreleased]: https://github.com/nasser1941/lcode/compare/v0.24.0...HEAD
556
+ [Unreleased]: https://github.com/nasser1941/lcode/compare/v0.26.0...HEAD
557
+ [0.26.0]: https://github.com/nasser1941/lcode/compare/v0.25.0...v0.26.0
558
+ [0.25.0]: https://github.com/nasser1941/lcode/compare/v0.24.0...v0.25.0
528
559
  [0.24.0]: https://github.com/nasser1941/lcode/compare/v0.23.0...v0.24.0
529
560
  [0.23.0]: https://github.com/nasser1941/lcode/compare/v0.22.0...v0.23.0
530
561
  [0.22.0]: https://github.com/nasser1941/lcode/compare/v0.21.0...v0.22.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: lcode-cli
3
- Version: 0.24.0
3
+ Version: 0.26.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/
@@ -90,6 +90,8 @@ with Session("path/to/repo", permission_mode="auto-edit") as session:
90
90
  - **Events while it runs:** `session.stream(prompt)` yields the events above, ending with the result.
91
91
  - **Permissions:** pass `approve` to decide permission requests. It gets
92
92
  `{"kind": "bash", "title": "…", "target": "pytest -q"}`; without it, every request is refused.
93
+ A `git commit` or `git push` that would add [what looks like a secret](usage.md#secrets-in-commits)
94
+ comes as `{"kind": "secret", …}` in `ask` mode, and doesn't run in the other modes.
93
95
  - **Options:** `model`, `context`, `permission_mode`, `allowed_tools`, `max_steps`, `think`,
94
96
  `web`, `memory`, `mcp` and `sandbox`. Leave one out to use your
95
97
  [configuration](configuration.md).
@@ -38,8 +38,10 @@ A model usually takes 4 to 12 minutes; each task stops after 5 minutes (`--timeo
38
38
  ### More tasks and features
39
39
 
40
40
  `--tasks find-concept` adds a task in a larger repository: 40 files of a small backend where the
41
- model has to find a function by what it does. It isn't in the default set, so results stay
42
- comparable with earlier versions. `--repo-map` gives the model the [repository map](search.md) and
41
+ model has to find a function by what it does. `--tasks runtime-bug` adds a bug that shows only
42
+ when the code runs: a camera wall's window grows taller with every video frame and can't be made
43
+ smaller again. Reading the code suggests band-aids that fail the hidden test; running it shows the
44
+ cause. Neither is in the default set, so results stay comparable with earlier versions. `--repo-map` gives the model the [repository map](search.md) and
43
45
  `--code-search` indexes each task's folder for [semantic search](search.md#semantic-code-search);
44
46
  both are off in `lcode bench` by default, for the same reason.
45
47
 
@@ -43,6 +43,7 @@ lcode config path # print the file location
43
43
  | `notify` | `true` | A desktop [notification](usage.md#notifications) when a long request is done or waits for your answer |
44
44
  | `notify_after` | `30` | Seconds a request runs before it notifies |
45
45
  | `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 |
46
+ | `secret_check` | `true` | Before `git commit` and `git push`, look for passwords, keys and tokens they'd add ([details](usage.md#secrets-in-commits)) |
46
47
  | `checkpoints` | `true` | Save a checkpoint before the model changes files, so [`/undo`](usage.md#undo-and-checkpoints) can restore them |
47
48
 
48
49
  Besides these settings, `config.toml` can hold [hooks](hooks.md) (`[[hooks]]`) and
@@ -29,6 +29,19 @@ All tools return text, and failures come back to the model as `Error: …` messa
29
29
  crashing the session, so the model can correct itself (for example re-read a file when an edit
30
30
  didn't match).
31
31
 
32
+ ### Checking its own work
33
+
34
+ Small models like to call work done after a syntax check. The system prompt tells the model what
35
+ counts as checked:
36
+
37
+ - run what it changed the way it will be used;
38
+ - reproduce a bug before fixing it, then show it's gone;
39
+ - check what it can't see (a GUI, a web page, a layout) without a screen: Qt's `offscreen`
40
+ platform, a headless browser, or a script that prints sizes or renders an image it then looks
41
+ at;
42
+ - never mark a task done or call something tested without having run it, and tell you what to
43
+ look at when it couldn't check.
44
+
32
45
  ### When the model repeats itself
33
46
 
34
47
  Small models sometimes fall into a loop: they make a call, get a result, and make the very same call
@@ -12,7 +12,7 @@ lcode
12
12
 
13
13
  ```text
14
14
  ╭──────────────────────────────────────────────╮
15
- │ lcode v0.24.0 — local coding agent │
15
+ │ lcode v0.26.0 — local coding agent │
16
16
  │ │
17
17
  │ model lcode-qwen3.6-35b │
18
18
  │ context 256K tokens │
@@ -213,6 +213,39 @@ deny lists, per user or per repository; deny always wins, even in `yolo`. Text a
213
213
  With the [sandbox](sandbox.md) on, `auto-edit` mode runs shell commands without asking too: they can
214
214
  only reach the project, and `/undo` can take their changes back.
215
215
 
216
+ ### Secrets in commits
217
+
218
+ Before the model runs `git commit` or `git push`, lcode checks what would go into the repository's
219
+ history: the lines a commit adds (including files a `git add` in the same command stages), or every
220
+ commit a push would send. It looks for:
221
+
222
+ - passwords, keys and tokens written as values in code or configuration, like
223
+ `"password": "…"` or `API_KEY=…`;
224
+ - credentials in addresses, like `rtsp://admin:…@192.168.1.10`;
225
+ - private keys and tokens with a known form (GitHub, GitLab, AWS, Slack, OpenAI, Anthropic, xAI,
226
+ Google, Hugging Face, Stripe);
227
+ - `.env` files.
228
+
229
+ Placeholders such as `CHANGE_ME`, `your_password`, `${PASSWORD}` or `os.environ["PASSWORD"]` don't
230
+ count.
231
+
232
+ ```text
233
+ This commit would add what looks like a secret to the repository's history:
234
+ - camera_wall.py:42: a password or key: "password": "Xk…"
235
+ ```
236
+
237
+ - **In `ask` mode,** you decide: lcode shows the findings and the command.
238
+ - **In `auto-edit` and `yolo`,** the command doesn't run. The model is told to read the secret from
239
+ an environment variable or a configuration file that git ignores, and to ask you if it isn't one.
240
+ - **`/commit` and `/pr`** show the same findings before you confirm.
241
+ - **The [GitHub Action](automation.md#github-action)** doesn't commit or push at all when it finds
242
+ one.
243
+
244
+ If a line only looks like a secret, add `lcode: allow-secret` (or `gitleaks:allow`) in a comment
245
+ on it. `lcode config set secret_check false` turns the check off. It's a safety net, not a full
246
+ scanner: for a public repository, also run a tool such as [gitleaks](https://github.com/gitleaks/gitleaks)
247
+ before you push.
248
+
216
249
  ## Plan mode
217
250
 
218
251
  Local models are slower than cloud models and make more mistakes, so on a bigger change it pays to
@@ -82,6 +82,7 @@ ignore = [
82
82
  "src/lcode/context.py" = ["E501"]
83
83
  "src/lcode/lsp.py" = ["E501"]
84
84
  "src/lcode/repomap.py" = ["E501"]
85
+ "src/lcode/bench_runtime.py" = ["E501"] # the task's files and hidden test
85
86
  "src/lcode/codesearch.py" = ["E501"]
86
87
  "src/lcode/bench_backend.py" = ["E501"]
87
88
  "src/lcode/backends.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.24.0"
6
+ __version__ = "0.26.0"
7
7
 
8
8
  API = ("Session", "Result", "Options", "SetupError")
9
9
 
@@ -302,7 +302,14 @@ def answer(hub: GitHub, task: Task, inputs: Inputs, cwd: Path) -> None:
302
302
  reply = result.text.strip() or "(no answer)"
303
303
  changed = git(cwd, "status", "--porcelain").strip()
304
304
  if changed:
305
- from lcode import gitflow
305
+ from lcode import gitflow, secretscan
306
+
307
+ found = secretscan.in_files(cwd, ["."], staged=False)
308
+ if found:
309
+ raise ActionError(
310
+ secretscan.report("commit", found, excerpts=False) + "\n\nSo lcode didn't commit or push its "
311
+ "changes. Secrets belong in the repository's secrets or in configuration that git ignores."
312
+ )
306
313
 
307
314
  data = gitflow.ask(
308
315
  session.agent,
@@ -81,14 +81,18 @@ SYSTEM_PROMPT = """You are lcode, an autonomous software-engineering agent runni
81
81
  - Use your tools to gather facts. Never guess file contents, APIs, or command output — read, search, or run them.
82
82
  - 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`.
83
83
  - 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).
84
- - After writing or changing code, verify it: run the script, the tests, or at least a syntax check (e.g. `{python} -m py_compile file.py`). Fix what fails.
85
84
  - Match the existing code style, naming and structure. Don't add unrequested features.
86
85
  - For multi-step tasks, plan with todo_write and keep it updated.
87
86
  - Work autonomously until the task is done. Only stop to ask the user when you are genuinely blocked or the decision is theirs.
88
87
  - Don't run destructive or irreversible commands (rm -rf, git reset --hard, git push --force, dropping data) unless the user explicitly asked.
89
- - Never claim something works without having run it. If something fails, say so and show the relevant error.
90
88
  - If the user denies a tool call, don't retry the same thing; adjust or ask.
91
89
 
90
+ # Verifying your work
91
+ - A syntax check or an import only shows that code loads. Run what you changed the way it will be used (the script, the tests, the app) and fix what fails.
92
+ - To fix a bug, first reproduce each problem the user reports with a test, a command or a short script, then run the same checks to show they're gone.
93
+ - For what you can't see, like a GUI, a web page or a layout, check it without a screen where you can: `QT_QPA_PLATFORM=offscreen` for Qt, a headless browser, or a script that prints sizes and positions or renders to an image{look}. Measure instead of assuming.
94
+ - Never claim something works, mark a todo done, or call it tested unless you ran it. If you couldn't check something, say so and tell the user exactly what to look for. If something fails, say so and show the relevant error.
95
+
92
96
  # Communication
93
97
  - Be concise and direct; no filler. Use GitHub-flavored markdown.
94
98
  - When you finish, give a short summary of what you found or changed and anything the user needs to do.
@@ -229,6 +233,7 @@ class Settings:
229
233
  notify: bool = False # desktop notifications after long requests (lcode.notify)
230
234
  notify_after: int = 30 # seconds: a request this long notifies when it's done or waits for an answer
231
235
  max_parallel_agents: int = 1
236
+ secret_check: bool = True # scan what git commit and git push would add (lcode.secretscan)
232
237
  repeat_limit: int = 3 # identical calls with identical results before the model is told (lcode.repeats)
233
238
 
234
239
 
@@ -317,6 +322,7 @@ class Agent:
317
322
  skills=extensions.skills_prompt(self.extensions().skills, self.settings.context),
318
323
  code=self.code_prompt(),
319
324
  map=self.map_prompt(),
325
+ look=" you look at with view_image" if self.vision_model() else "",
320
326
  )
321
327
 
322
328
  def repo_map(self) -> repomap.RepoMap | None:
@@ -194,6 +194,7 @@ def open_agent(cwd: Path, options: Options, console: Console, hw: Hardware | Non
194
194
  embed_model=cfg["embed_model"],
195
195
  notify=cfg["notify"],
196
196
  notify_after=cfg["notify_after"],
197
+ secret_check=cfg["secret_check"],
197
198
  repeat_limit=cfg["repeat_limit"],
198
199
  )
199
200
  agent = Agent(ollama, settings, cwd, console=console)
@@ -746,7 +746,7 @@ TASKS = [
746
746
 
747
747
  def extra_tasks() -> list[Task]:
748
748
  """Tasks that only run when asked for with --tasks (they're slower, or test optional features)."""
749
- from lcode import bench_backend
749
+ from lcode import bench_backend, bench_runtime
750
750
 
751
751
  return [
752
752
  Task(
@@ -755,7 +755,14 @@ def extra_tasks() -> list[Task]:
755
755
  bench_backend.PROMPT,
756
756
  bench_backend.FILES,
757
757
  bench_backend.check,
758
- )
758
+ ),
759
+ Task(
760
+ "runtime-bug",
761
+ "Fix a bug that shows only when it runs",
762
+ bench_runtime.PROMPT,
763
+ bench_runtime.FILES,
764
+ bench_runtime.check,
765
+ ),
759
766
  ]
760
767
 
761
768
 
@@ -0,0 +1,156 @@
1
+ """A bug that only shows when the code runs, for `lcode bench --tasks runtime-bug`.
2
+
3
+ A camera wall lays out video panels in a window. While video plays, the window grows taller frame after
4
+ frame, past the screen, and can't be made smaller again: each panel's minimum size is taken from the
5
+ frame it shows, and the frame is scaled to the whole cell, status bar included. Reading the code, the
6
+ obvious fixes are band-aids (ignore the minimum sizes, or only fix the scaling) that fail part of the
7
+ hidden test. Running it, the window sizes show the ratchet. It's the kind of bug a GUI has, without
8
+ needing one, and rewards reproducing a problem before fixing it.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import re
14
+ from pathlib import Path
15
+
16
+ WALL = '''\
17
+ """A camera wall: a window with a grid of panels, each showing its camera's latest video frame."""
18
+
19
+ SPACING = 8 # pixels between and around the panels
20
+ STATUS_BAR = 20 # under its video, each panel shows the camera's name and status
21
+
22
+
23
+ class Panel:
24
+ def __init__(self, name):
25
+ self.name = name
26
+ self.min_width = self.min_height = 1 # the panel can't get smaller than this
27
+ self.video = (0, 0) # the size of the frame on screen
28
+
29
+ def show(self, frame_width, frame_height, cell_width, cell_height):
30
+ """Show a frame scaled to fit the panel's cell, keeping its aspect ratio."""
31
+ scale = min(cell_width / frame_width, cell_height / frame_height)
32
+ self.video = (int(frame_width * scale), int(frame_height * scale))
33
+ # The video and the status bar under it must stay visible.
34
+ self.min_width, self.min_height = self.video[0], self.video[1] + STATUS_BAR
35
+
36
+
37
+ class Wall:
38
+ def __init__(self, names, rows, cols):
39
+ self.rows, self.cols = rows, cols
40
+ self.panels = [Panel(name) for name in names]
41
+ self.width = self.height = 0
42
+
43
+ def resize(self, width, height):
44
+ """Ask for a window size. The window can't be smaller than its panels need."""
45
+ need_width = max(p.min_width for p in self.panels) * self.cols + SPACING * (self.cols + 1)
46
+ need_height = max(p.min_height for p in self.panels) * self.rows + SPACING * (self.rows + 1)
47
+ self.width, self.height = max(width, need_width), max(height, need_height)
48
+
49
+ def cell(self):
50
+ """The size of one panel's cell."""
51
+ return (
52
+ (self.width - SPACING * (self.cols + 1)) // self.cols,
53
+ (self.height - SPACING * (self.rows + 1)) // self.rows,
54
+ )
55
+
56
+ def new_frame(self, frame_width, frame_height):
57
+ """Every camera delivered a frame: show it in each panel, then let the layout settle."""
58
+ cell_width, cell_height = self.cell()
59
+ for panel in self.panels:
60
+ panel.show(frame_width, frame_height, cell_width, cell_height)
61
+ self.resize(self.width, self.height)
62
+ '''
63
+
64
+ SIMULATE = '''\
65
+ """Play the camera wall without a screen: python simulate.py [WIDTH HEIGHT [ROWSxCOLS]]"""
66
+
67
+ import sys
68
+
69
+ from wall import Wall
70
+
71
+
72
+ def main():
73
+ width, height = (int(sys.argv[1]), int(sys.argv[2])) if len(sys.argv) > 2 else (2560, 1080)
74
+ rows, cols = map(int, sys.argv[3].split("x")) if len(sys.argv) > 3 else (3, 3)
75
+ wall = Wall([f"camera {n + 1}" for n in range(rows * cols)], rows, cols)
76
+ wall.resize(width * 9 // 10, height * 9 // 10)
77
+ print(f"{rows}x{cols} wall on a {width}x{height} screen, window {wall.width}x{wall.height}")
78
+ for n in range(1, 6):
79
+ wall.new_frame(1920, 1080)
80
+ print(f"after frame {n}: window {wall.width}x{wall.height}, video {wall.panels[0].video}")
81
+
82
+
83
+ if __name__ == "__main__":
84
+ main()
85
+ '''
86
+
87
+ FILES = {"wall.py": WALL, "simulate.py": SIMULATE}
88
+
89
+ PROMPT = (
90
+ "Users say the camera wall in wall.py keeps growing taller while video plays, until it's bigger than "
91
+ "their screen, and after that it can't be made smaller again. Fix it."
92
+ )
93
+
94
+ HIDDEN_TEST = """\
95
+ import unittest
96
+
97
+ from wall import SPACING, STATUS_BAR, Wall
98
+
99
+ SCREENS = [(2560, 1080, 3, 3), (1920, 1080, 2, 3), (1366, 768, 3, 3), (3440, 1440, 2, 4), (1280, 1024, 2, 2)]
100
+
101
+
102
+ def play(width, height, rows, cols, frames=30, frame=(1920, 1080)):
103
+ wall = Wall([f"camera {n}" for n in range(rows * cols)], rows, cols)
104
+ wall.resize(width, height)
105
+ for _ in range(frames):
106
+ wall.new_frame(*frame)
107
+ return wall
108
+
109
+
110
+ class HiddenTest(unittest.TestCase):
111
+ def test_keeps_its_size(self):
112
+ for width, height, rows, cols in SCREENS:
113
+ with self.subTest(screen=(width, height), grid=(rows, cols)):
114
+ wall = play(width * 9 // 10, height * 9 // 10, rows, cols)
115
+ self.assertEqual((wall.width, wall.height), (width * 9 // 10, height * 9 // 10))
116
+
117
+ def test_can_be_made_smaller(self):
118
+ wall = play(2304, 972, 3, 3)
119
+ wall.resize(1280, 720)
120
+ wall.new_frame(1920, 1080)
121
+ self.assertEqual((wall.width, wall.height), (1280, 720))
122
+
123
+ def test_video_fits_and_fills_its_cell(self):
124
+ for width, height, rows, cols in SCREENS:
125
+ for frame in [(1920, 1080), (1280, 960), (720, 1280)]:
126
+ with self.subTest(screen=(width, height), grid=(rows, cols), frame=frame):
127
+ wall = play(width * 9 // 10, height * 9 // 10, rows, cols, frames=3, frame=frame)
128
+ cell_width = (wall.width - SPACING * (cols + 1)) // cols
129
+ cell_height = (wall.height - SPACING * (rows + 1)) // rows
130
+ for panel in wall.panels:
131
+ video_width, video_height = panel.video
132
+ self.assertLessEqual(video_width, cell_width)
133
+ self.assertLessEqual(video_height + STATUS_BAR, cell_height)
134
+ fills = video_width >= cell_width - 2 or video_height + STATUS_BAR >= cell_height - 2
135
+ self.assertTrue(fills, "the video doesn't use the room it has")
136
+ self.assertAlmostEqual(video_width / video_height, frame[0] / frame[1], delta=0.02)
137
+ """
138
+
139
+ PROBLEMS = {
140
+ "test_keeps_its_size": "the window still grows",
141
+ "test_can_be_made_smaller": "the window can't be made smaller",
142
+ "test_video_fits_and_fills_its_cell": "the video doesn't fit its cell above the status bar (or doesn't fill it)",
143
+ }
144
+
145
+
146
+ def check(folder: Path, answer: str):
147
+ from lcode.bench import Check, last_line, run_python
148
+
149
+ if not (folder / "wall.py").is_file():
150
+ return Check(False, "wall.py is missing")
151
+ (folder / "test_hidden_wall.py").write_text(HIDDEN_TEST)
152
+ r = run_python(folder, "-m", "unittest", "-q", "test_hidden_wall")
153
+ if r.returncode == 0:
154
+ return Check(True, "keeps its size, can be made smaller, and the video fits")
155
+ failed = [text for name, text in PROBLEMS.items() if re.search(rf"\b{name}\b", r.stderr)]
156
+ return Check(False, "; ".join(failed) or last_line(r.stderr) or last_line(r.stdout))
@@ -60,6 +60,7 @@ SETTINGS: dict[str, tuple[object, type, str]] = {
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
62
  "repeat_limit": (3, int, "identical tool calls with identical results before lcode steps in (0: never)"),
63
+ "secret_check": (True, bool, "before git commit and git push, look for passwords, keys and tokens they'd add"),
63
64
  "max_parallel_agents": (1, int, "subagents that may run at the same time (more needs OLLAMA_NUM_PARALLEL)"),
64
65
  }
65
66
  ENV_OVERRIDES = {
@@ -24,6 +24,7 @@ from typing import TYPE_CHECKING
24
24
 
25
25
  from rich.markup import escape
26
26
  from rich.panel import Panel
27
+ from rich.text import Text
27
28
 
28
29
  if TYPE_CHECKING:
29
30
  from lcode.agent import Agent
@@ -316,6 +317,9 @@ def commit_command(agent: Agent, hint: str = "") -> None:
316
317
  chosen = [f for f in answer.get("files") or [] if f in paths]
317
318
  files = chosen or paths
318
319
  left_out = [p for p in paths if p not in files]
320
+ from lcode import secretscan
321
+
322
+ secrets_found = secretscan.in_files(root, files, bool(staged)) if agent.settings.secret_check else []
319
323
  while True:
320
324
  c.print(Panel(escape(message), title="Commit message", title_align="left", border_style="cyan"))
321
325
  c.print(f" [bold]Files[/] {escape(', '.join(files))}")
@@ -323,6 +327,9 @@ def commit_command(agent: Agent, hint: str = "") -> None:
323
327
  c.print(f" [dim]Left out (unrelated, the model thinks): {escape(', '.join(left_out))}[/]")
324
328
  if risky:
325
329
  c.print(f" [dim]Left out (may hold secrets; stage them yourself if needed): {escape(', '.join(risky))}[/]")
330
+ if secrets_found:
331
+ c.print(Text(secretscan.report("commit", secrets_found), style="red"))
332
+ c.print(" [dim]Commit them only if they aren't secrets; otherwise move them out of the code first.[/]")
326
333
  choice = confirm(agent, " [y] commit · [e] edit the message · [n] cancel: ", message)
327
334
  if choice == "e":
328
335
  from lcode.planning import edit_text
@@ -552,6 +559,14 @@ def pr_command(agent: Agent, arg: str = "") -> None:
552
559
 
553
560
 
554
561
  def push(agent: Agent, root: Path, remote: str, branch: str) -> bool:
562
+ from lcode import secretscan
563
+
564
+ found = secretscan.outgoing(root, branch) if agent.settings.secret_check else []
565
+ if found:
566
+ agent.console.print(Text(secretscan.report("push", found), style="red"))
567
+ if confirm(agent, " Push anyway? [y]es / [n]o: ") != "y":
568
+ agent.console.print("Not pushed.")
569
+ return False
555
570
  try:
556
571
  with agent.console.status(f"Pushing {escape(branch)}…", spinner="dots"):
557
572
  git(root, "push", "-q", "-u", remote, branch, timeout=300)