krodo 0.1.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 (117) hide show
  1. krodo-0.1.0/.github/workflows/ci.yml +64 -0
  2. krodo-0.1.0/.gitignore +43 -0
  3. krodo-0.1.0/.krodo/config.yaml +8 -0
  4. krodo-0.1.0/.krodoignore +12 -0
  5. krodo-0.1.0/.python-version +1 -0
  6. krodo-0.1.0/AGENTS.md +407 -0
  7. krodo-0.1.0/CHANGELOG.md +116 -0
  8. krodo-0.1.0/CONTRIBUTING.md +169 -0
  9. krodo-0.1.0/LICENSE +201 -0
  10. krodo-0.1.0/PKG-INFO +421 -0
  11. krodo-0.1.0/README.md +386 -0
  12. krodo-0.1.0/README.zh-CN.md +393 -0
  13. krodo-0.1.0/SECURITY.md +178 -0
  14. krodo-0.1.0/docs/ARCHITECTURE_DESIGN-GLM.md.archive +698 -0
  15. krodo-0.1.0/docs/MODELS.md +279 -0
  16. krodo-0.1.0/docs/QUICKSTART.md +187 -0
  17. krodo-0.1.0/docs/architecture.md +1228 -0
  18. krodo-0.1.0/docs/fixes/001-approval-prompt-missing-tool-args.md +117 -0
  19. krodo-0.1.0/docs/reviews/ARCHITECTURE_REVIEW_v0.2.md +304 -0
  20. krodo-0.1.0/docs/reviews/ARCHITECTURE_REVIEW_v0.3.md +156 -0
  21. krodo-0.1.0/docs/reviews/ARCHITECTURE_REVIEW_v0.4.md +75 -0
  22. krodo-0.1.0/docs/reviews/PLAN_REVIEW_fix-subcommand-routing.md +229 -0
  23. krodo-0.1.0/pyproject.toml +136 -0
  24. krodo-0.1.0/scripts/prototype.py +528 -0
  25. krodo-0.1.0/src/krodo/__init__.py +0 -0
  26. krodo-0.1.0/src/krodo/cli/__init__.py +0 -0
  27. krodo-0.1.0/src/krodo/cli/banner.py +71 -0
  28. krodo-0.1.0/src/krodo/cli/diff_preview.py +87 -0
  29. krodo-0.1.0/src/krodo/cli/doctor.py +276 -0
  30. krodo-0.1.0/src/krodo/cli/group.py +202 -0
  31. krodo-0.1.0/src/krodo/cli/main.py +692 -0
  32. krodo-0.1.0/src/krodo/cli/repl.py +306 -0
  33. krodo-0.1.0/src/krodo/cli/resume.py +490 -0
  34. krodo-0.1.0/src/krodo/cli/undo.py +245 -0
  35. krodo-0.1.0/src/krodo/core/__init__.py +25 -0
  36. krodo-0.1.0/src/krodo/core/budget.py +248 -0
  37. krodo-0.1.0/src/krodo/core/compression.py +343 -0
  38. krodo-0.1.0/src/krodo/core/config.py +141 -0
  39. krodo-0.1.0/src/krodo/core/context.py +160 -0
  40. krodo-0.1.0/src/krodo/core/events.py +195 -0
  41. krodo-0.1.0/src/krodo/core/loop.py +671 -0
  42. krodo-0.1.0/src/krodo/core/recovery.py +317 -0
  43. krodo-0.1.0/src/krodo/core/types.py +100 -0
  44. krodo-0.1.0/src/krodo/core/workspace.py +153 -0
  45. krodo-0.1.0/src/krodo/llm/__init__.py +5 -0
  46. krodo-0.1.0/src/krodo/llm/litellm_provider.py +338 -0
  47. krodo-0.1.0/src/krodo/llm/protocols.py +68 -0
  48. krodo-0.1.0/src/krodo/llm/streaming.py +132 -0
  49. krodo-0.1.0/src/krodo/memory/__init__.py +12 -0
  50. krodo-0.1.0/src/krodo/memory/agents_md.py +276 -0
  51. krodo-0.1.0/src/krodo/memory/replay.py +196 -0
  52. krodo-0.1.0/src/krodo/memory/store.py +300 -0
  53. krodo-0.1.0/src/krodo/obs/__init__.py +5 -0
  54. krodo-0.1.0/src/krodo/obs/cost.py +75 -0
  55. krodo-0.1.0/src/krodo/obs/logger.py +191 -0
  56. krodo-0.1.0/src/krodo/sandbox/__init__.py +5 -0
  57. krodo-0.1.0/src/krodo/sandbox/approval.py +293 -0
  58. krodo-0.1.0/src/krodo/sandbox/checkpoint.py +207 -0
  59. krodo-0.1.0/src/krodo/sandbox/firewall.py +131 -0
  60. krodo-0.1.0/src/krodo/sandbox/ignore.py +260 -0
  61. krodo-0.1.0/src/krodo/sandbox/path_filter.py +81 -0
  62. krodo-0.1.0/src/krodo/sandbox/protocols.py +55 -0
  63. krodo-0.1.0/src/krodo/tools/__init__.py +5 -0
  64. krodo-0.1.0/src/krodo/tools/builtin/__init__.py +0 -0
  65. krodo-0.1.0/src/krodo/tools/builtin/fs.py +322 -0
  66. krodo-0.1.0/src/krodo/tools/builtin/git.py +241 -0
  67. krodo-0.1.0/src/krodo/tools/builtin/patch.py +315 -0
  68. krodo-0.1.0/src/krodo/tools/builtin/search.py +385 -0
  69. krodo-0.1.0/src/krodo/tools/builtin/shell.py +121 -0
  70. krodo-0.1.0/src/krodo/tools/protocols.py +79 -0
  71. krodo-0.1.0/src/krodo/tools/registry.py +63 -0
  72. krodo-0.1.0/tests/e2e/__init__.py +0 -0
  73. krodo-0.1.0/tests/integration/__init__.py +0 -0
  74. krodo-0.1.0/tests/integration/test_cli_e2e.py +394 -0
  75. krodo-0.1.0/tests/integration/test_cli_repl.py +274 -0
  76. krodo-0.1.0/tests/integration/test_cli_stdin.py +103 -0
  77. krodo-0.1.0/tests/integration/test_cli_subcommand_routing.py +412 -0
  78. krodo-0.1.0/tests/integration/test_long_session.py +209 -0
  79. krodo-0.1.0/tests/integration/test_repl_continue.py +136 -0
  80. krodo-0.1.0/tests/integration/test_repl_slash.py +226 -0
  81. krodo-0.1.0/tests/integration/test_resume_e2e.py +225 -0
  82. krodo-0.1.0/tests/integration/test_undo_e2e.py +184 -0
  83. krodo-0.1.0/tests/unit/__init__.py +0 -0
  84. krodo-0.1.0/tests/unit/test_agents_md.py +221 -0
  85. krodo-0.1.0/tests/unit/test_apply_patch.py +242 -0
  86. krodo-0.1.0/tests/unit/test_approval.py +241 -0
  87. krodo-0.1.0/tests/unit/test_approval_persistence.py +292 -0
  88. krodo-0.1.0/tests/unit/test_budget.py +282 -0
  89. krodo-0.1.0/tests/unit/test_checkpoint.py +270 -0
  90. krodo-0.1.0/tests/unit/test_cli_group.py +351 -0
  91. krodo-0.1.0/tests/unit/test_compression.py +379 -0
  92. krodo-0.1.0/tests/unit/test_config.py +173 -0
  93. krodo-0.1.0/tests/unit/test_cost.py +260 -0
  94. krodo-0.1.0/tests/unit/test_diff_preview.py +69 -0
  95. krodo-0.1.0/tests/unit/test_edit_file.py +150 -0
  96. krodo-0.1.0/tests/unit/test_events.py +247 -0
  97. krodo-0.1.0/tests/unit/test_firewall.py +157 -0
  98. krodo-0.1.0/tests/unit/test_fs_tools.py +227 -0
  99. krodo-0.1.0/tests/unit/test_git_tools.py +258 -0
  100. krodo-0.1.0/tests/unit/test_ignore.py +257 -0
  101. krodo-0.1.0/tests/unit/test_imports.py +31 -0
  102. krodo-0.1.0/tests/unit/test_litellm_provider.py +399 -0
  103. krodo-0.1.0/tests/unit/test_logger.py +119 -0
  104. krodo-0.1.0/tests/unit/test_loop.py +1096 -0
  105. krodo-0.1.0/tests/unit/test_obs.py +148 -0
  106. krodo-0.1.0/tests/unit/test_path_filter.py +107 -0
  107. krodo-0.1.0/tests/unit/test_recovery.py +349 -0
  108. krodo-0.1.0/tests/unit/test_replay.py +375 -0
  109. krodo-0.1.0/tests/unit/test_resume_history_display.py +272 -0
  110. krodo-0.1.0/tests/unit/test_search_tools.py +402 -0
  111. krodo-0.1.0/tests/unit/test_session_store.py +295 -0
  112. krodo-0.1.0/tests/unit/test_shell_tool.py +131 -0
  113. krodo-0.1.0/tests/unit/test_streaming.py +398 -0
  114. krodo-0.1.0/tests/unit/test_tool_registry.py +102 -0
  115. krodo-0.1.0/tests/unit/test_undo_command.py +294 -0
  116. krodo-0.1.0/tests/unit/test_workspace.py +282 -0
  117. krodo-0.1.0/uv.lock +2288 -0
@@ -0,0 +1,64 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ # Only one CI run per branch — newer pushes cancel older in-flight runs.
10
+ concurrency:
11
+ group: ${{ github.workflow }}-${{ github.ref }}
12
+ cancel-in-progress: true
13
+
14
+ jobs:
15
+ lint-type-test:
16
+ name: Lint, type-check, test (Python ${{ matrix.python-version }} on ${{ matrix.os }})
17
+ runs-on: ${{ matrix.os }}
18
+ strategy:
19
+ fail-fast: false
20
+ matrix:
21
+ os: [ubuntu-latest, macos-latest]
22
+ python-version: ["3.12"]
23
+
24
+ steps:
25
+ - name: Checkout
26
+ uses: actions/checkout@v4
27
+
28
+ - name: Install uv
29
+ uses: astral-sh/setup-uv@v3
30
+ with:
31
+ enable-cache: true
32
+ cache-dependency-glob: "uv.lock"
33
+
34
+ - name: Set up Python ${{ matrix.python-version }}
35
+ run: uv python install ${{ matrix.python-version }}
36
+
37
+ - name: Install dependencies
38
+ run: uv sync --all-groups --frozen
39
+
40
+ - name: Lint (ruff)
41
+ run: uv run ruff check .
42
+
43
+ - name: Format check (ruff)
44
+ run: uv run ruff format --check .
45
+
46
+ - name: Type check (mypy --strict)
47
+ run: uv run mypy src
48
+
49
+ - name: Run tests with coverage
50
+ run: uv run pytest --cov-report=xml
51
+
52
+ - name: Upload coverage artifact
53
+ if: matrix.os == 'ubuntu-latest'
54
+ uses: actions/upload-artifact@v4
55
+ with:
56
+ name: coverage-report
57
+ path: |
58
+ coverage.xml
59
+ htmlcov/
60
+ if-no-files-found: ignore
61
+ retention-days: 14
62
+
63
+ # Phase 2.6 will add a provider matrix job (Claude / GPT / Gemini / Ollama)
64
+ # that runs against recorded VCR cassettes. Stub left intentionally for now.
krodo-0.1.0/.gitignore ADDED
@@ -0,0 +1,43 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ build/
6
+ dist/
7
+ wheels/
8
+ *.egg-info
9
+
10
+ # Virtual environments
11
+ .venv/
12
+
13
+ # IDE
14
+ .idea/
15
+ .vscode/
16
+ *.swp
17
+ *.swo
18
+
19
+ # OS
20
+ .DS_Store
21
+ Thumbs.db
22
+
23
+ # Environment
24
+ .env
25
+ .env.*
26
+ !.env.example
27
+
28
+ # Tool caches
29
+ .pytest_cache/
30
+ .mypy_cache/
31
+ .ruff_cache/
32
+ .coverage
33
+ .coverage.*
34
+ htmlcov/
35
+ coverage.xml
36
+
37
+ # Krodo state
38
+ .krodo/cache/
39
+ .krodo/sessions/
40
+ .krodo/logs/
41
+
42
+ # Cursor state
43
+ .cursor/
@@ -0,0 +1,8 @@
1
+ # 模型名遵循 LiteLLM 命名约定:<provider>/<model-id>
2
+ # 完整列表 https://docs.litellm.ai/docs/providers
3
+ # 例:zai/glm-4.6 / anthropic/claude-sonnet-4-5-20250929 / openai/gpt-5 / gemini/gemini-2.5-pro
4
+ # 也可直接用 OpenAI Chat Completions 兼容名(如 ollama/llama3.1)
5
+ model: zai/glm-4.6
6
+ approval: auto_edit # read_only | auto_edit | full_auto
7
+ max_tool_calls: 15 # 达到 10 给 soft warning,15 强制摘要
8
+ # soft_warning_at 不是 KrodoConfig 字段,krodo 内部硬编码为 max_tool_calls 的 2/3
@@ -0,0 +1,12 @@
1
+ .env
2
+ .env.*
3
+ *.pem
4
+ *.key
5
+ id_rsa*
6
+ credentials.json
7
+ node_modules/
8
+ .venv/
9
+ __pycache__/
10
+ dist/
11
+ build/
12
+ target/
@@ -0,0 +1 @@
1
+ 3.12
krodo-0.1.0/AGENTS.md ADDED
@@ -0,0 +1,407 @@
1
+ # Project memory: Krodo
2
+
3
+ > This file is auto-loaded into the system prompt of every Krodo session running in this repository (see `docs/architecture.md` §5.6). Keep it concise — total budget is 8K tokens.
4
+
5
+ ## What this project is
6
+
7
+ Krodo is a local-first, multi-provider coding agent CLI. We are currently in **Phase 1 (MVP)**: building a usable REPL + headless `krodo exec` with 11 tools, three approval modes, automatic git checkpoints, JSONL-backed sessions, and AGENTS.md project memory. We are explicitly **not** building TUI / RAG / Docker sandbox / IDE plugins / Web UI in this phase.
8
+
9
+ The single source of truth for design decisions is [`docs/architecture.md`](docs/architecture.md). When in doubt, read it first; if the design is unclear or wrong, update the doc in the same PR as the code.
10
+
11
+ ## Engineering rules (the seven principles, see architecture.md §11)
12
+
13
+ 1. Make it work first; optimize later.
14
+ 2. **Protocol first** — every core module begins as a `typing.Protocol` in `src/krodo/<module>/base.py`.
15
+ 3. **Safe by default** — deny unless explicitly allowed; dangerous ops always prompt; sensitive files always ignored.
16
+ 4. **Day 1 observability** — every tool call gets a trace span; every LLM call records tokens + cost. PRs without trace/log are not merged.
17
+ 5. **Git is the safety net** — checkpoint with `git stash create` before every write; `krodo undo` restores.
18
+ 6. Extract shared abstractions only after a pattern repeats across **three** modules.
19
+ 7. **Pin major versions** of LLM/agent core dependencies (`litellm>=1.40,<2`, `anthropic` SDK, etc.). Minor upgrades require a PR review and a passing regression run.
20
+
21
+ ## Code conventions
22
+
23
+ - Python **3.12+ only**. Use modern syntax: `list[int]`, `int | None`, `match` statements, `@override`.
24
+ - Strict typing: `mypy --strict` must pass with zero errors. No `# type: ignore` without an inline reason.
25
+ - Lint: `ruff check` with rules `E F I N W UP B S ASYNC T20`. **Never use `print()` in `src/`** — use `structlog`.
26
+ - `subprocess.run(shell=True)` is forbidden in production code (bandit S602). If you need shell semantics, go through `krodo.sandbox.shell.run()` which validates against the dangerous-command policy.
27
+ - Test layout: `tests/unit/` mirrors `src/krodo/`; `tests/integration/` for cross-module tests; `tests/e2e/` for full-loop tests with VCR-recorded LLM responses.
28
+ - Coverage target: ≥ 90% for `krodo.core` / `krodo.llm`; 100% for `krodo.tools` / `krodo.sandbox`.
29
+ - All new tools must:
30
+ 1. Define a `pydantic.BaseModel` for arguments.
31
+ 2. Return a `ToolResult` (never raise to the loop).
32
+ 3. Mark `requires_approval` correctly (default = read-only, write/exec = True).
33
+ 4. Have a corresponding entry in `tests/unit/tools/test_<tool>.py` and a recorded VCR cassette in `tests/e2e/`.
34
+
35
+ ## Module dependency rules (enforced via `import-linter` in CI)
36
+
37
+ - `core` may depend on `llm` / `tools` / `sandbox` / `memory` / `obs`. Never the reverse.
38
+ - `tools` may depend on `sandbox` / `memory` / `obs`. Never on `core`.
39
+ - `cli` and `tui` are thin layers over `core`'s facade — they never construct messages or call LLMs directly.
40
+
41
+ ## Common commands
42
+
43
+ ```bash
44
+ uv sync # install / sync deps
45
+ uv run pytest # all tests
46
+ uv run pytest tests/unit # unit only (fast)
47
+ uv run pytest --cov-report=html # coverage report → htmlcov/
48
+ uv run ruff check . --fix # lint + autofix
49
+ uv run mypy src # type-check
50
+ uv run python scripts/prototype.py # Phase 0 prototype (when written)
51
+ ```
52
+
53
+
54
+ ## M2 tools: the 8 new tools added in Phase 1 M2
55
+
56
+ M2 extended the 3 M1 tools to a full set of 11:
57
+
58
+ ### Search / read tools (no approval needed)
59
+ - **`list_dir`** — list directory with depth control (1–10). Skips noise dirs (node_modules, __pycache__, .git, .venv, etc.).
60
+ - **`glob`** — find files by pathlib glob pattern. Filters via `filter_allowed_paths()` (symlink-safe).
61
+ - **`grep`** — regex search in files. Prefers ripgrep (`rg`) for speed; Python `re` fallback. Install ripgrep for large codebases.
62
+
63
+ ### Edit / patch tools (require approval in auto_edit mode)
64
+ - **`edit_file`** — precise string replacement. `old_string` must be unique unless `replace_all=true`. On ambiguity, reports line numbers to help the model narrow context.
65
+ - **`apply_patch`** — apply a unified diff (udiff). Atomic transaction: snapshots all targets before writing; rolls back all on any failure. Handles LF/CRLF transparently.
66
+
67
+ ### Git tools (status/diff = no approval; commit = approval)
68
+ - **`git_status`** — `git status --porcelain` via GitPython. Error if not in a git repo.
69
+ - **`git_diff`** — unified diff; supports `staged=true` and path filter.
70
+ - **`git_commit`** — commit staged files. `add_all=true` runs `git add -u` first. Auto-redacts API key literals from commit messages.
71
+
72
+ ### Path safety (all tools)
73
+ `sandbox/path_filter.py` provides `filter_allowed_paths()` and `is_noise_dir()`, used by bulk-result tools to silently drop out-of-workspace paths and noise directories.
74
+
75
+ ### Approval modes (M2 complete)
76
+ - `read_only` — only read/search/status tools; write/shell always denied.
77
+ - `auto_edit` (default) — reads auto-approved; writes prompt `[y/n/a/p/?]`.
78
+ - `p` enters a pattern rule: `<tool_name> <glob>` (e.g. `run_shell pytest*`).
79
+ - Pattern rules stored in memory; persisted via `JsonlSessionStore` in M5.
80
+ - `full_auto` — all tools auto-approved; red warning banner printed at startup.
81
+
82
+ ## M3: Token budget, dual compression, and error recovery (Phase 1 M3)
83
+
84
+ M3 brings context-window safety and centralised error recovery to the agent loop.
85
+
86
+ ### Token budget (§3.4.1)
87
+
88
+ `src/krodo/core/budget.py` — `BudgetCalculator`:
89
+ - Total budget = model context window × 0.80
90
+ - Output reserve = total_budget × 0.15
91
+ - `check(messages)` → `BudgetStatus(action: BudgetAction)`: OK / COMPRESS / TRUNCATE / REFUSE
92
+ - `MODEL_CONTEXT_WINDOW` table covers ~20 common models (conservative 95% values)
93
+ - `KRODO_TOKEN_RATIO` env var overrides the per-model ratio (Claude = 1.1× default)
94
+
95
+ ### Dual compression (`KRODO_COMPRESS`)
96
+
97
+ `src/krodo/core/compression.py` — `make_compressor(strategy, provider)`:
98
+ - `llm` (default): calls the same LLMProvider to summarise oldest N rounds into a `<SUMMARY>` block; emits `SessionEvent(COMPRESSION)` with cost.
99
+ - `algorithmic`: drops `tool_result` content; keeps tool-call metadata + file paths; zero extra LLM cost.
100
+ - **Pinned context**: most-recent 5 file paths from tool_call args + last user message are **never** compressed.
101
+ - Compression is triggered before each `provider.chat()` call (§4.9 of M3 plan).
102
+
103
+ ### Error recovery (`src/krodo/core/recovery.py`)
104
+
105
+ `handle(RecoveryContext) -> (RecoveryAction, str)` dispatches 7 scenarios:
106
+
107
+ | # | `error_kind` | `RecoveryAction` | Behaviour |
108
+ |---|-------------|-----------------|-----------|
109
+ | 1 | `bad_json` | RETRY / ABORT | Re-inject schema+error; retry ×2 |
110
+ | 2 | `tool_timeout` | SKIP | Kill; inject partial-result stub |
111
+ | 3 | `stall` | ABORT | Print last 3 calls; abort turn |
112
+ | 4 | `context_loss` | RETRY | Re-inject pinned file paths |
113
+ | 5 | `sha256_conflict` | SKIP | Block write; ask for re-read |
114
+ | 6 | `provider_error` | RETRY / ABORT | Exp backoff 1s/2s/4s ×3 |
115
+ | 7 | `eacces` | SKIP | Report path + permission bits |
116
+
117
+ `StallDetector`: tracks write-tool-call signatures; raises `StallError` at 3 consecutive identical calls (read-only tools excluded).
118
+
119
+ ### SHA-256 conflict detection (scenario 5)
120
+
121
+ `read_file` caches the SHA-256 of the file at read-time in a module-level `_sha256_cache`. `edit_file` and `apply_patch` validate the cache before writing; if the on-disk hash differs, they return an error message asking the agent to re-read the file.
122
+
123
+ ### SessionEvent stream (`src/krodo/core/events.py`)
124
+
125
+ `SessionEventLogger` wraps the existing JSONL logger with:
126
+ - Typed `emit(SessionEventType, data)` → `SessionEvent`
127
+ - `emit_from(event)` — for compressor-generated events (overwrites session_id + seq)
128
+ - Monotonically-increasing `seq` counter
129
+ - Session events: `<workspace>/.krodo/sessions/<session_id>.jsonl`
130
+ - App logs: `<workspace>/.krodo/logs/<session_id>.log`
131
+ - Factory: `SessionEventLogger.from_store(session_id, store)` (preferred) or `from_workspace_path` (legacy)
132
+
133
+ Events emitted by AgentLoop: `USER_MESSAGE`, `ASSISTANT_MESSAGE`, `TOOL_CALL`, `APPROVAL_DECISION`, `TOOL_RESULT`, `COMPRESSION`, `ERROR`.
134
+
135
+ ### New CLI flags (M3)
136
+
137
+ - `--max-tool-calls N` — tool calls per turn limit (default 25; REPL offers to continue when hit)
138
+ - `--summary-window N` — dialogue rounds to compress in one pass (default 2)
139
+ - Startup banner now shows: model context window / compression strategy / max tool calls
140
+
141
+ ## M4: `.krodoignore`, Git checkpoint, `krodo undo`, diff preview
142
+
143
+ ### KrodoIgnore (`src/krodo/sandbox/ignore.py`)
144
+
145
+ `KrodoIgnore` merges 4 tiers of ignore rules (backed by `pathspec` gitignore semantics):
146
+
147
+ | Tier | Source | Always active? |
148
+ |------|--------|----------------|
149
+ | 1 | Hard-coded defaults: `.env`, `*.pem`, `id_rsa`, `node_modules/`, `__pycache__/`, etc. | ✅ yes |
150
+ | 2 | Project `.gitignore` | — |
151
+ | 3 | `<workspace_root>/.krodoignore` | — |
152
+ | 4 | `~/.config/krodo/krodoignore` | — |
153
+
154
+ **API**: `ignore.match(path) -> MatchResult`; `ignore.is_ignored(path) -> bool`.
155
+
156
+ All read tools (`read_file`, `list_dir`, `glob`, `grep`) check `ctx.ignore.match()` before accessing the path. On match, they return `PathIgnoredError: '<path>' is ignored (rule: '<pattern>' from <source>)` as `ToolResult(is_error=True)`.
157
+
158
+ One `KrodoIgnore` instance is constructed at session start and injected via `ToolContext.ignore`.
159
+
160
+ ### GitCheckpointManager (`src/krodo/sandbox/checkpoint.py`)
161
+
162
+ `GitCheckpointManager.create(affected_paths) -> str | None`:
163
+ - Runs `git stash create` (does **not** push to stash stack; working tree untouched).
164
+ - Returns the stash SHA on success, `None` on clean tree or non-git workspace.
165
+ - Logs a warning and returns `None` on non-git workspaces (no crash).
166
+
167
+ `GitCheckpointManager.restore(sha, paths)`:
168
+ - Runs `git checkout <sha> -- <paths>` (only touches the listed paths).
169
+ - Raises `CheckpointError` on non-git workspace or bad SHA.
170
+
171
+ `shell_command_writes(cmd) -> bool`: heuristic that returns `True` if *cmd* likely modifies the filesystem (redirect `>`, `rm`, `mv`, `sed -i`, `tee`, `wget`, etc.).
172
+
173
+ Write tools wire-up:
174
+ - `write_file`, `edit_file` → `create([target_path])` before writing.
175
+ - `apply_patch` → `create(all_affected_paths)` before applying.
176
+ - `run_shell` → `create([workspace_root])` when `shell_command_writes()` returns `True`.
177
+
178
+ Each checkpoint emits a `CHECKPOINT` `SessionEvent` via `ctx.event_logger`.
179
+
180
+ ### `krodo undo` (`src/krodo/cli/undo.py`)
181
+
182
+ Typer subcommand registered in `main.py`:
183
+
184
+ ```bash
185
+ krodo undo [--root <workspace>] [--session <session_id>]
186
+ ```
187
+
188
+ Behaviour:
189
+ 1. Find the session JSONL (`<workspace>/.krodo/logs/<session_id>.jsonl`; defaults to most-recent).
190
+ 2. Find the latest `CHECKPOINT` event (by `seq`).
191
+ 3. Call `GitCheckpointManager.restore(sha, affected_paths)`.
192
+ 4. Emit `UNDO` `SessionEvent`.
193
+ 5. Non-git workspace or no CHECKPOINT → exit 1 with friendly error.
194
+ 6. If `affected_paths == [workspace_root]` (shell command scope) → prompt for confirmation.
195
+
196
+ ### Diff preview (`src/krodo/cli/diff_preview.py`)
197
+
198
+ `render_diff(old, new, path) -> rich.syntax.Syntax`: returns a Rich `Syntax` object showing a unified diff, truncated to 200 lines.
199
+
200
+ `render_new_file(content, path) -> Syntax`: convenience wrapper for new files (`old=None`).
201
+
202
+ `TerminalApprovalManager._maybe_render_diff()` renders diffs for `write_file`, `edit_file`, `apply_patch` before the `y/n` approval prompt in `auto_edit` mode.
203
+
204
+ ### Banner update (`src/krodo/cli/banner.py`)
205
+
206
+ Session banner now shows a `git: <root> | none` line reflecting `workspace.git_root`.
207
+
208
+ ### New dependency
209
+
210
+ `pathspec>=0.12,<1` added to `pyproject.toml` (major version pinned per engineering rule 7).
211
+
212
+ ## M4.9: Interactive REPL (multi-turn dialogue)
213
+
214
+ The CLI now has two entry shapes — both share the *same* `AgentLoop`
215
+ instance, so history persists naturally:
216
+
217
+ ```bash
218
+ krodo "one-shot task" # headless: run once and exit (unchanged)
219
+ krodo # REPL: read → run → repeat
220
+ ```
221
+
222
+ ### Implementation map
223
+
224
+ - `src/krodo/cli/main.py`
225
+ - `_build_session_components(...)` → returns a `SessionComponents`
226
+ bundle (workspace + AgentLoop + logger + session_id + event_logger
227
+ + log_path + max_tokens).
228
+ - `_run_headless(prompt, components)` → one `loop.run` + headless
229
+ summary (preserves pre-M4.9 behaviour byte-for-byte).
230
+ - `ABORT_REASONS` / `_echo_turn_result` / `_collect_written_paths`
231
+ are module-level so the REPL can reuse them.
232
+ - `src/krodo/cli/repl.py`
233
+ - `run_repl(components)` — the multi-turn loop.
234
+ - `input()` runs in `asyncio.to_thread` so it doesn't block the event loop.
235
+ - Exit tokens: `exit` / `quit` / `:q` / `\q`; Ctrl-D (EOFError) exits
236
+ immediately; Ctrl-C at an empty prompt requires a second press
237
+ (`last_ctrl_c` sentinel, mirrors Python/IPython UX).
238
+ - Ctrl-C *during* a turn cancels just that turn (does **not** exit REPL).
239
+ - Empty / whitespace-only input is silently skipped.
240
+ - Session summary is printed exactly once at exit (`print_session_summary`
241
+ in `main.py`); per-turn output is just the model answer or abort message.
242
+
243
+ ### What carries across REPL turns
244
+
245
+ | State | Lifetime |
246
+ |---|---|
247
+ | `session_id` / `SessionEventLogger` / JSONL log path | whole REPL session |
248
+ | `GitCheckpointManager` | whole REPL session (checkpoints accumulate) |
249
+ | `AgentLoop.context_manager` (conversation history) | whole REPL session |
250
+ | `StallDetector` / `tool_calls_made` / `invalid_args_retry` | per turn (reset by `loop.run`) |
251
+ | Banner / `full_auto` warning | printed once at session start |
252
+
253
+ ### What's deliberately deferred to M6
254
+
255
+ - `prompt_toolkit` upgrade (arrow-key history, multi-line, syntax highlighting)
256
+ - Slash commands (`:undo`, `:tokens`, `:clear`, `/compact`)
257
+ - Cancelling in-flight LLM streaming on single Ctrl-C (requires provider-level
258
+ cancellation)
259
+
260
+ ## M5: Persistence + memory (JSONL sessions, `krodo resume`, AGENTS.md, config)
261
+
262
+ ### JsonlSessionStore (`src/krodo/memory/store.py`)
263
+
264
+ `JsonlSessionStore` implements the `SessionStore` Protocol backed by one `.jsonl` file per session in `<workspace>/.krodo/sessions/`.
265
+
266
+ - `create_session(session_id, workspace, model, agents_md_hash)` → writes `SESSION_INIT` as `seq=0`.
267
+ - `append_event(session_id, event)` → appends a single JSONL line.
268
+ - `load_events(session_id)` → reads all events in order.
269
+ - `max_seq(session_id)` → reads only the **last line** for O(1) seq lookup.
270
+ - `list_recent(n)` → reads the `SESSION_INIT` header from each file to build `SessionRow` summaries.
271
+
272
+ `SessionRow` is a lightweight dataclass (`session_id`, `workspace`, `model`, `started_at`, `event_count`) returned by `list_recent`.
273
+
274
+ ### `krodo resume` (`src/krodo/cli/resume.py`)
275
+
276
+ ```bash
277
+ krodo resume --list # show recent sessions
278
+ krodo resume <id-or-prefix> # replay + continue in REPL
279
+ krodo resume --root /path <id> # explicit workspace
280
+ ```
281
+
282
+ Internals:
283
+ 1. `_resolve_session_id(store, token)` — exact match or unique prefix match; error on ambiguity.
284
+ 2. `store.load_events(session_id)` — load all stored events.
285
+ 3. `replay_events(events, context_manager)` — reconstruct `InMemoryContextManager._history` from `USER_MESSAGE`, `ASSISTANT_MESSAGE`, `TOOL_RESULT`, and `COMPRESSION` events.
286
+ 4. Drop into the REPL with the restored history.
287
+
288
+ `ReplayStats` (`src/krodo/memory/replay.py`) tracks `messages_restored`, `tool_results_restored`, and `compressed` (bool).
289
+
290
+ ### AGENTS.md 3-tier merge (`src/krodo/memory/agents_md.py`)
291
+
292
+ `load_agents_md(workspace, cwd) -> AgentsMdBundle` collects and merges:
293
+
294
+ | Tier | Path | Droppable on overflow? |
295
+ |------|------|----------------------|
296
+ | 1 (system) | `~/.config/krodo/AGENTS.md` | Yes |
297
+ | 2 (project) | `<workspace>/AGENTS.md` | **No** — always kept |
298
+ | 3 (subdir) | `<cwd>/AGENTS.md` … up to workspace root | Yes (outermost first) |
299
+
300
+ Rules:
301
+ - Per-file limit: **8K tokens** (truncated with `…[truncated]` suffix).
302
+ - Total limit: **12K tokens** (tiers 1 and 3 are dropped first).
303
+ - The bundle's SHA-256 hash is stored in the `SESSION_INIT` event.
304
+ - Content is injected as a `<project_memory>…</project_memory>` user message at index 0 of `_history`.
305
+
306
+ ### Config loading (`src/krodo/core/config.py`)
307
+
308
+ `KrodoConfig` (Pydantic model) covers `model`, `approval`, `max_tool_calls`, `summary_window`, `compress`, `token_ratio`.
309
+
310
+ `load_config(workspace_root) -> tuple[KrodoConfig, list[tuple[Path, str]]]` merges:
311
+
312
+ 1. `~/.config/krodo/config.toml` (user, TOML)
313
+ 2. `<workspace>/.krodo/config.yaml` (workspace, YAML — takes precedence over user)
314
+
315
+ Precedence chain: **CLI flag > env var > workspace YAML > user TOML > built-in default**.
316
+
317
+ `main.py` applies config values to Typer options only when `ParameterSource` (from `click.core`) shows the option is still at its default — CLI flags and env vars always win.
318
+
319
+ `krodo doctor` now shows a **Config sources** section listing which files were found and which keys they contribute.
320
+
321
+ ## M6: streaming, cost, stdin, slash commands, approval persistence
322
+
323
+ ### Streaming output (`src/krodo/llm/streaming.py`, M6.1)
324
+
325
+ `ChunkAccumulator` reassembles `LLMChunk`s into a complete assistant `Message`: text deltas are concatenated, tool-call fragments are merged by index (the `arguments` JSON string arrives split across chunks and is `json.loads`-ed at the end, falling back to `{"_raw": ...}` on malformed JSON so the BAD_JSON recovery path still triggers), and the final `usage` / `finish_reason` are captured.
326
+
327
+ `AgentLoop._call_llm` streams when **both** `LoopConfig.stream` (default True) and the provider's `supports_streaming` attribute are truthy; otherwise it falls back to non-streaming `chat()`. Test mocks don't set the attribute, so they transparently use `chat()` — zero test churn. Text deltas go through the constructor-injected `on_delta` callback (default: Rich raw print, no markup). `TurnResult.streamed` tells `_echo_turn_result` not to print `final_text` a second time.
328
+
329
+ ### Cost tracking (`src/krodo/obs/cost.py`, M6.2 — engineering rule #4)
330
+
331
+ - `Message.usage` (`{prompt_tokens, completion_tokens, total_tokens}`) and `Message.cost_usd` are filled by `LiteLLMProvider.chat()` via `response.usage` + `litellm.completion_cost`; the streaming path gets usage from the final chunk and the loop estimates cost with `litellm.cost_per_token`. Unknown models: tokens tracked, cost `None`.
332
+ - `CostTracker` accumulates per-session totals; `AgentLoop` emits one `COST_SNAPSHOT` event per turn with `{turn_*, total_*}` token/cost fields.
333
+ - Both session summaries print `tokens : 12.3k in / 4.1k out | cost $0.0231` (cost omitted when unknown). Replay skips `COST_SNAPSHOT`.
334
+
335
+ ### Pipe stdin entry (M6.3)
336
+
337
+ `echo "fix the bug" | krodo` runs headless with the piped text as the prompt. `git diff | krodo "review this"` appends stdin as a `<stdin>...</stdin>` context block after the prompt. Empty piped stdin (CliRunner test streams) keeps the REPL behaviour — this completes task 1.10's three-entry acceptance (REPL / exec / pipe).
338
+
339
+ ### REPL slash commands (M6.4)
340
+
341
+ Handled locally in `repl.py:_dispatch_slash` — the LLM never sees them. Checked after the exit-token test, so `:q` still exits.
342
+
343
+ | Command | Action |
344
+ |---------|--------|
345
+ | `:help` | list commands |
346
+ | `:sessions` | recent-sessions table (shared `render_sessions_table` with `resume --list`) |
347
+ | `:undo` | `undo_command(...)` with `typer.Exit` caught so the REPL survives |
348
+ | `:cost` | CostTracker totals |
349
+ | `:resume <id>` | switch session: `run_repl` returns the target id; `repl_session_cycle` (resume.py) rebuilds components, replays history, re-enters |
350
+
351
+ ### Approval trust persistence (M6.5)
352
+
353
+ `TerminalApprovalManager.export_state()/restore_state()` snapshot `_session_trusted` + `_pattern_trust`. When a decision is `approve_session`/`approve_pattern`, the `APPROVAL_DECISION` event carries the full `state` snapshot (last one wins). `replay_events(events, ctx, approval=...)` re-applies the latest snapshot on resume, so `a`/`p` answers survive `krodo resume`. Cross-session global policy stays Phase 3 (`policy.toml`).
354
+
355
+ ## M7: Brand rename + v0.1.0 release (Phase 1 closeout)
356
+
357
+ M7 closed out Phase 1. The work happened in five independent commits, each independently revertable:
358
+
359
+ | Commit | Type | What |
360
+ |--------|------|------|
361
+ | `ce24ef6` | `fix(mypy)` | cleared 10 mypy `--strict` errors (`types-PyYAML` stubs + streaming `int(object)` overloads + bare `dict`/`list` generics in CLI helpers + 2 stale `# type: ignore`) |
362
+ | `295ce49` | `feat(rename)` | full rebrand `Coda → krodo`: `src/coda/` → `src/krodo/`, ~826 coda/Coda/CODA references across 84+ files (perl 3-case replace), pyproject 5 fields, `.codaignore` → `.krodoignore`, all docs/AGENTS/README/architecture |
363
+ | `0c5c607` | `docs` | QUICKSTART / CONTRIBUTING / SECURITY / CHANGELOG + README polish (4 badges, doc table, PyPI-deferred notice) |
364
+ | `634d051` | `fix(rename)` | cleanup residuals missed by initial perl pass: `.gitignore` patterns + LICENSE copyright holder + physical `.coda/` → `.krodo/` directory on disk |
365
+ | `05a82bd` | `fix(ci)` | ruff format backfill (10 pre-existing files) + correct repo owner `liangck` → `wadekun` across all docs |
366
+ | `90aa414` (PR #1 merge) | merge | M7 dogfood: krodo fixed its own `checkpoint_skipped` warning noise (non-git workspaces now warn once per session, then debug) — `836d829` |
367
+
368
+ ### Why the rename
369
+
370
+ - PyPI `coda` was occupied by an unrelated 2017 file-tagging package (claiming both `import coda` and the `coda` console script).
371
+ - GitHub `agno-agi/coda` is an active same-category coding-agent project (Slack surface, well-funded); we would have lost brand discovery.
372
+ - `krodo` verified clean across PyPI / npm / `.ai` / `.dev` / `.com`, no negative etymology.
373
+
374
+ ### Release flow (this repo's pattern)
375
+
376
+ 1. Bump `version` in `pyproject.toml`.
377
+ 2. Move `[Unreleased]` → `[x.y.z] — YYYY-MM-DD` in `CHANGELOG.md`.
378
+ 3. `git commit -m "chore(release): vx.y.z"`.
379
+ 4. `git tag vx.y.z && git push --tags`.
380
+ 5. `gh release create vx.y.z --notes-from-tag` (or paste CHANGELOG section).
381
+ 6. PyPI upload is **deferred past v0.1.0** (distribution name `krodo` locked; first release via GitHub only; PyPI in a minor follow-up).
382
+
383
+ ### Documentation index (where things live)
384
+
385
+ | Doc | Path | Audience |
386
+ |-----|------|---------|
387
+ | Project overview + roadmap | `README.md` | everyone |
388
+ | 5-minute install | `docs/QUICKSTART.md` | new users |
389
+ | Design baseline (source of truth) | `docs/architecture.md` | contributors |
390
+ | Auto-loaded project memory | `AGENTS.md` (this file) | krodo + contributors |
391
+ | Dev setup + CI gate + PR flow | `CONTRIBUTING.md` | contributors |
392
+ | Threat model + reporting | `SECURITY.md` | users + researchers |
393
+ | Milestone-by-milestone changes | `CHANGELOG.md` | everyone |
394
+
395
+ ### M7 lesson learned: full-repo rename coverage
396
+
397
+ The initial rename commit (`295ce49`) ran perl 3-case replacement on a hardcoded list of files (`pyproject` / `README` / `AGENTS` / `architecture` / `ci.yml` / `.krodoignore`). That missed `.gitignore`, `LICENSE`, and the on-disk `.coda/` directory — caught only after CI failed on the first GitHub push. For future full-repo renames, use `rg -l 'pattern'` to enumerate every hit first, then exclude historical archives and intentional references.
398
+
399
+ ## When you (the agent) modify this codebase
400
+
401
+ - Always `read_file` before `edit_file` — never guess file contents.
402
+ - Before any write, describe what you intend to do in plain language.
403
+ - After changes, run the relevant test suite (not the full suite — pick what's affected).
404
+ - If `mypy --strict` fails, fix it in the same edit. Do not push type errors.
405
+ - Use `git_diff` to verify your change before declaring success; do not assume the file was written correctly.
406
+ - Never modify `pyproject.toml` dependency versions without explicit user approval (rule 7).
407
+ - Never modify `LICENSE`, `.gitignore`, or `.github/` without explicit user approval.
@@ -0,0 +1,116 @@
1
+ # Changelog
2
+
3
+ All notable changes to Krodo are documented here.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html)
7
+ once v0.1.0 is tagged.
8
+
9
+ ## [Unreleased]
10
+
11
+ ### Added
12
+ - M7 release scaffold: QUICKSTART, CONTRIBUTING, SECURITY (this changelog).
13
+
14
+ ### Changed
15
+ - **Brand rename: Coda → Krodo.** Distribution name, Python import package,
16
+ CLI command, workspace state directory (`.coda/` → `.krodo/`), config
17
+ directory (`~/.config/coda/` → `~/.config/krodo/`), and environment
18
+ variables (`CODA_*` → `KRODO_*`) all renamed. Reason: PyPI `coda` occupied
19
+ by an unrelated 2017 file-tagging package; GitHub `agno-agi/coda` is an
20
+ active same-category coding-agent project. Krodo is verified clean across
21
+ PyPI / npm / `.ai` / `.dev` / `.com`.
22
+
23
+ ### Fixed
24
+ - mypy `--strict` now passes with zero errors (was 10 errors before M7).
25
+ Root causes: missing `types-PyYAML` stubs, `int(object)` overload failures
26
+ in `LLMChunk.usage` parsing, bare `dict`/`list` generics in two CLI helpers.
27
+
28
+ ## [0.1.0] — Phase 1 milestones M1–M6 (collected changelog)
29
+
30
+ Krodo v0.1.0 is the Phase 1 feature-complete release. It ships a usable
31
+ REPL + headless CLI with 11 tools, three approval modes, automatic git
32
+ checkpoints, JSONL-backed sessions, and AGENTS.md project memory. The
33
+ sections below summarise each milestone; full design notes live in
34
+ [`docs/architecture.md`](docs/architecture.md) §10 changelog.
35
+
36
+ ### M1 — Walking skeleton
37
+ - Single-turn ReAct loop (`krodo/core/loop.py`), 3 tools (`read_file`,
38
+ `write_file`, `run_shell`), Typer CLI entry, `ToolRegistry` with
39
+ auto-generated LiteLLM JSON schemas.
40
+
41
+ ### M2 — Full tools + three approval modes
42
+ - 8 more tools: `list_dir`, `glob`, `grep`, `edit_file`, `apply_patch`,
43
+ `git_status`, `git_diff`, `git_commit`. Total 11.
44
+ - `TerminalApprovalManager` with `read_only` / `auto_edit` (default) /
45
+ `full_auto` modes. `auto_edit` prompts `y/n/a/p/?` and supports
46
+ pattern trust (e.g. `run_shell pytest*`).
47
+ - Path firewall + dangerous-command blocklist + symlink-safe
48
+ `filter_allowed_paths()`.
49
+
50
+ ### M3 — Token budget, dual compression, error recovery
51
+ - `BudgetCalculator` triggers compression at 80% context window.
52
+ - `make_compressor("llm"|"algorithmic")`: LLM summarises oldest N rounds
53
+ into `<SUMMARY>` block; algorithmic drops `tool_result` content but
54
+ keeps metadata. Pinned context (recent 5 file paths + last user
55
+ message) is never compressed.
56
+ - `recovery.py` covers 7 scenarios: `bad_json`, `tool_timeout`, `stall`,
57
+ `context_loss`, `sha256_conflict`, `provider_error`, `eacces`.
58
+ - `StallDetector` aborts the turn after 3 consecutive identical write calls.
59
+ - `SessionEventLogger` emits typed JSONL events for replay and observability.
60
+ - New CLI flags: `--max-tool-calls`, `--summary-window`.
61
+
62
+ ### M4 — `.krodoignore`, git checkpoint, `krodo undo`, diff preview
63
+ - `.krodoignore` (4-tier merge): hard-coded defaults + project `.gitignore`
64
+ + `<workspace>/.krodoignore` + `~/.config/krodo/krodoignore`. Backed by
65
+ `pathspec` gitignore semantics.
66
+ - `GitCheckpointManager.create()` runs `git stash create` before every
67
+ write (does NOT touch the stash stack).
68
+ - `krodo undo` restores files via `git checkout <sha> -- <paths>`.
69
+ - Diff preview rendered before `y/n` approval in `auto_edit` mode.
70
+
71
+ ### M4.5–M4.9 — UX polish and interactive REPL
72
+ - LiteLLM noise silenced; abort reason surfaced to user; Windows `glm-*`
73
+ compatibility; `krodo doctor` diagnostics command.
74
+ - Dynamic tool list injected into system prompt; configurable `--max-tokens`.
75
+ - `stop_reason` diagnostic logging; invalid-args retry budget.
76
+ - **Interactive REPL** (`krodo` with no prompt): multi-turn dialogue with
77
+ persistent conversation history. Exit via `exit`/`quit`/`:q`/Ctrl-D/two
78
+ Ctrl-C. Ctrl-C during a turn cancels just that turn.
79
+
80
+ ### M5 — Persistence + memory
81
+ - `JsonlSessionStore` writes `<workspace>/.krodo/sessions/<id>.jsonl`
82
+ (one event per line, monotonic `seq`).
83
+ - `krodo resume [--list] [<id-prefix>]` replays stored events into
84
+ `InMemoryContextManager`, restoring full conversation context.
85
+ - AGENTS.md 3-tier merge: system (`~/.config/krodo/AGENTS.md`) + project
86
+ (`<workspace>/AGENTS.md`, always kept) + subdir walk. 8K per-file,
87
+ 12K total budget, SHA-256 stored in `SESSION_INIT`.
88
+ - Config loading: `~/.config/krodo/config.toml` + `<workspace>/.krodo/config.yaml`.
89
+ Precedence: CLI flag > env > workspace YAML > user TOML > default.
90
+
91
+ ### M6 — Streaming, cost, pipe stdin, slash commands, approval persistence
92
+ - **M6.1 streaming**: `ChunkAccumulator` reassembles LiteLLM chunks;
93
+ `AgentLoop._call_llm` streams when provider declares `supports_streaming`;
94
+ falls back to `chat()` for mocks.
95
+ - **M6.2 cost tracking**: `Message.usage` / `cost_usd` filled by provider;
96
+ `CostTracker` accumulates session totals; one `COST_SNAPSHOT` event per
97
+ turn; session summary prints `tokens: Xk in / Yk out | cost $Z`.
98
+ - **M6.3 pipe stdin**: `echo task | krodo` runs headless with stdin as
99
+ prompt; `git diff | krodo "review this"` adds stdin as `<stdin>` context.
100
+ - **M6.4 REPL slash commands**: `:help` / `:sessions` / `:undo` / `:cost` /
101
+ `:resume <id>`. Handled locally, never sent to the LLM.
102
+ - **M6.5 approval trust persistence**: `approve_session` / `approve_pattern`
103
+ decisions survive `krodo resume` via state snapshots in `APPROVAL_DECISION`
104
+ events.
105
+ - Tool-call-limit interrupt now synthesises a `[skipped: tool call limit
106
+ reached]` tool_result so `krodo` can `continue_turn()` mid-batch without
107
+ losing the LLM's pending work.
108
+
109
+ ### M7 — Release preparation
110
+ - mypy strict clean (see Fixed above).
111
+ - Brand rename to Krodo (see Changed above).
112
+ - Documentation quartet (QUICKSTART / CONTRIBUTING / SECURITY / this file).
113
+ - GitHub repo + v0.1.0 tag + Release (deferred until owner confirms).
114
+
115
+ [Unreleased]: https://github.com/wadekun/krodo/compare/v0.1.0...HEAD
116
+ [0.1.0]: https://github.com/wadekun/krodo/releases/tag/v0.1.0