co-agent-coder 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 (86) hide show
  1. co_agent_coder-0.1.0/CHANGELOG.md +427 -0
  2. co_agent_coder-0.1.0/DESIGN.md +674 -0
  3. co_agent_coder-0.1.0/LICENSE +201 -0
  4. co_agent_coder-0.1.0/MANIFEST.in +11 -0
  5. co_agent_coder-0.1.0/PKG-INFO +396 -0
  6. co_agent_coder-0.1.0/PRD.md +652 -0
  7. co_agent_coder-0.1.0/README.md +339 -0
  8. co_agent_coder-0.1.0/docs/DEVELOPMENT.md +63 -0
  9. co_agent_coder-0.1.0/notebooks/bootstrap.ipynb +175 -0
  10. co_agent_coder-0.1.0/notebooks/t4_smoke.ipynb +261 -0
  11. co_agent_coder-0.1.0/pyproject.toml +123 -0
  12. co_agent_coder-0.1.0/setup.cfg +4 -0
  13. co_agent_coder-0.1.0/src/co_agent_coder.egg-info/PKG-INFO +396 -0
  14. co_agent_coder-0.1.0/src/co_agent_coder.egg-info/SOURCES.txt +84 -0
  15. co_agent_coder-0.1.0/src/co_agent_coder.egg-info/dependency_links.txt +1 -0
  16. co_agent_coder-0.1.0/src/co_agent_coder.egg-info/entry_points.txt +5 -0
  17. co_agent_coder-0.1.0/src/co_agent_coder.egg-info/requires.txt +39 -0
  18. co_agent_coder-0.1.0/src/co_agent_coder.egg-info/top_level.txt +1 -0
  19. co_agent_coder-0.1.0/src/coagent/__init__.py +84 -0
  20. co_agent_coder-0.1.0/src/coagent/__main__.py +6 -0
  21. co_agent_coder-0.1.0/src/coagent/agent/__init__.py +27 -0
  22. co_agent_coder-0.1.0/src/coagent/agent/budget.py +220 -0
  23. co_agent_coder-0.1.0/src/coagent/agent/loop.py +1102 -0
  24. co_agent_coder-0.1.0/src/coagent/agent/prompts.py +228 -0
  25. co_agent_coder-0.1.0/src/coagent/agent/state.py +181 -0
  26. co_agent_coder-0.1.0/src/coagent/cli.py +351 -0
  27. co_agent_coder-0.1.0/src/coagent/config.py +307 -0
  28. co_agent_coder-0.1.0/src/coagent/context/__init__.py +26 -0
  29. co_agent_coder-0.1.0/src/coagent/context/artifacts.py +431 -0
  30. co_agent_coder-0.1.0/src/coagent/context/repo_map.py +590 -0
  31. co_agent_coder-0.1.0/src/coagent/context/retrieval.py +375 -0
  32. co_agent_coder-0.1.0/src/coagent/context/tokens.py +93 -0
  33. co_agent_coder-0.1.0/src/coagent/evals/__init__.py +0 -0
  34. co_agent_coder-0.1.0/src/coagent/gitx/__init__.py +52 -0
  35. co_agent_coder-0.1.0/src/coagent/gitx/commit.py +91 -0
  36. co_agent_coder-0.1.0/src/coagent/gitx/durable.py +147 -0
  37. co_agent_coder-0.1.0/src/coagent/gitx/git.py +351 -0
  38. co_agent_coder-0.1.0/src/coagent/gitx/lock.py +157 -0
  39. co_agent_coder-0.1.0/src/coagent/gitx/log.py +85 -0
  40. co_agent_coder-0.1.0/src/coagent/gitx/resume.py +199 -0
  41. co_agent_coder-0.1.0/src/coagent/gitx/session.py +233 -0
  42. co_agent_coder-0.1.0/src/coagent/llm/__init__.py +83 -0
  43. co_agent_coder-0.1.0/src/coagent/llm/base.py +235 -0
  44. co_agent_coder-0.1.0/src/coagent/llm/grammar.py +306 -0
  45. co_agent_coder-0.1.0/src/coagent/llm/registry.py +147 -0
  46. co_agent_coder-0.1.0/src/coagent/llm/transformers_backend.py +326 -0
  47. co_agent_coder-0.1.0/src/coagent/py.typed +0 -0
  48. co_agent_coder-0.1.0/src/coagent/safety/__init__.py +24 -0
  49. co_agent_coder-0.1.0/src/coagent/safety/denylist.py +146 -0
  50. co_agent_coder-0.1.0/src/coagent/safety/policy.py +219 -0
  51. co_agent_coder-0.1.0/src/coagent/safety/redact.py +146 -0
  52. co_agent_coder-0.1.0/src/coagent/templates/config.toml +104 -0
  53. co_agent_coder-0.1.0/src/coagent/tools/__init__.py +155 -0
  54. co_agent_coder-0.1.0/src/coagent/tools/artifacts.py +307 -0
  55. co_agent_coder-0.1.0/src/coagent/tools/base.py +226 -0
  56. co_agent_coder-0.1.0/src/coagent/tools/fs.py +507 -0
  57. co_agent_coder-0.1.0/src/coagent/tools/notebooks.py +875 -0
  58. co_agent_coder-0.1.0/src/coagent/tools/pyt.py +240 -0
  59. co_agent_coder-0.1.0/src/coagent/tools/shell.py +558 -0
  60. co_agent_coder-0.1.0/src/coagent/ui/__init__.py +50 -0
  61. co_agent_coder-0.1.0/src/coagent/ui/gradio_app.py +353 -0
  62. co_agent_coder-0.1.0/src/coagent/ui/notebook.py +342 -0
  63. co_agent_coder-0.1.0/src/coagent/ui/render.py +422 -0
  64. co_agent_coder-0.1.0/tests/__init__.py +0 -0
  65. co_agent_coder-0.1.0/tests/_fake_gradio.py +74 -0
  66. co_agent_coder-0.1.0/tests/test_agent_budget.py +220 -0
  67. co_agent_coder-0.1.0/tests/test_agent_loop.py +342 -0
  68. co_agent_coder-0.1.0/tests/test_artifacts.py +423 -0
  69. co_agent_coder-0.1.0/tests/test_bootstrap.py +227 -0
  70. co_agent_coder-0.1.0/tests/test_cli.py +67 -0
  71. co_agent_coder-0.1.0/tests/test_config.py +76 -0
  72. co_agent_coder-0.1.0/tests/test_gitx.py +440 -0
  73. co_agent_coder-0.1.0/tests/test_llm_backend.py +175 -0
  74. co_agent_coder-0.1.0/tests/test_llm_grammar.py +211 -0
  75. co_agent_coder-0.1.0/tests/test_llm_integration.py +131 -0
  76. co_agent_coder-0.1.0/tests/test_llm_transformers.py +181 -0
  77. co_agent_coder-0.1.0/tests/test_policy.py +138 -0
  78. co_agent_coder-0.1.0/tests/test_repo_map.py +341 -0
  79. co_agent_coder-0.1.0/tests/test_retrieval.py +282 -0
  80. co_agent_coder-0.1.0/tests/test_safety.py +121 -0
  81. co_agent_coder-0.1.0/tests/test_tools_fs.py +256 -0
  82. co_agent_coder-0.1.0/tests/test_tools_notebooks.py +472 -0
  83. co_agent_coder-0.1.0/tests/test_tools_pyt.py +194 -0
  84. co_agent_coder-0.1.0/tests/test_tools_shell.py +211 -0
  85. co_agent_coder-0.1.0/tests/test_ui.py +368 -0
  86. co_agent_coder-0.1.0/tests/test_ui_gradio.py +260 -0
@@ -0,0 +1,427 @@
1
+ # Changelog
2
+
3
+ All notable changes to coAgent. Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
4
+ versioning: [SemVer](https://semver.org/spec/v2.0.0.html).
5
+
6
+ ## [0.1.0] - 2026-11-13
7
+
8
+ ### Added
9
+
10
+ - `src/` package layout with the module tree from `DESIGN.md` §11.
11
+ - `.coagent/config.toml` template shipped as package data, covering model,
12
+ verify, git, durability, agent budgets, tools, artifacts, safety, and UI.
13
+ - `coagent.config` — TOML loading, per-section dataclasses, unknown-key
14
+ rejection, hard 12,288-token context cap, and schema-version refusal
15
+ (PRD FR-DIS-5).
16
+ - `coagent` CLI with `init`, `doctor`, `config`, `tools`, `resume`, and
17
+ `models` verbs.
18
+ - `coagent doctor` reports the GPU envelope and whether it is a free-tier T4.
19
+ - Dev extras (`[llm]`, `[ui]`, `[context]`, `[colab]`, `[dev]`) so the package
20
+ installs and tests on CPU without torch.
21
+
22
+ ### Safety layer (`coagent.safety`)
23
+
24
+ - `redact` — GitHub, Hugging Face, OpenAI, Anthropic, AWS, Google, Stripe,
25
+ Slack, JWT, URL-credential, `Bearer`, assigned-secret, and private-key
26
+ redaction. Keeps the last four characters; scrubs nested dicts and lists.
27
+ (PRD FR-PRV-2, FR-BOOT-5)
28
+ - `denylist` — tokenized destructive-command rules: force-push, push to `main`,
29
+ `reset --hard`, `branch -D`, `rm -rf /`, `curl | sh`, fork bombs,
30
+ shutdown/reboot, mkfs. Deny rules evaluate before allow rules (FR-GIT-9).
31
+ - `policy` — the approval matrix enforced in code. Reads are jailed (including
32
+ `.git`); writes are allowed only inside the repo; shell/install/network/
33
+ delete ask in interactive mode and **deny** in non-interactive mode
34
+ (FR-TOL-4). `check_push` allows only configured remotes and `coagent/*`
35
+ refs (FR-GIT-8).
36
+
37
+ ### Tool layer (`coagent.tools`)
38
+
39
+ - Registry with JSON schemas, unknown-tool and unknown-argument rejection, and
40
+ type coercion, all with corrective messages (FR-TOL-1).
41
+ - Per-phase tool sets: SURVEY is read-only, PLAN gets **no tools at all**,
42
+ VERIFY cannot write (DESIGN §7.1).
43
+ - `read_file`, `list_dir`, `grep` — jailed reads, line-numbered output, and
44
+ head/tail truncation (FR-CTX-3).
45
+ - `edit_file` — SEARCH/REPLACE with a unique-match requirement, a fuzzy
46
+ nearest-region hint on failure, read-before-write enforcement (FR-AGT-4), and
47
+ a syntax gate that rejects an unparseable result (FR-VER-4).
48
+ - `write_file` — new files, or full rewrites only under 200 lines.
49
+ - `run_shell`, `run_python` — process-group isolation with timeout kill, a
50
+ scrubbed environment (no tokens reach the child), `CUDA_VISIBLE_DEVICES=""`
51
+ by default (PRD D4), and a 64 KB head/tail output cap (FR-TOL-3).
52
+ - `run_tests` — pytest output as a digest rather than a dump: pass/fail verdict,
53
+ failing node ids, and the `>` pointer plus `E` block for each failure, capped
54
+ at `verify.feedback_max_tokens` (DESIGN §7.5). Accepts `argv` (preferred, so a
55
+ tokenized command from config is never re-split) or a `command` string, which
56
+ is shlex-split without a shell. Classified EXEC, so it is unreachable from the
57
+ read-only SURVEY phase.
58
+ - `coagent.tools.pyt` — the parser behind `run_tests`: `parse_pytest_output`
59
+ (failed names, per-failure traces, summary, version, duration) and
60
+ `summarise_pytest`. Whole traces only, most recent first, with an explicit
61
+ omission note instead of a half-rendered traceback.
62
+
63
+ ### Git layer (`coagent.gitx`) — git as the agent's database
64
+
65
+ - `git.py` — a thin, jailed git runner: scrubbed env, `GIT_TERMINAL_PROMPT=0`
66
+ (a missing credential errors instead of hanging), porcelain/-z output, and the
67
+ push allowlist enforced *before* spawn. `GIT_ASKPASS` carries the token; it is
68
+ never written to `.git/config` (DESIGN §9.2).
69
+ - `commit.py` — the six `Coagent-*` trailers from PRD Appendix B, values
70
+ flattened to one line so a newline in a task cannot break `--grep`. Refuses
71
+ an empty commit, which would corrupt step numbering.
72
+ - `session.py` — session branch `coagent/<date>-<id>`; **refuses a dirty tree**
73
+ with the list of paths and a remedy (FR-GIT-1, AT-3); refuses to work on
74
+ `main`; reuses an existing branch as a *resume*. coAgent's own runtime state
75
+ (`.coagent/lock.json`, `tmp/`, `sessions/`) is excluded from the dirty check.
76
+ - `lock.py` — single-writer lock with heartbeat TTL, stale expiry, and `--force`
77
+ takeover that reports the blocking session (FR-GIT-6).
78
+ - `durable.py` — `checkpoint()`: push the session branch, or fall back to a
79
+ `git bundle` (full first, then `A..B` increments) when the push fails or a
80
+ token is missing. A failed sync warns and continues (FR-GIT-4).
81
+ - `resume.py` — a template-extracted briefing (no LLM call) from branch
82
+ trailers, the plan file, and the JSONL log; falls back to trailers when a
83
+ hard kill left no log. `next_step` is monotonic across plan commits.
84
+ - `log.py` — JSONL session log, **redacted before the write** (FR-PRV-3).
85
+ - `coagent init` now also adds the `.coagent` runtime entries to `.gitignore`;
86
+ the session path only warns rather than editing a tracked file.
87
+
88
+ ### Fixed
89
+
90
+ - Pinning `core.autocrlf` in the git child env made every CRLF worktree look
91
+ permanently modified. The value is now inherited, never overridden.
92
+ - `os.getpgid`/`start_new_session` do not exist on Windows; both are guarded so
93
+ the tool suite runs on a dev laptop while Linux keeps real process-group kills.
94
+
95
+ ### Agent loop (`coagent.agent`)
96
+
97
+ - `prompts.py` — the five phase prompts in one auditable file. Tool results are
98
+ wrapped in `<observation>` and the system prompt declares them untrusted data,
99
+ which is the first layer of the prompt-injection defence (DESIGN §9.3).
100
+ PLAN's prompt states it has no tools.
101
+ - `budget.py` — hard ceilings on steps, wall clock, tokens, GPU minutes,
102
+ commits, and consecutive failures. Exhaustion raises `BudgetExhausted` so the
103
+ run checkpoints and reports instead of burning the session (FR-AGT-5).
104
+ `LoopDetector` fingerprints `(tool, args)`; 3 repeats force a replan, 5 stop
105
+ the run (DESIGN §7.4).
106
+ - `state.py` — `PlanFile`, the durable plan in `.coagent/plans/`, written before
107
+ the first edit and re-read after eviction, so memory survives both context
108
+ loss and VM death (FR-CTX-5). Observations are a bounded window.
109
+ - `loop.py` — the explicit state machine SURVEY → PLAN → ACT → VERIFY →
110
+ CHECKPOINT → REPORT. PLAN structurally cannot act; one write or exec per
111
+ turn; read-before-write; verify-then-commit; `done` is a predicate over
112
+ passing checks and plan completion, never the model's opinion (FR-AGT-8).
113
+ - `coagent run "<task>"` with `--auto` and `--dry-run` (scripted model, no GPU).
114
+ - `python -m coagent` for use inside a Colab cell.
115
+
116
+ ### Fixed
117
+
118
+ - `git commit` failed with a bare exit 1 and empty stderr because nothing was
119
+ staged. The loop now stages changed and new files, never `.coagent/` state.
120
+ - Plan steps were never ticked, so no task could ever satisfy the definition of
121
+ done. A successful edit ticks the step naming that file, and passing checks
122
+ tick the step naming them.
123
+ - `Budget.check()` used `>=`, so the Nth step raised instead of the (N+1)th.
124
+ Ceilings are now inclusive.
125
+ - A final checkpoint committed failing work labelled as verified. The trailer
126
+ now records `failed (work in progress)`.
127
+ - The report phase could echo a raw JSON tool envelope at the user. It is now
128
+ detected with the real parser and replaced with a generated summary.
129
+ - A failed commit is no longer silent: the reason is emitted as a `commit_failed`
130
+ event.
131
+
132
+ ### Model layer (`coagent.llm`)
133
+
134
+ - `base.py` — the `LLMBackend` interface (`generate`, `count_tokens`, streaming,
135
+ cancel, `load`/`unload`), plus `TokenBudget`, which rejects an over-cap request
136
+ **before** generation using the model's own tokenizer (PRD D2, FR-CTX-6), and
137
+ `FakeBackend`, a scripted backend that makes the whole loop testable on CPU.
138
+ - `grammar.py` — tolerant envelope extraction with a 5-step escalation ladder:
139
+ labelled fence → bare fence → bare `{...}` (balanced-brace scan that respects
140
+ strings) → JSON5-ish repair (single quotes, unquoted keys, trailing commas,
141
+ literal control characters inside strings) → failure with a **corrective
142
+ message** the loop feeds straight back. Never raises `JSONDecodeError` at the
143
+ caller. Failures are diagnosed individually: "cut off" (unbalanced braces,
144
+ with advice to send a smaller edit), "malformed" (bad escapes, missing comma),
145
+ and "no JSON object" (plain prose) are different problems and get different
146
+ advice.
147
+ - `registry.py` — exactly two validated configurations (7B/NF4/12k and
148
+ 3B/NF4/8k fallback), and `preflight_gpu()`, which checks T4 / compute 7.5 /
149
+ ≥14 GB VRAM and returns an actionable refusal instead of raising (PRD D1).
150
+ - `transformers_backend.py` — NF4 4-bit with fp16 compute and
151
+ `attn_implementation="sdpa"` (Turing has no bf16 and no FlashAttention-2),
152
+ `expandable_segments` set before torch import, `TextIteratorStreamer` on a
153
+ worker thread, a real `StoppingCriteria` for cancel, and the OOM ladder
154
+ (`shrink_context` → `halve_output` → `fallback_model` → `stop`) that unloads
155
+ the model and never loses the session. Torch and transformers are resolved
156
+ through module-level accessors, so `import coagent` stays torch-free and the
157
+ orchestration is testable without a GPU.
158
+
159
+ ### Fixed
160
+
161
+ - `_is_cuda_oom` missed a wrapped `OutOfMemoryError` that only appeared in the
162
+ message text; the OOM ladder now recognises it.
163
+ - `OutOfMemoryLadder.exhausted` reported False after all three recovery actions;
164
+ it now flips once the ladder is spent.
165
+
166
+ - `.coagent/plans/` was missing from the runtime paths, so a second run refused
167
+ to start on coAgent's own plan files.
168
+
169
+ ### Context layer (`coagent.context`)
170
+
171
+ - `repo_map` — signature-only repository map built for a 12k window
172
+ (DESIGN §5.1–5.2). Parses every source file with `ast` and falls back to
173
+ tree-sitter, then to line-anchored regex, so a file that will not parse
174
+ degrades the map instead of ending the session. Emits signatures and
175
+ module constants only — never bodies.
176
+ - PageRank-lite ranking, `0.5·reference_count + 0.3·recently_edited +
177
+ 0.2·mentioned_in_task`: a shared module outranks a leaf script, and a file
178
+ the task names always survives the budget. Reference counting is inbound and
179
+ token-set based — the naive per-symbol regex version cost 4 s on a 7k-line
180
+ repo, now ~0.12 s.
181
+ - Greedy, deterministic packing into the repo map's 2000-token slot, cutting on
182
+ file boundaries rather than mid-signature. Vendored, generated, and
183
+ `__pycache__` trees are excluded; per-file signatures are capped at 40.
184
+ - `tokens` — ~4 chars/token estimator, `fit` for budget packing, and
185
+ `truncate_middle` for head/tail truncation (monotone, so an "it fits" answer
186
+ stays true for the real tokenizer).
187
+ - `context.repo_map_tokens` and `context.max_files` config knobs, validated with
188
+ a 200-token floor.
189
+ - The agent loop's `_repo_map` stand-in is replaced: SURVEY now receives the
190
+ ranked map, seeded with the task text and the files the session has touched.
191
+
192
+ ### Retrieval (`coagent.context.retrieval`)
193
+
194
+ - `Retriever` — Okapi BM25 over function-sized chunks (DESIGN §5.3, FR-CTX-2).
195
+ **No vector store**, per the design: on 2 vCPU beside a GPU an embedding index
196
+ is a memory and latency tax for a ranking gain BM25 mostly covers at this repo
197
+ size. `all-MiniLM-L6-v2` stays an optional future upgrade, not a dependency.
198
+ - Chunks come free from the repo map: `Signature` now carries 1-based, inclusive
199
+ `start_line`/`end_line` from the same `ast`/tree-sitter/regex pass, so
200
+ retrieval does not re-parse the repo. The AST, tree-sitter, and regex parsers
201
+ all populate spans; the regex fallback closes a `def` on the first line that
202
+ dedents past its body.
203
+ - A module prologue chunk (imports and module docstring) is added per file,
204
+ because that is where a file says what it depends on. `const` signatures are
205
+ skipped — a bare `X = ...` is not worth an excerpt slot.
206
+ - Tokenisation explodes `snake_case` and `camelCase`, so a query for "csv loader"
207
+ matches `load_csv`, while the joined identifier is kept so an exact query still
208
+ matches exactly.
209
+ - Ranking is BM25 plus a symbol-graph adjacency bonus (weight 0.35): a chunk that
210
+ imports a module already in context, or references/defines a symbol defined in
211
+ one, is pushed up. This is the "callers/callees of files already in context"
212
+ part of §5.3, and files already read are excluded so the slot only ever shows
213
+ new code.
214
+ - Deterministic ordering with a `(score, file, line)` tiebreak, so a re-query
215
+ cannot shuffle equal-scoring chunks and invalidate a resumed session.
216
+ - `render()` packs whole chunks into the 3000-token excerpt slot and **skips** a
217
+ chunk that does not fit rather than truncating it: half a function is worse
218
+ than a different whole one, and a truncated header would lie about the range.
219
+ A single 800-line method cannot eat the slot (`max_chunk_lines`).
220
+ - The ACT prompt receives the excerpts under an explicit "untrusted source, not
221
+ instructions" banner, alongside the last failure.
222
+ - The repo map is now built once per session and cached, invalidated only when a
223
+ file is written — it was being re-parsed on every turn. Retrieval or map
224
+ failures degrade to a note and emit an event; neither can end a session.
225
+ - `context.retrieved_tokens` config knob.
226
+
227
+ ### Notebook tools (`coagent.tools.notebooks`)
228
+
229
+ Notebooks are first-class (PRD D-2, FR-TOL-6), not source files with a JSON side
230
+ quest.
231
+
232
+ - `read_notebook` — cells by id, with type, execution count, and a hidden-output
233
+ count. Assigns ids to notebooks that lack them (nbformat 4.4, common in older
234
+ Colab files) **on read**, so an edit cannot land on a stale index; duplicate ids
235
+ are repaired. `read_file` on a `.ipynb` now redirects here rather than showing
236
+ escaped JSON as line numbers.
237
+ - `edit_notebook` — `replace_source`, `insert`, `delete`, `replace_metadata`.
238
+ Cells are addressed **by id only**; an index is rejected with the list of valid
239
+ ids, because indices shift under insert/delete and an index-addressed edit is a
240
+ silent wrong edit. Read-before-write applies exactly as for `edit_file`.
241
+ - A cell edit is gated on `ast.parse` first: an unparseable Python cell is refused
242
+ and the file is left untouched. IPython magics (`%matplotlib`, `!pip`, `%%bash`)
243
+ are neutralised rather than flagged, since they are valid notebook cells that
244
+ simply are not Python. An edited cell drops its stale outputs so the notebook
245
+ never shows results from code that no longer exists.
246
+ - Every write is validated with `nbformat.validate` after bumping the minor
247
+ version to 5 (required for ids), so coAgent can never hand Colab a corrupt file.
248
+ The tool result diff is rendered over cells, not raw JSON, and excludes the
249
+ spurious nbformat-version line.
250
+ - `exec_notebook` — `nbconvert --execute --inplace` in a subprocess, 300 s default
251
+ (`tools.notebook_timeout_s`), run against a **copy** and moved back, so a crash
252
+ mid-write cannot corrupt a tracked notebook. Outputs are read back from the
253
+ notebook, never from nbconvert's stdout.
254
+ - `allow_errors=True` means a failing cell does not stop the run; the tool reports
255
+ `N failing cell(s)` (nbconvert exits 0 either way, so the exit code is not the
256
+ verdict), extracts the last traceback, and never prints the whole log when a
257
+ traceback already explains the failure.
258
+ - FR-CTX-4 is enforced twice: only the last traceback and a 40-line stdout tail
259
+ enter context, **and** each line is capped at 300 chars, because
260
+ `print(x * 200)` is one line that would otherwise walk past a line-count budget.
261
+ Images and HTML become artifacts in `meta["outputs"]`, and never reach the
262
+ prompt as base64.
263
+ - Timeouts are detected from both paths: nbconvert's own `CellTimeoutError` and
264
+ coAgent's process-group kill.
265
+ - Shell escapes inside code cells (`!pip install …`) are gated by the same
266
+ approval matrix as `run_shell`, so `exec_notebook` is not a way around it.
267
+ - Phase gating: SURVEY may read a notebook, VERIFY may execute one, PLAN has no
268
+ tools, and only ACT may edit. `exec_notebook` counts as a write for commit
269
+ purposes, since it rewrites the notebook in place with outputs.
270
+
271
+ ### Artifacts (`coagent.context.artifacts`, `coagent.tools.artifacts`)
272
+
273
+ The output of a data task is often a plot or a CSV, not a source file, and "done"
274
+ means the figure saved and visible (PRD US-3, G5). This is that pipeline.
275
+
276
+ - `ArtifactStore` applies the §3.5 size policy in code, not by convention:
277
+ under `artifacts.commit_under_mb` goes to `artifacts/<run-id>/` and is
278
+ committed; over it goes to Drive keyed by sha256 and only a pointer is
279
+ recorded; over 2 GB is never copied at all. **AT-7 is a test**: a 20 MB file
280
+ produces no bytes in the tree, only a manifest entry.
281
+ - `artifacts/<run-id>/manifest.json` records
282
+ `{path, sha256, bytes, store, created_by_step}` per entry, sorted by path, and
283
+ replaces an entry on re-run rather than duplicating it — so re-running a cell
284
+ does not make the manifest describe a file that no longer exists.
285
+ - Drive is optional and detected, never required (FR-BOOT-4). With no mount an
286
+ over-cap artifact is **not** silently dropped and not forced into git: it
287
+ becomes an honest pointer whose note says to mount Drive or raise the cap on
288
+ purpose. That is the graceful degradation the PRD asks for.
289
+ - Artifact names are untrusted input from cell ids and model arguments, so they
290
+ are sanitised into a single path segment: `../../etc/passwd` becomes
291
+ `passwd.txt`, and a missing extension is inferred from the MIME type.
292
+ - `save_artifact` — stores an existing file by `source` or inline `text`.
293
+ It refuses to invent content: an agent that can fabricate a plot it never drew
294
+ is worse than one that reports the cell produced nothing. Empty files are
295
+ rejected as a failed job, not saved as an artifact.
296
+ - `display_artifact` — renders image/HTML/text inline (FR-UX-4) through an
297
+ optional injected callback, so the notebook UI stays out of the tool layer.
298
+ Headless (CLI, tests) it references the file instead; a renderer that raises is
299
+ reported and never fails the step. With no path it renders the manifest.
300
+ - Notebook integration: `exec_notebook` now stores displayed images and HTML
301
+ automatically, named from the cell id, so re-running replaces the figure
302
+ instead of piling up `output-1.png`, `output-2.png`. Cell streams are not
303
+ artifacts — they are the tail, not the product.
304
+ - `save_artifact` joins the commit set, so the artifact and its manifest ride in
305
+ the same commit that produced them, and the `Coagent-Artifacts` trailer names
306
+ only bytes actually in that commit. A Drive pointer is never claimed as a path.
307
+ - `Coagent-Artifacts` is now populated end-to-end: `state.artifacts` ← store ←
308
+ the `save_artifact` / `exec_notebook` step, and the manifest is written even
309
+ when every artifact went to Drive or the run stopped early.
310
+ - `resume` reads `artifacts/<run>/manifest.json` from git and names the artifacts
311
+ in the briefing, because a fresh VM has the files but no memory of them
312
+ (§3.6). A malformed manifest is ignored rather than blocking a resume.
313
+ - The loop now shares one `FileSession` and one `ArtifactStore` across phases, so
314
+ a SURVEY read satisfies ACT's read-before-write guard and artifacts accumulate
315
+ for the whole session.
316
+ - `SafetyClass.UI` is now reachable: ACT, VERIFY, and REPORT may render an
317
+ artifact; SURVEY and PLAN may not.
318
+
319
+ ### Notebook UI (`coagent.ui`)
320
+ The guaranteed surface (DESIGN §8.1): a chat cell that is good enough that the
321
+ product works without Gradio. Split in two on purpose — `render.py` is the
322
+ logic and is fully tested; `notebook.py` is plumbing.
323
+
324
+ - `render.Transcript` folds the loop's event stream into rows. Tool calls become
325
+ collapsed `<details>` blocks — one informative line closed, args and truncated
326
+ output open — so a 40-step run stays readable in a phone-width pane without
327
+ hiding the evidence. HTML in tool output is escaped.
328
+ - Headline rendering is per tool, because a collapsed row has to be useful on its
329
+ own: `edit_file src/loader.py +18 −3` counts real changed lines (diff headers
330
+ excluded), a read names the path and size, a shell call reports its status line.
331
+ - `render_status_line` is FR-UX-3 in one line: model, context, GPU, session, and
332
+ budget. One line because it sits at the top of every repaint.
333
+ - Unknown events are rendered, not dropped, so a new loop event appears in the
334
+ transcript the day it is added rather than the day this file learns of it.
335
+ - The row buffer is bounded; old rows leave the *view* but never the session log.
336
+ - `ChatCell` draws through `display_id` + `update` rather than
337
+ `clear_output(wait=True)`, which is what §8.1 asks for: the latter flickers and
338
+ scrolls the pane.
339
+ - Approvals block on a `threading.Event` a button callback sets — Colab delivers
340
+ widget messages on another thread, so the cell can wait without freezing. The
341
+ wait has a timeout and **denies** on expiry: a missing human is not consent,
342
+ consistent with the non-interactive default (§9.1).
343
+ - FR-UX-7 (reconnect-safe): `cell.command("approve"|"deny"|"stop"|"resume")`
344
+ resolves a pending decision from a plain text cell, so a lost widget cannot
345
+ strand a session.
346
+ - `ChatCell.render_artifact` draws images and HTML below the transcript, keeping
347
+ the log on screen (FR-UX-4). A renderer that raises is reported, never fatal.
348
+ - Everything degrades: no IPython or ipywidgets and the cell still records the
349
+ transcript, and `coAgent()` outside a live kernel stays headless.
350
+ - `coAgent()` now wires the cell automatically in a notebook — `on_event`,
351
+ `approver`, and `renderer` — while explicit arguments win and
352
+ `[ui] mode = "headless"` turns it off. That makes the documented two-line
353
+ notebook API (§8.2) actually true.
354
+ - `agent.show_log()` — the last N steps with the committed diff (§8.2), read from
355
+ the durable JSONL log so it still works after a resume.
356
+ - The loop's `tool` event now carries `path`, so the transcript can name the file
357
+ in a collapsed row without re-parsing tool output.
358
+
359
+ ### Optional Gradio UI (`coagent.ui.gradio_app`)
360
+ DESIGN §8.3 calls this a *nicety*, so it is built as one: optional, guarded, and
361
+ reusing the same `Transcript` the notebook draws. Nothing in the loop requires it.
362
+
363
+ - `GradioApp` — a status line, a markdown transcript, an approval banner with
364
+ Approve/Deny, and a prompt box, on `ui.gradio_port` (default 7860).
365
+ - Served **loopback only** (`server_name="127.0.0.1"`, `share=False`). In Colab
366
+ the tab reaches it through `serve_kernel_port_as_window`, so there is no reason
367
+ to bind a public interface; ngrok and `share=True` stay out of scope.
368
+ - `pip install 'coagent[gradio]'`, or `coagent[all]`. Importing `coagent.ui`
369
+ without Gradio is fine — the widget modules load lazily and `GradioApp()` raises
370
+ `GradioUnavailable` with the exact install command.
371
+ - Approvals reuse the notebook contract: a `threading.Event` the loop blocks on,
372
+ a timeout, and **deny on expiry**, because a missing human is not consent (§9.1).
373
+ - The pending command is HTML-escaped before it reaches the banner: it is model
374
+ output, and the banner is markup.
375
+ - `render_artifact` records the path in the transcript rather than pretending
376
+ Gradio can display an artifact inline; the notebook cell remains the place
377
+ artifacts are actually shown. The two surfaces are honest about the difference.
378
+ - Tests substitute a minimal stub when real Gradio is unavailable (it does not
379
+ install on Python 3.15), so the approval queue, hook wiring, HTML escaping, and
380
+ launch kwargs are exercised rather than skipped.
381
+
382
+ ### Tests
383
+
384
+ 475 unit and integration tests, CPU-only — no GPU and no network required.
385
+
386
+ ### Bootstrap (FR-BOOT-1/2, FR-DIS-2)
387
+
388
+ - `notebooks/bootstrap.ipynb` — the Open-in-Colab landing page. A preflight cell,
389
+ then **exactly three lines** (install, construct, chat), then what the three
390
+ lines hide: the log, the reviewable branch, the resume-after-death cell, and
391
+ the text-command fallback. Tests assert the three-line limit and the
392
+ preflight-before-agent ordering so the landing page cannot silently rot.
393
+ - **FR-BOOT-2 is now enforced, not just printed.** `coAgent.__init__` runs the
394
+ T4 preflight *before* constructing the backend and raises `HardwareError` with
395
+ an actionable message, so a CPU runtime fails in a second instead of after a
396
+ four-gigabyte download. `allow_unsupported=True` (and `coagent run
397
+ --allow-unsupported`) is documented in the error itself. An injected backend
398
+ skips the check — that caller has already decided about hardware.
399
+ - `coAgent(repo="https://…")` now clones into `coagent-workspace/` instead of
400
+ failing as a bad path, so the documented one-liner is true. Shallow, once:
401
+ an existing `.git` is reused rather than re-cloned. `/content` on Colab, the
402
+ cwd elsewhere.
403
+ - **Fixed: a run no longer leaves the tree dirty.** `ArtifactStore.flush()`
404
+ wrote an empty `artifacts/<session>/manifest.json` that nothing committed, and
405
+ the next session then refused to start with `DirtyTreeError` — the resume path
406
+ was broken by a file that recorded nothing. An empty manifest is now not
407
+ written; a real one still is.
408
+ Includes end-to-end runs on a real git repository: a scripted model drives
409
+ SURVEY → PLAN → ACT → VERIFY → commit with trailers, and separate tests prove
410
+ the dirty-tree refusal, plan rejection before any write, honest verify trailers,
411
+ and clean budget exhaustion. Notebook execution tests run against a real Jupyter
412
+ kernel and skip cleanly when none is installed.
413
+
414
+ ### Release notes — 0.1.0
415
+
416
+ First release: milestone M1. The loop is proven end-to-end on `FakeBackend` in
417
+ `notebooks/bootstrap.ipynb`, and the full v0.1 tool surface from `DESIGN.md` is
418
+ built — including `run_tests`/`tools/pyt.py`, which turns pytest output into a
419
+ digest instead of a dump. Every module in `DESIGN.md` §11 exists; the remaining
420
+ work before a T4 run is breadth of behaviour on a real GPU, not missing code.
421
+
422
+ Verification performed for this release: 476 tests pass with no skips, and the
423
+ built wheel installs into a clean venv where `coagent init`, `config`, `tools`,
424
+ and `run --dry-run` all succeed outside the source tree.
425
+
426
+ Not yet proven: a real 7B NF4 load and a live loop run on a free T4 (acceptance
427
+ test AT-1). That requires the Colab runtime and is the first post-release check.