GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — Tags · agenticarus/patch

Tags

v0.2.0

Toggle v0.2.0's commit message
## [0.2.0] - 2026-08-31

### Added

- `--command-timeout SECONDS`, which bounds how long a command run by `/run`, `/test` or
  `--test-cmd` may take, with `0` meaning no limit. The 120-second default introduced with command
  timeouts had no escape hatch, and a suite longer than that is ordinary -- this repository's own
  takes about 150 seconds, so `patch --test-cmd pytest` here was killed at 120s and, under
  `--auto-test`, the timeout was handed to the model as a test failure to go and fix. The setting
  carries across a coder switch and applies to the linter as well.

- A fourth full-repository audit in `AUDIT_REPORT.md`, split into independent security,
  correctness/reliability, performance/concurrency, and maintainability/testing/packaging/docs
  passes. It records nine evidence-backed findings (F-98 – F-106), and the candidates each
  pass investigated and rejected.
- A release process in `CONTRIBUTING.md`. The version comes from the git tag rather than a file, so
  an untagged build carries the `+g<sha>` local segment that PyPI refuses; the steps cover cutting
  the changelog section, tagging, building, and publishing.
- A workflow section in `AGENTS.md` requiring each task to land as one commit that carries its own
  doc updates, so documentation cannot drift behind the code. It also states when a released
  changelog section may be corrected, and that the GitHub release body is updated alongside it.
- `.github/workflows/packaging.yml`, which builds a wheel from a `.git`-less copy of the working
  tree, installs it into a clean environment, imports it from another directory and checks the
  console script and the bundled data files. The test suite runs from the repository root, where
  `tests/__init__.py` places the working tree on `sys.path`, so `import patch` resolved to the source
  checkout and no test ever exercised the artifact users install.

- A second full-repository audit in `AUDIT_REPORT.md`, performed after the first audit's findings
  were all resolved. It records 35 findings (F-42 – F-76), 4 of them Critical, each verified by
  executing the code. Three of the four are fixed in this release and listed below; the fourth,
  `set-env` in a repository config bypassing the environment denylist, is still open.

- A third full-repository audit in `AUDIT_REPORT.md`, performed after the first two rounds were
  fully remediated. It records 21 findings (F-77 – F-97) — one Critical, three High — tracked as
  and all fixed in this release. It also adds a "Disproved during this audit" section recording
  seven candidate findings that were attacked and discarded, so a fourth round does not re-derive
  them.

- `AUDIT_PROMPT.md`, the reusable brief for a full-repository audit. Three rounds have been run and
  each had to rediscover the same process lessons, so the file carries them: verify Critical and
  High findings by executing the tree's own code rather than by reading; search for the shape every
  Critical finding across three rounds has had (a guard that exists, is right, and is missing from
  one call site); distrust gates that are greener than their coverage; and record what you disprove.
  It includes a table of seven candidates already investigated and rejected, the current baseline so
  a regression in the gates is itself a finding, and the report and issue formats the existing
  rounds use.

- Regression tests for the findings whose absence was the actual root cause: a repository config
  naming its own env file, the `HOME`-is-the-repository collapse, a failing summarizer leaving the
  history intact, atomic writes surviving a failed write, and an unrunnable lint command staying out
  of the lint results.

### Changed

- **`check_untyped_defs` is on by default**, with an explicit three-module exception list that is
  meant to shrink (`base_coder.py`, `models.py`, `io.py`). Without it mypy does not analyse the body
  of an unannotated function at all, so most of the 29 gated modules were unchecked. Turning it on
  reported 97 errors across 12 modules, six of which were real latent defects rather than annotation
  noise — see Fixed above. 26 of the 29 are now clean under it.
- **State carried across a coder switch is a declared table**, `Coder.CARRIED_OVER`, rather than a
  dict literal written out by hand in `Coder.create`. Forwarding by hand is how a setting gets
  silently dropped on `/ask` or `/model`, as the voice settings once were. A test now asserts
  every name in the table survives a real switch, and that each is a constructor parameter.
- **The repository-trust policy is a module, not sixteen functions scattered through `main()`.**
  `patch/repo_trust.py` owns the denylists, the path checks and the three layers, and `RepoTrust`
  applies them in a fixed order — `sanitize_args` raises `TrustOrderError` if it runs before the
  `.env` layer, since argparse reads `PATCH_*` variables between them and that ordering was
  previously an unwritten property of the statement sequence. `RepoTrust.command_line_file` is now
  the only way to ask "did the user name this file, or did the repository?", which is the question
  the `env-file` bypass got wrong. The denylist is also split by *why* each key is denied
  (execution, credentials, file-naming, self-trust) rather than being one flat set.
- **The trust suite asserts the policy over the whole argument surface**, not one key at a time. A
  new test enumerates `parser._actions` and fails on any argument that names a file or a command
  and has no recorded decision — either on the denylist, or in `REVIEWED_SAFE_KEYS` with the reason
  it is safe. Removing `env-file` from the denylist now fails that test by name, which is what the
  bypass above needed and did not have.
- **`main()` is 411 lines rather than 777**, with eight pieces extracted that can be called from a
  test without constructing a session: `resolve_git_root`, `config_search_path`, `build_config`,
  `apply_theme_settings`, `apply_env_settings`, `setup_analytics`, `resolve_input_files`,
  `setup_model`, `run_one_shot_modes` and `run_repl`. Behaviour is unchanged; the two parse passes
  and the trust layers between them now live in `build_config`, where their order is stated.
- **Token/cost accounting and the chat transcript moved out of `Coder`** into
  `coders/usage.py` (`UsageTracker`) and `coders/chat_state.py` (`ChatState`). The per-provider
  cache-pricing arithmetic and the summariser's sentinel contract — a failed summary must never
  replace the transcript — are each testable on their own now. `Coder` keeps its
  existing surface through delegating properties, so the ~480 places the suite touches these
  counters are unchanged.
- **`cmd_add`'s refusal rules are a named method**, `reason_to_refuse_add`, rather than two compound
  conditions fifty lines into a hundred-line method — which is how the containment check came to be
  gated on `--auto-commits`.
- The mypy gate covers **29 modules** rather than ten. Widening it again immediately caught an
  implicit `Optional` in `waiting.step`, the second real defect the last two widenings have found.
  `patch/repo_trust.py` is at 93% test coverage.
- The mypy gate covers ten modules rather than three, adding `exceptions.py`, `history.py`,
  `linter.py`, `report.py`, `run_cmd.py`, `utils.py` and `watch.py`. Widening it immediately caught
  `ExInfo`, whose `name`, `retry` and `description` were annotated as required while most entries
  pass `None` and `get_ex_info` returns `ExInfo(None, None, None)`.
- CI reports test coverage (`--cov=patch --cov-report=term-missing`) on the Ubuntu job. It is
  reported, not gated: a threshold now would either assert nothing or fail the build on unrelated
  changes. The workflows also no longer key their pip cache on `requirements-dev.txt`, which no job
  installs.
- `get_file_mentions` builds its normalized word set once instead of once per repository file. The
  set is loop-invariant and this runs after every model reply, costing ~0.4s per reply on a
  20k-file repository against ~0.001s now.
- The repo-map tag cache is read once per hit rather than twice. `get_tags` already held the entry
  and then looked it up again, paying a second diskcache read and unpickle for every tracked file on
  the warm path.
- `importlib-metadata` is no longer capped in the runtime dependencies. The cap existed to work
  around a `twine` bug in release tooling, but it applied to everyone who installed `patch-chat`,
  holding a June-2024 version that predates the Python 3.13 and 3.14 this project supports. It now
  resolves to 9.0.1.
- The lint and type gate runs on a pinned Python 3.10 rather than whatever the CI image ships, so
  mypy is checked against the version `pyproject.toml` configures. This also populates
  `env.pythonLocation`, which the hook cache keys interpolate — unset, they collapsed to an empty
  segment and restored hook environments built against a different interpreter.
- `scripts/pip-compile.sh` forwards all its arguments to `uv pip compile`, not just the first, so
  flags that take a value such as `--upgrade-package <name>` work.
- Dropped `pip-tools` from the `dev` extra (the compile script uses `uv`), the unreachable
  `tree-sitter` marker for Python below 3.10, and `--extra-index-url` from `.agents/setup`, which
  resolved across both PyPI and the torch index rather than replacing it.
- The mypy gate now checks something. `follow_imports` was `"skip"`, which resolved `patch.io` and
  `patch.models` to `Any` even though both were listed for checking, so nothing crossing a module
  boundary was verified; it is now `"silent"`, and `Coder.io` is annotated so the IO boundary is
  actually typed. `check_untyped_defs` stays off — it reports 69 errors that are predominantly
  noise.
- Installation now leads with `pip install patch-chat` from PyPI instead of a source checkout, which
  is kept as the contributor path. `README.md` gains PyPI version and supported-Python badges, and
  `AGENTS.md` records that versions come from git tags and that the fork numbers from `0.1.0`.

### Security

- Repository-controlled `.env` and `set-env` can no longer replace the interactive command shell
  or inject shell startup files, and repository config can no longer redirect chat, input, LLM or
  analytics logs to arbitrary writable paths. Explicit command-line and trusted home settings keep
  working.
- A repository can no longer grant itself trust by naming its own environment file. `env-file` was
  the one key missing from the config denylist, so `env-file:./evil.env` in a repo's
  `.patch.conf.yml` made that file the "explicitly named" one `load_dotenv_files` trusts — loading
  it with `override=True` and no filtering. Because the parser uses `auto_env_var_prefix="PATCH_"`,
  the file could then set `PATCH_TEST_CMD`, which `sanitize_repo_config` cannot strip: it reads yaml
  files rather than the environment, and runs after the `.env` is loaded. `git clone && patch` was
  enough to reach arbitrary command execution and redirect every prompt and API key.
  Only a `--env-file` actually present in `argv` now confers trust.
- The Docker images no longer set `HOME` to the directory the project is mounted into. The whole
  repository-trust boundary is a comparison against `Path.home()`, and `ENV HOME=/app` — where
  `/app` is also `WORKDIR` and the bind-mount target — made the repository's own `.patch.conf.yml`
  and `.env` indistinguishable from the user's, leaving every filter inert in exactly the
  environment people use to run an agent against unreviewed code. It also put the pickle-backed
  repo-map cache back inside the repository under edit. `HOME` is now `/home/appuser`, and
  `is_trusted_config_path` additionally fails closed if it is ever told that home and the git root
  are the same directory.
- Provider exceptions are no longer sent verbatim to analytics. `message_send_exception` forwarded
  the full `str(err)`, which for litellm routinely carries the request URL, `api_base` and
  occasionally prompt fragments — contradicting the opt-in prompt's promise that Patch "never
  collects your code, chat messages, keys or personal info". Only the exception class name is sent;
  the full text is still shown locally.
- `/add` no longer stops checking containment when `--auto-commits` is off. The guard against adding
  files from outside the repository root was `and self.coder.auto_commits`, a flag with no bearing
  on where a path is, so `--no-auto-commits` disabled it entirely and out-of-root file contents were
  sent to the model as chat context.
- The crash reporter redacts credentials found in the traceback. Values of `*_API_KEY`, `*_TOKEN`,
  `*_SECRET`, `*_PASSWORD` and `*_ACCESS_KEY` environment variables are replaced before the issue
  body is composed; previously only file paths were scrubbed, and an exception message quoting the
  failed request could carry a key into a prefilled browser URL.
- The OpenRouter OAuth key is no longer written world-readable. `~/.patch` is created `0700` and
  `oauth-keys.env` `0600`; under the usual umask they were `0755` and `0644`, leaving a live billing
  credential readable by every local account — and because `main.py` treats that file as trusted
  configuration, writable by them too.
- Scraping an HTML page no longer downloads and installs a pandoc binary without asking. The install
  ran unprompted and unverified, reached from ordinary chat input via URL detection, while every
  other third-party install in Patch is gated behind a confirmation.
- The repo-map tag cache moved out of the repository, to
  `~/.patch/caches/tags.v<N>/<hash of the repo path>`. It was kept in
  `.patch.tags.cache.v<N>/` inside the repository being edited, and `diskcache` stores its values as
  pickles — so a repository that committed a crafted cache had it deserialized during an ordinary
  repo-map build, with no user interaction, on any checkout path an attacker could predict (CI
  workspaces and containers are fixed and public). Existing `.patch.tags.cache.v*` directories are
  no longer read and can be deleted.
- A repository's `.patch.conf.yml` can no longer set the environment variables its `.env` is
  forbidden to set. `set-env` was absent from the untrusted-config denylist, so a repository could
  set `OPENAI_API_BASE`, `HTTPS_PROXY`, `SSL_CERT_FILE` or `REQUESTS_CA_BUNDLE` through it and
  achieve the redirection the `.env` layer exists to prevent. `set-env` from an untrusted config is
  now filtered the same way a repository `.env` is — dangerous names dropped, ordinary ones such as
  `PATCH_MODEL=gpt-4o` kept — and `api-key`, `openai-api-base`, `openai-api-type`,
  `openai-api-version`, `openai-api-deployment-id`, `analytics-posthog-host`,
  `analytics-posthog-project-api-key`, `model-settings-file` and `model-metadata-file` are refused
  outright.
- The trust check on model settings files no longer treats an argparse default as a deliberate
  choice. `--model-settings-file` defaults to the bare filename, which resolves against the
  repository being edited, so the default was being honoured as "a file the user named" — which
  would have let a cloned repository supply model settings despite the fix above. Whether the user
  named a file is now decided from the command line itself.
- A repository can no longer redirect your requests through its own
  `.patch.model.settings.yml`. That file was loaded from the git root and working directory with no
  trust check at all, and its `extra_params` are passed straight to litellm — so a cloned repository
  could set `api_base`, `api_key` and `extra_headers` for *every* model and receive each prompt, the
  repo map, the contents of every file in the chat, and the header carrying your real API key. The
  reserved `patch/extra_params` entry applies to all models, so no knowledge of your setup was
  needed. Both this file and `.patch.model.metadata.json` now follow the same rule as
  `.patch.conf.yml` and `.env`: only your home-directory copy and a file you name with
  `--model-settings-file` / `--model-metadata-file` are trusted, ignored files are reported, and
  `--trust-repo-config` opts back in.
- Files added with `--read` or `/read-only` can no longer be written. `allowed_to_edit` never
  consulted the read-only set, and `path_within_root` returned `True` for any path in it — so the
  model could overwrite a reference file, and a read-only file outside the repository was a way
  through the containment boundary entirely under `--no-git`. Reading such files is unaffected;
  neither caller of `path_within_root` is on a read path.

### Removed

- The `mixpanel` runtime dependency and the dead analytics path it served. The only use was
  importing `MixpanelException`; the construction that would populate `self.mp` has been commented
  out, so the attribute was permanently `None` and the tracking block unreachable.
- Roughly 440 unreachable lines from `patch/coders/search_replace.py` (757 → 309): the
  `editblock_strategies` and `udiff_strategies` tables that nothing imports — `udiff_coder` builds
  its own list containing only `search_and_replace` — and with them `git_cherry_pick_osr_onto_o`,
  `git_cherry_pick_sr_onto_so`, `dmp_apply`, `dmp_lines_apply`, `map_patches`, `line_pad`,
  `line_unpad`, `lines_to_chars` and the module's benchmark harness. `diff-match-patch` remains a
  dependency: the live `diff_lines` uses it.
- `watch.py:main`, which was broken rather than merely unused — it passed a directory string where
  `FileWatcher` requires a coder and immediately dereferences `coder.io`, then called a
  `get_changes()` that does not exist.
- A `dump(edits)` debug statement in `prepare_to_edit`, on the production write path, and the
  `sys.version_info >= (3, 10)` compatibility branch in `IgnorantTemporaryDirectory` — the project
  requires 3.10.
- PyTorch from the `patch-full` Docker image. Nothing in the package imports it and it is in no
  requirements file; it cost several hundred megabytes and a long build step.
- `scripts/versionbump.py`, inherited from aider and left behind when versioning moved to git tags.
  It rewrote the `__version__` assignment in `patch/__init__.py` -- which is now the `safe_version`
  floor rather than the version -- left `CHANGELOG.md` and `pyproject.toml`'s `fallback_version`
  untouched, and pushed a second `vX.Y.Z+1.dev` tag that setuptools-scm would then have derived
  every later build from. Nothing referenced it, and `CONTRIBUTING.md` documents the release steps
  that replaced it.

### Fixed

- **The documented release command stripped every heading from the tag message.** `git tag -F`
  defaults to `--cleanup=strip`, which deletes each line beginning with `#`, so feeding it a
  changelog section dropped the `## [X.Y.Z]` title and all the `###` groups and left an
  undifferentiated wall of bullets. `CONTRIBUTING.md` now passes `--cleanup=verbatim`, and writes
  the section to a file that the later `gh release create --notes-file` reuses -- the previous
  `<release notes>` placeholder was unusable in PowerShell, where `<` is a reserved operator.

- **`patch --gui` disabled its file watcher in a development checkout.** The dev-version test
  looked for `-dev` in the version string, but setuptools-scm spells the marker `.dev` (PEP 440),
  so it never matched and streamlit was always started in the packaged-release configuration. A
  stale comment above it linked to an upstream aider issue number rewritten to point at this
  repository, where no such issue exists.

- **Stopping a command crashed on Windows.** `_stop_process_tree` called
  `isinstance(process, pexpect.spawn)`, but `pexpect` does not define `spawn` on Windows at all --
  the attribute is absent, so that raised `AttributeError` rather than returning `False`. Every path
  that stops a command went through it, so a timeout returned exit status 1 with the AttributeError
  text instead of `TIMEOUT_EXIT_STATUS`, because the outer `except Exception` swallowed it. The same
  shape applied to `os.killpg`, which is also POSIX-only. Both are now resolved once at import and
  tested for directly, which says what the code needs rather than inferring it from the platform
  name.

- Failed or empty `--message-file` input now exits nonzero instead of reporting success or entering
  the REPL; OpenRouter OAuth always releases its callback thread and port; and failed editor temp
  writes preserve their original error without leaking partial files.
- Shell commands now time out after 120 seconds with status 124, terminate descendants and retain a
  bounded output tail while still streaming everything to the terminal. Repo-map memoization now
  uses a 16-entry LRU and skips modes that cannot reuse it.
- Built package metadata now carries the SPDX `Apache-2.0` license expression and packaging CI
  verifies it, the bundled license and the Python requirement. Installation docs now state the
  supported Python 3.10–3.14 range.
- **`compile()` raising without a `lineno` no longer crashes the linter.** `lint_python_compile`
  read `err.lineno` off a bare `Exception`; `compile()` raises `SyntaxError`, which carries it, but
  also `ValueError` for source containing null bytes, which does not — turning a lintable file into
  an `AttributeError` from inside the linter.
- **`dump()` no longer raises when the caller's source is unavailable.** It read
  `traceback.extract_stack[-2][3]`, which is `None` for exec'd code, a frozen build, or a `.pyc`
  without its `.py`.
- **The crash handler restores the default excepthook instead of setting it to `None`.** CPython
  prints "sys.excepthook is missing" and loses the traceback when the hook is `None`, which is the
  opposite of what that line intended.
- Containment is re-checked **at the moment of writing**, not only when the edit is planned.
  `allowed_to_edit` runs during `prepare_to_edit` and the writes happen later in `apply_edits`, so a
  lint command, a test command or the file watcher could replace a checked path with a symlink
  pointing outside the root in between. Every coder now writes through
  `Coder.write_edited_file`, and a test asserts none of them calls `io.write_text` directly.
- `/run` no longer behaves differently depending on whether stdin is a tty. The pexpect path spawned
  `$SHELL -i`, sourcing the user's rc files — aliases, functions, `PATH` edits — while the
  subprocess path used `/bin/sh` with none of that, so the same `--test-cmd` could pass
  interactively and fail in CI.
- Commit attribution no longer mutates `os.environ` process-wide. `GIT_COMMITTER_NAME` and
  `GIT_AUTHOR_NAME` are scoped to the git subprocess via `custom_environment`, so the watcher and
  cache-warming threads no longer see them and a crash cannot leave them set. Reading `user.name`
  from a repo that has none is also guarded, rather than raising out of `commit()`.
- The non-atomic write fallback warns instead of being silent. On Windows, when another process
  holds a file open, `os.replace` is refused and the write falls back in place — the one remaining
  path where an interrupted write can truncate a file. The user is now told which file, and why.
- `run_install` returned a list rather than the joined output string on the success path.
- A failed history summarization no longer discards the entire conversation. `summarize_all` raises
  `ValueError` whenever every model fails — a transient provider outage is enough — and the worker
  caught it, leaving `summarized_done_messages` as the empty list the previous `summarize_end` had
  reset. `summarize_end` then assigned that over `done_messages`, silently wiping the chat with only
  a one-line warning, after which the model continued with no memory of the session. The result is
  now a `None` sentinel that `summarize_end` refuses to adopt, and unexpected exceptions in the
  daemon thread are reported rather than escaping as a raw traceback under the prompt.
- A new litellm release can no longer break every LLM call. `LiteLLMExceptions._load` raised
  `ValueError` for any `*Error` class not already in the hard-coded table, and `send()` builds the
  registry *inside* its `except` clause — so the raise replaced the real API error with a message
  about Patch's exception list. Unknown classes are now recorded rather than raised (the `strict`
  flag that was never read now gates that), and the registry is built once instead of on every error
  and every retry.
- Edits already written to disk are no longer lost when a later one fails. Only the `ValueError`
  branch of `apply_updates` reported partial progress; the `OSError`, git-error and general branches
  returned an empty set, so `send_message` skipped `auto_commit` for files that had already changed
  — never committed, never linted, and with no commit for `/undo` to revert. Applied paths are now
  tracked on the coder, so every failure path reports them.
- Files are written through a temporary file and renamed into place, instead of being truncated and
  rewritten. A crash, Ctrl-C or full disk mid-write left the user's source file empty or partial.
  Where the rename is refused because another process holds the file open — a Windows-only case —
  it falls back to the previous in-place write rather than failing.
- A lint command that cannot be run is reported as such, rather than as lint errors for the model to
  fix. `run_cmd_subprocess` flattened every exception into `(1, str(e))`, so a missing binary was
  indistinguishable from a linter reporting problems, and `[Errno 2] No such file or directory` was
  fed back as something to correct. It now re-raises `OSError`, which makes the handlers in both
  callers reachable again.
- `simple_send_with_retries` no longer swallows every `AttributeError` in its body, which turned any
  typo or API-shape change into a silent `None` — the same `None` the summarizer reads as "this
  model failed", hiding the cause of the history loss above. Its errors and retry notices also go
  through `io` instead of bare `print`, which wrote past the display while a spinner was running
  during commit-message generation.
- `read_image` reports "file not found" and "is a directory" again. Both handlers sat after
  `except OSError`, of which they are subclasses, so neither could ever run.
- `RepoMap.warned_files` is per instance rather than shared by the class, so the "repo-map can't
  include" warning is not suppressed for the rest of the process after one coder switch, and the set
  no longer grows without bound.
- `PATCH_CACHE_KEEPALIVE_DELAY` is parsed defensively and clamped to at least one second. A
  non-numeric value raised `ValueError` straight out of the request path, and `0` billed a
  completion every second.
- A redundant `nonlocal server_error` in the OAuth callback handler, which only ever assigns
  `auth_code`. Current flake8 reports it as F824; the pinned 7.1.0 does not, so the next routine
  `pre-commit autoupdate` would have failed the build for an unrelated reason.
- The test suite no longer reads or writes the developer's own state. The model-info singleton is
  built at import time and had captured the real `~/.patch/caches`, so results depended on whether
  that cache happened to be fresh and a blocked fetch could overwrite it with `{}`; the real `git`
  binary read the developer's global config, letting `core.hooksPath`, `commit.gpgsign` and
  `core.autocrlf` change test behaviour; a `webbrowser` patcher was never stopped; and several tests
  cleared the live process environment. The commit-attribution tests now configure a real `GitRepo`
  instead of a mock whose `args` attribute no production code sets, and consequently run on Windows
  as well.
- `--notifications` no longer blocks the prompt on Windows. The default was a modal
  `MessageBox::Show`, which returns only when the dialog is dismissed, run from the synchronous path
  before every input — so Patch stopped until the user clicked OK, while the macOS and Linux
  commands return immediately. It is now a non-blocking toast, and any notification command is given
  a 5-second timeout so a hanging one cannot wedge the REPL on any platform.
- `--yes-always` no longer opens URLs automatically. The offered URLs are extracted from provider
  error text, which a hostile or compromised endpoint controls, so this is now an explicit-yes
  prompt — matching how LLM-suggested shell commands are already treated.
- A terminal encoding failure at the prompt reports one line instead of a traceback; its handler sat
  after `except Exception` and could never run.
- `--voice-format` and `--voice-input-device` survive a mode switch. `Commands.clone()` runs on
  every switch (`/ask`, `/code`, `/architect`, `/model`, `/chat-mode`) and did not forward them, so
  after the first one `/voice` recorded from the default microphone as wav.
- `/paste` no longer leaves a temporary directory behind for every image. Each paste created a fresh
  one that was never removed, and outlived the process; there is now one per session, cleaned up at
  exit.
- A `patch`-format body can no longer make Patch read files outside the repository while parsing.
  Writes were already contained, but the context files a patch names were read first, with no
  equivalent check.
- The clipboard watcher reports failures through the normal output channel instead of a debug
  printer that wrote raw to stdout underneath the prompt, and the unused `dump` import is gone from
  22 modules.
- Whitespace-only lines in a unified diff keep their line ending. `cleanup_pure_whitespace_lines`
  indexed where it meant to slice, so a CRLF blank line collapsed to a lone carriage return before
  every hunk was matched.
- `/run` and `/test` no longer crawl through large output. The loop read one byte at a time and
  flushed a write for each, which cost about 28x: 2 MB of output took 1.42s to consume and now takes
  0.17s. Reading by line changes nothing about what is captured.
- On Windows, `/run` and `/test` no longer mangle commands containing `|`, `&`, `>`, `^` or quotes.
  When the parent shell was PowerShell, `powershell -Command ` was spliced onto the front of a
  string that was then handed to `cmd.exe`, which consumed those characters before PowerShell saw
  them — so the same command behaved differently than on POSIX. PowerShell is now invoked as an
  argument list.
- Optional provider SDKs are no longer installed on the strength of an object that is not a model
  name. `check_for_dependencies` ran `startswith("bedrock/")` on whatever it was handed, which is
  truthy for a stand-in, so a `boto3` install could be triggered for a model that was never a
  Bedrock model. CI has been failing on this since the checks were first enabled.
- `from patch import *`, `from patch.llm import *` and `from patch.coders import *` work. All three
  `__all__` lists held objects rather than names, so the first raised `ModuleNotFoundError` for a
  submodule named `0` (it exported the version *value*) and the last raised
  `TypeError: Item in __all__ must be str, not type`. The edit-format registry that scanned
  `patch.coders.__all__` now has its own explicit `ALL_CODERS` list.
- A file change or clipboard paste arriving as the prompt starts no longer discards what you were
  typing or stops `--watch-files` working. The watchers are started immediately before the prompt,
  and interrupting an application that is not yet running raises — which killed the file-watcher
  thread outright, and left an "interrupted" flag set that made the next prompt throw away the line
  you had actually typed. The interrupt is now dispatched on the application's own event loop and
  only takes effect once accepted, and a file watcher that does stop says so instead of vanishing.
- Completions no longer break when typing quickly. Each keystroke runs the completer on its own
  thread, and the shared word list was both latched as "ready" before it had been filled and then
  iterated while another thread was still writing to it — raising out of the completion thread and
  leaving completions empty or partial for the rest of the prompt.
- `--cache-keepalive-pings` now actually sends its pings. The worker built its request arguments
  with `dict(extra_params)`, which raises `TypeError` for the usual case of no `extra_params` — and
  it ran in a daemon thread, ahead of the `try` that would have reported it, so the thread died
  silently. Prompt caching went unwarmed and full uncached prices were paid, with nothing to
  indicate it.
- `--model-metadata-file` overrides are no longer discarded. Context windows and pricing you supply
  were returned only while litellm had not yet been imported; once anything touched it — the normal
  case — litellm's own table won instead. Local and custom models silently reported the wrong
  context limit and `$0.00` costs.
- Reasoning and thinking-token settings no longer leak between models. A `patch/extra_params` entry
  in your model settings applies to every model, and the merge assigned its nested dictionaries by
  reference; `--reasoning-effort` and `--thinking-tokens` then mutated those dictionaries in place,
  writing back into the shared table. Every model built afterwards in the same process inherited the
  settings — the weak model, the editor model, and anything switched to with `/model` — sending
  reasoning parameters, and their cost, to models never configured for them.
- An edit that empties a file is no longer reported to the model as a failed match. A SEARCH block
  covering a whole file with an empty REPLACE — truncating it — produced `""`, which every gate in
  the edit chain tested for truthiness and so could not distinguish from "no match found". The model
  was told `SearchReplaceNoExactMatch` and retried an edit that had already succeeded, and the
  response was counted against the malformed-response limit. The same applied to a unified-diff hunk
  that empties a file.
- Malformed or truncated model output no longer raises Python exceptions out of the edit parsers.
  Three cases, each of which surfaced as a traceback and was reflected back to the model as a raw
  interpreter message: a unified diff whose first hunk has no `--- `/`+++ ` header (`TypeError`; the
  `udiff-simple` format never asks for those headers except on a new file), a response cut off
  directly after a ` ```diff ` fence (`UnboundLocalError`, losing the entire response), and an empty
  SEARCH section (`IndexError`, which also discarded the record of edits already written to disk, so
  they were neither committed nor linted). An unterminated fence with content also no longer drops
  its last line.
- A wheel built from a source tree with no `.git` no longer omits `model-settings.yml`.
  `[tool.setuptools.package-data]` enumerated only `*.scm` and `*.json`; everything else reached the
  wheel through setuptools-scm's git file finder, which is unavailable in a source export or the
  Docker build context (`.dockerignore` excludes `.git`). `patch/models.py` opens that file at import
  time, so both Docker images built an entrypoint that raised `FileNotFoundError` on every
  invocation — while passing the entire test suite.
- `--dry-run` no longer commits your uncommitted work. Editing a file with unstaged changes
  triggered a pre-edit commit of that file, because `dirty_commit` did not make the `--dry-run`
  check that `auto_commit` already made — so a run intended to preview changes rewrote git history
  and spent an LLM call writing the commit message. The file edit itself was already correctly
  skipped.
- The editor and web-scraping tests no longer depend on the machine they run on. `test_pipe_editor`
  asserted the Unix default editor, so it failed on Windows and for anyone with `VISUAL`/`EDITOR`
  set, and the scrape tests reached `try_pandoc()`, which looked for a pandoc install under the
  temporary home directory and then tripped the suite's network guard trying to download one.
  • Aug 31, 2026
  • ddee929
  • zip
  • tar.gz

v0.1.0

Toggle v0.1.0's commit message
Patch's first release. This fork of aider starts its own version hist…

…ory at `0.1.0` rather

than continuing the upstream `0.86.x` numbering.

- `--trust-repo-config` (`PATCH_TRUST_REPO_CONFIG`) to opt back into execution-capable settings
  and endpoint/credential environment variables from a repository's own `.patch.conf.yml` and
  `.env`. Accepted only from the command line or your shell environment, and itself on the
  ignored list, so a repository cannot grant itself trust.
- `AGENTS.md`, a guide for AI coding agents working in this repository: setup and test commands,
  the black/isort/flake8/mypy/codespell rules CI enforces, a map of `patch/coders/` and the other
  main modules, and the generated files that must not be hand-edited.

- Renamed the Python package, imports, command, configuration files, environment variables,
  caches, Git attribution and distribution metadata from the previous identity to Patch.
- Simplified `/help` to show command help and link to the Patch website without bundling a
  second documentation application and its large optional dependency set.
- Completed the user-facing Patch rebrand across terminal and browser UI copy, notifications,
  onboarding, package metadata, and issue reporting.
- Rebranded `README.md` as Patch: replaced the previous logo, screencast and badges, and rewrote the
  feature and installation sections.
- Documented the write boundary and the repository trust model in the README.
- Trimmed the Windows CI test matrix from five Python versions to `3.10, 3.13`. Ubuntu CI covers
  every declared version, `3.10` through `3.14`.

- The bundled Jekyll website, its build/update scripts, historical release archive, interactive
  help indexer, and website-only benchmark reporting utilities.
- Legacy GitHub Actions workflows that depended on external secrets, package registries and
  domains not owned by Patch:
  - `check_pypi_version.yml` and `windows_check_pypi_version.yml` — daily cron jobs that installed
    `patch-chat` from PyPI and compared it against this repo's git tags.
  - `release.yml` — published the `patch-chat` package to PyPI.
  - `docker-release.yml` and `docker-build-test.yml` — built and pushed `patch` / `patch-full`
    images to Docker Hub. The build-test workflow also ran a multi-arch build on every push to
    `main`.
  - `issues.yml` — ran `scripts/issues.py --yes` against the issue tracker every 12 hours.
  - `pages.yml` — built the `patch/website/` Jekyll docs and deployed them to `patch.chat`.

  Remaining workflows: `pre-commit.yml`, `ubuntu-tests.yml`, `windows-tests.yml`.
- The root `CNAME` file, which pointed GitHub Pages at `patch.chat`.
- The `site/` landing page, which now lives in a separate project outside this repository.

- The test suite now clears ambient provider credentials and terminal color flags, uses a temporary
  cross-platform home directory, blocks external socket connections, and mocks analytics and web
  scraping clients. Tests no longer read user config files or depend on live services.
- Model instances now deep-copy mutable model settings before applying thinking-token and reasoning
  overrides, preventing request parameters from leaking across model switches, weak/editor models,
  and later sessions in the same process.
- GitHub Copilot token exchange now runs only for `github/` models, caches its token on the model,
  and passes it directly to LiteLLM instead of parsing or overwriting the user's `OPENAI_API_KEY`.
- Repo-map query captures are now normalized once without duplicating each tag category's final
  node or discarding all earlier nodes, restoring accurate reference counts and ranking inputs.
- Output-limit continuation is capped at eight additional requests and stops immediately if a
  prefill-capable model returns no new content, preventing unbounded API spend and memory growth.
- Startup version checks and GitHub Copilot token exchange now use bounded HTTP connect/read
  timeouts instead of potentially blocking forever.
- Edit failures caused by filesystem errors and unexpected programming exceptions are no longer
  mistaken for Git errors or returned as successfully edited files, preventing automatic commits
  of partially applied changes and ensuring the error is reflected back to the model.
- A failed SEARCH/REPLACE against a file that cannot be decoded now reports the normal match error
  instead of crashing while trying to suggest similar lines.
- `--dry-run` no longer deletes files, removes move sources, or creates directories and empty files
  when applying `patch` and unified-diff edits.
- Patch-format `Move to:` destinations now receive the same edit confirmation, gitignore, and
  dirty-file checks as other write paths, and both sides of a move are included in commits and
  `/undo` tracking.
- Builds from a source tree without `.git` no longer produce a wheel missing most of the package.
  Only the top-level `patch` package was declared, so `patch.coders`, the `.scm` repo-map queries
  and the bundled model metadata were pulled in incidentally by setuptools-scm reading the git
  index. An sdist rebuild, source export or Docker context either failed on version detection or
  produced a wheel with 48 files and no coders at all, which installed cleanly and then crashed on
  first use. The same build now produces 288 files with everything present.
- Cached-token cost accounting no longer subtracts a per-token price from a token count, which had
  inflated the reported cost of every cache-hit request.
- Unified-diff edits now report partial success correctly, so the model is no longer told to resend
  hunks that already applied.
- All three edit formats now track which files were actually written on a partial failure and
  return only those paths, instead of the `patch` coder aborting on the first failing action and the
  other formats over- or under-reporting what landed on disk.
- `--line-endings platform` (the default) now preserves an existing file's LF or CRLF style instead
  of rewriting every line to the platform's line ending on a single-line edit.
- Interrupted subprocesses (Ctrl-C during a lint/test command) are now terminated, reaped and have
  their stdout closed instead of being leaked.
- The clipboard watcher no longer busy-spins when `pyperclip.paste()` fails, such as on a headless
  or Wayland session.
- `/voice` recordings are now removed after use in both the success and failure paths, and audio is
  streamed to disk instead of buffered unbounded in memory.
- The chat-summarizer thread no longer blocks process exit on Ctrl-C.
- Cloning a coder for `/lint` no longer permanently disables cache warming on the live coder.
- A malformed `.patch.model.settings.yml` or `.patch.model.metadata.json` now aborts startup with
  the parse error instead of silently continuing with the wrong edit format, context window, or
  pricing.
- Configuration precedence (home directory, git root, working directory) is now identical across
  every parse pass, so the working directory reliably wins.
- The Docker image now pins its base image by digest, drops world-writable permissions and
  redundant ownership changes, isolates the PyTorch CPU index from general dependency resolution,
  and excludes `.git` from the build context.
- Unused runtime dependencies (`fastapi`, `orjson`, `socksio`, `backoff`) were dropped, imports used
  directly (`dotenv`, `tqdm`, `pygments`) are now declared, and the universal lock generation script
  preserves resolved pins and version markers instead of widening them into ranges.
- Tests can no longer install packages from PyPI mid-run; an autouse fixture rejects package-install
  attempts, and browser tests mock the optional dependency checks they exercise.
- Mypy is now gated in pre-commit CI over `patch/io.py`, `patch/models.py`, and
  `patch/coders/base_coder.py`.
- Image and PDF extensions are now matched case-insensitively, so uppercase filenames like
  `photo.PNG` are no longer routed through text decoding.
- The configured editor is now invoked as an argv list without a shell, and its temporary file is
  always cleaned up, including on launch failure.
- Web scraping now streams HTTP content under a 5 MB cap and checks serialized DOM size before
  copying page content into Python, instead of materializing unbounded content.
- Spinner teardown no longer races between the worker and the caller thread, which had let a
  `RecursionError` in the repo-map leave the terminal cursor hidden.
- PostHog's `on_error` callback now receives its actual arguments and detaches the failed client
  instead of printing a wall of `X` characters that could corrupt terminal output.
- Removed unreachable fuzzy-match code, an unused parameter, a shadowed `ArchitectCoder` default,
  and a redundant analytics instantiation.
- Ubuntu CI now covers every declared Python version (3.10 through 3.14), caches pip from both lock
  files, installs `pytest-env` explicitly, and no longer skips tests when only workflow files change.
- New-file and append handling for edit-block and unified-diff edits now share one tested helper
  instead of two independently drifting implementations.
- `--version` no longer reports a `0.86.3.dev+less` error sentinel, which this fork produced because
  it carries no version tags.

- Analytics no longer enables PostHog's exception autocapture, which could transmit filesystem
  paths and nearby source lines despite the consent message promising that code and personal data
  are never collected.
- Command-line and verbose history logging now redacts keys passed through `--api-key`, secret
  environment values passed through `--set-env`, and custom PostHog project keys, in addition to
  the dedicated OpenAI and Anthropic options.
- Edits are now confined to the repository. Every path the model produces is canonically resolved
  and must land inside the repository root before anything is created or modified; absolute paths,
  `..` traversal and symlinks pointing out of the tree are refused and reported. Nothing checked
  this before, so a path such as `../../.ssh/authorized_keys` was written, and an absolute path
  replaced the root entirely — with no prompt under `--yes` and no barrier at all under `--no-git`.
  Also closes two routes that bypassed even the incidental checks: the `patch` format's `Move to:`
  target, written with no confirmation, and `/add`, which compared paths as strings and so accepted
  `/work/repo-secret` while you worked in `/work/repo`.

  Files outside the repository root can no longer be edited, even when passed on the command line.
  Reference files added with `--read` are unaffected; they come from you rather than the model, and
  are only ever read.
- A cloned repository can no longer configure Patch. `git clone && patch` was previously enough to
  be attacked: a repository could ship `test: true` with `test-cmd: <payload>` to run a shell
  command, or set `OPENAI_API_BASE` in its `.env` to send every prompt and every file in your chat
  to someone else's endpoint. A `.patch.conf.yml` or `.env` found inside the repository can no
  longer set:
  - Config keys `test`, `test-cmd`, `lint`, `lint-cmd`, `auto-test`, `auto-lint`, `load`,
    `notifications-command`, `editor`, `verify-ssl`, `git-commit-verify`, `trust-repo-config`.
  - Environment variables ending in `_API_BASE`, `_BASE_URL`, `_API_URL` or `_ENDPOINT`, plus
    `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`, `REQUESTS_CA_BUNDLE`, `SSL_CERT_FILE`,
    `CURL_CA_BUNDLE`, and the `PATCH_*` equivalents of the keys above.

  Each ignored setting is reported at startup. Your home config, `~/.patch/oauth-keys.env`, and any
  file named with `--config` or `--env-file` remain trusted; ordinary variables from a repository
  `.env`, including `OPENAI_API_KEY` and `PATCH_MODEL`, still load normally; and anything passed on
  the command line always wins.
  • Aug 30, 2026
  • 41d8b44
  • zip
  • tar.gz
You are visitor 01402520 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.