Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
f1eb891
fix: strip image blocks from release notes on website
peteromallet Mar 14, 2026
45da83d
fix: derive project root from state_file path in do_import_run follow…
peteromallet Mar 15, 2026
82cf038
fix: preserve plan_start_scores during force-rescan to protect manual…
peteromallet Mar 15, 2026
10890e6
feat: require explicit triage decisions for auto-clusters
peteromallet Mar 15, 2026
86e1c05
feat: require explicit backlog decisions for auto-clusters in staged …
peteromallet Mar 15, 2026
5ffc9a9
feat: unified triage pipeline + step detail display improvements
peteromallet Mar 16, 2026
ad784a8
feat: lifecycle transition messages and agent directives
peteromallet Mar 16, 2026
58811e9
feat: add dev test-hermes command and bump skill doc version
peteromallet Mar 16, 2026
25bca6e
fix: handle missing git in review coordinator, remove unused import
peteromallet Mar 16, 2026
dcb9f58
docs: update Hermes overlay for delegate_task, add directives docs, u…
peteromallet Mar 16, 2026
0f41dca
fix(r): correct shell quote escaping in lintr command (PR #424)
peteromallet Mar 16, 2026
55f9aaf
feat(r): add Jarl as fast R linter with autofix (PR #425)
peteromallet Mar 16, 2026
6dd58a1
fix: phpstan stderr/JSON parser fixes (PR #420)
peteromallet Mar 16, 2026
0f00b43
fix(engine): prevent workflow::create-plan re-injection after resolut…
peteromallet Mar 16, 2026
0104241
feat: add SCSS language plugin (PR #428)
peteromallet Mar 16, 2026
ba9f190
fix: Rust dep graph hangs from string-literal fake imports (PR #429)
peteromallet Mar 16, 2026
a0b0fde
fix: binding-aware unused import detection for JS/TS (PR #433)
peteromallet Mar 16, 2026
0cbd123
fix: project root detection, force-rescan plan wipe, and manual clust…
peteromallet Mar 16, 2026
8f1f6db
perf(scan): detector prefetch + cache for faster scans (PR #432)
peteromallet Mar 16, 2026
dd73df5
feat(frameworks): FrameworkSpec layer + Next.js spec (PR #414)
peteromallet Mar 16, 2026
6f2693b
fix: allow scan when queue is fully drained regardless of lifecycle p…
peteromallet Mar 16, 2026
f0250d8
fix: quote paths for Windows cmd /c and use utf-8 encoding in log rec…
peteromallet Mar 16, 2026
6531668
fix: merge retry batch results with original run before coverage check
peteromallet Mar 16, 2026
d8fdb1e
fix(docs): SKILL.md cleanup — remove unsupported frontmatter, fix fil…
peteromallet Mar 16, 2026
df9bff4
cleanup: remove dead _strip_c_style_comments_preserve_lines shim from…
peteromallet Mar 16, 2026
30f97ef
refactor: move queue_total==0 check into score_display_mode (#441)
peteromallet Mar 16, 2026
2d4a939
fix: extract anonymous functions in tree-sitter specs (R lang)
peteromallet Mar 16, 2026
11b8c16
fix(docs): sync .agents SKILL.md with docs copy, add pip fallback and…
peteromallet Mar 16, 2026
b062ad1
fix: collapse cmd /c arguments into single string for proper Windows …
peteromallet Mar 16, 2026
e1979b3
fix: skip coverage gate on partial batch retry instead of merging res…
peteromallet Mar 16, 2026
ad10a45
bump version to 0.9.10
peteromallet Mar 16, 2026
3b299e6
bump version to 0.9.10
peteromallet Mar 16, 2026
731119a
bump version to 0.9.10
peteromallet Mar 16, 2026
b631f9c
chore: gitignore .agents/ and untrack generated skill doc
peteromallet Mar 16, 2026
f8600d3
Merge branch 'cleanup/remove-dead-rust-shim' into 0.9.10
peteromallet Mar 16, 2026
4a1e929
Merge branch 'fix/scan-preflight-empty-queue-441' into 0.9.10
peteromallet Mar 16, 2026
1e26024
Merge branch 'fix/r-anonymous-function-extraction' into 0.9.10
peteromallet Mar 16, 2026
d056850
Merge branch 'fix/windows-codex-runner-442-v2' into 0.9.10
peteromallet Mar 16, 2026
e8b2e91
Merge branch 'fix/batch-retry-merge-443' into 0.9.10
peteromallet Mar 16, 2026
5f759bb
Merge branch 'fix/skill-md-cleanup' into 0.9.10
peteromallet Mar 16, 2026
23262ad
fix: add hermes and droid to update-skill help text
peteromallet Mar 16, 2026
9af7bb4
docs: draft 0.9.10 release notes
peteromallet Mar 16, 2026
c342bdb
feat(ruby): improve plugin — excludes, detect markers, default_src, s…
klausagnoletti Mar 16, 2026
cba7a92
feat: add Factory Droid skill harness support (#451)
sims1253 Mar 16, 2026
0fd0d94
docs(python): add user-facing section to README (#459)
klausagnoletti Mar 16, 2026
8502d05
feat(javascript): add plugin tests and documentation (#458)
klausagnoletti Mar 16, 2026
f2178ac
fix(docs): correct autofix command in Ruby and JS plugin READMEs
peteromallet Mar 16, 2026
e2cd0dc
docs: update release notes with late-merged PRs and stats
peteromallet Mar 16, 2026
7a835d4
fix(scss): replace {file_path} placeholders with glob patterns and us…
peteromallet Mar 16, 2026
ad4c7c6
Merge remote-tracking branch 'origin/main' into 0.9.10
peteromallet Mar 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ pytest-full.xml

# Local agent-generated skills (keep canonical copies under docs/)
.claude/
.agents/
/skills/
CLAUDE.md
AGENTS.md
Expand Down
127 changes: 127 additions & 0 deletions RELEASE_NOTES_0.9.10.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
<p align="center">
<img src="https://raw.githubusercontent.com/peteromallet/desloppify/main/assets/mascot-no-bg.png" width="180" alt="Desloppify mascot">
</p>

This release adds **experimental Hermes Agent integration** for fully autonomous cleanup loops, **framework-aware detection** with a full Next.js spec, **SCSS language support**, significant **R language improvements**, and a **scan performance boost** from detector prefetch + caching — alongside a batch of bug fixes from the community.

---

**152 files changed | 54 commits | 5,466 tests passing**

## Hermes Agent Integration (Experimental)

We've been exploring what it looks like when a codebase health tool can actually *drive* an AI agent — not just generate reports, but orchestrate the entire cleanup loop autonomously. This release ships our first experimental integration with [Hermes Agent](https://github.com/NousResearch/hermes-agent).

The core idea: desloppify already knows what needs to be done (scan, triage, review, fix). Instead of printing instructions for a human, it can now tell the agent directly — switch to a cheap model for mechanical fixes, switch to an expensive one for architectural review, reset context between tasks, and keep the agent working via `/autoreply`, all without a human in the loop.

What the integration enables:

- **Autonomous review loops** — desloppify orchestrates blind reviews via `delegate_task` subagents (up to 3 concurrent), no human needed
- **Model switching at phase boundaries** — cheap models for execution, expensive for planning/review, switched automatically
- **Context management** — automatic resets between tasks to keep the agent focused on long sessions
- **Lifecycle transitions** — desloppify tells Hermes what to do next via the Control API

### How to try it

**This requires the Control API branch of Hermes** ([NousResearch/hermes-agent#1508](https://github.com/NousResearch/hermes-agent/pull/1508)), which hasn't been merged upstream yet. Without it, Hermes works as a normal harness but can't do autonomous model switching or self-prompting.

**Step 1 — Install Hermes from the Control API branch:**

```bash
git clone -b feat/control-api-autoreply https://github.com/peteromallet/hermes-agent.git
cd hermes-agent
pip install -e .
```

**Step 2 — Install desloppify and set up the Hermes skill doc:**

```bash
pip install desloppify[full]
cd /path/to/your/project
desloppify update-skill hermes
```

This writes a `AGENTS.md` skill document into your project that teaches Hermes how to use desloppify.

**Step 3 — Start Hermes with the Control API enabled, pointed at your project:**

```bash
cd /path/to/your/project
HERMES_CONTROL_API=1 hermes
```

**Step 4 — Tell it to scan.** In the Hermes session, type:

```
Run desloppify scan, then follow its coaching output to clean up the codebase.
```

Desloppify will guide Hermes through the full lifecycle — scanning, triaging findings, running blind reviews with subagents, and fixing issues. It switches models and resets context automatically at phase boundaries.

**This is experimental and we're iterating fast.** We'd love feedback on the approach, rough edges, and what you'd want to see next. If you try it, please open an issue — every report helps.

## Framework-Aware Detection

Massive contribution from **@MacHatter1** (PR #414). A new `FrameworkSpec` abstraction layer for framework-specific detection, shipping with a full Next.js spec that understands App Router conventions, server components, `use client`/`use server` directives, and Next.js-specific lint rules. This means dramatically fewer false positives when scanning Next.js projects — framework idioms are recognized, not flagged. The spec system is extensible, so adding support for other frameworks (Remix, SvelteKit, etc.) is now a matter of writing a spec, not changing the engine.

## SCSS Language Plugin

Thanks to **@klausagnoletti** for adding SCSS/Sass support via stylelint integration (PR #428). Detects code smells, unused variables, and style issues in `.scss` and `.sass` files. @klausagnoletti has also submitted a follow-up PR (#452) with bug fixes, tests, and honest documentation — expected to land shortly after release.

## Plugin Tests, Docs, and Ruby Improvements

**@klausagnoletti** also contributed across multiple language plugins:

- **Ruby plugin improvements** (PR #462) — expanded exclusions, detect markers (`Gemfile`, `Rakefile`, `.ruby-version`, `*.gemspec`), `default_src="lib"`, `spec/` + `test/` support, and 13 wiring tests. Also adds `external_test_dirs` and `test_file_extensions` params to the generic plugin framework.
- **JavaScript plugin tests + README** (PR #458) — 12 sanity tests covering ESLint integration, command construction, fixer registration, and output parsing.
- **Python plugin README** (PR #459) — user-facing documentation covering phases, requirements, and usage.

## R Language Improvements

**@sims1253** has been steadily building out R support and contributed four PRs to this release:

- **Jarl linter** with autofix support (PR #425) — adds a fast R linter as an alternative to lintr
- **Shell quote escaping fix** for lintr commands (PR #424) — prevents command injection on paths with special characters
- **Tree-sitter query improvements** (PR #449) — captures anonymous functions in `lapply`/`sapply` calls and `pkg::fn` namespace imports
- **Factory Droid harness support** (PR #451) — adds Droid as a new skill target, following the existing harness pattern exactly

## Scan Performance: Detector Prefetch + Cache

Another big one from **@MacHatter1** (PR #432). Cold and full scan times reduced significantly. Detectors now prefetch file contents and cache results across detection phases, avoiding redundant I/O. On large codebases this is a noticeable improvement.

## Lifecycle & Triage

- **Lifecycle transition messages** — the tool now tells agents what phase they're in and what to do next, with structured directives for each transition
- **Unified triage pipeline** with step detail display
- **Staged triage** now requires explicit decisions for auto-clusters before proceeding — no more accidentally skipping triage steps

## Bug Fixes

- **Binding-aware unused import detection for JS/TS** — @MacHatter1 (PR #433). No longer flags imports used via destructuring, `as` renames, or re-export patterns. This was a significant source of false positives in real JS/TS projects.
- **Rust dep graph hangs** — @fluffypony (PR #429). String literals that look like import paths (e.g., `"path/to/thing"`) no longer cause the dependency graph builder to hang. @fluffypony also contributed Rust inline-test filtering (PR #440), which prevents `#[cfg(test)]` diagnostic noise from inflating production debt scores.
- **Project root detection** (PR #439) — fixed cases where the project root was derived incorrectly, plus force-rescan now properly wipes stale plan data, and manual clusters are visible in triage.
- **workflow::create-plan re-injection** — @cdunda-perchwell (PR #435). Resolved workflow items no longer reappear in the execution queue after reconciliation. @cdunda-perchwell also identified the related communicate-score cycle-boundary sentinel issue (#447, fix in PR #448).
- **PHPStan parser fixes** — @nickperkins (PR #420). stderr output and malformed JSON from PHPStan no longer crash the parser. Clean, focused fix.
- **Preserve plan_start_scores during force-rescan** — manual clusters are no longer wiped when force-rescanning.
- **Import run project root** — `--scan-after-import` now derives the project root correctly from the state file path.
- **Windows codex runner** (PR #453) — proper `cmd /c` argument quoting + UTF-8 log encoding for Windows. Reported by **@DenysAshikhin**.
- **Scan after queue drain** (PR #454) — `score_display_mode` now returns LIVE when queue is empty, fixing the UX contradiction where `next` says "run scan" but scan refuses. Reported by **@kgelpes**.
- **SKILL.md cleanup** (PR #455) — removes unsupported `allowed-tools` frontmatter, fixes batch naming inconsistency (`.raw.txt` not `.json`), adds pip fallback alongside uvx. Three issues all reported by **@willfrey**.
- **Batch retry coverage gate** (PR #456) — partial retries now bypass the full-coverage requirement instead of being rejected. Reported by **@imetandy**.
- **R anonymous function extraction** (PR #461) — the tree-sitter anonymous function pattern from PR #449 now actually works (extractor handles missing `@name` capture with `<anonymous>` fallback).

## Community

This release wouldn't exist without the community. Seriously — thank you all.

**@MacHatter1** delivered three major PRs (framework-aware detection, detector prefetch + cache, binding-aware unused imports) that each individually would have been a headline feature. The framework spec system in particular opens up a whole new category of detection accuracy.

**@fluffypony** contributed both the Rust dep graph hang fix and the inline-test filtering — the latter being 1,000+ lines of carefully tested Rust syntax parsing with conservative cfg predicate handling and thorough edge-case coverage.

**@sims1253** has been the driving force behind R language support, with four PRs spanning linting, tree-sitter queries, and harness support. The R plugin is becoming genuinely useful thanks to this sustained effort.

**@klausagnoletti** added SCSS support, improved the Ruby plugin, and contributed tests and documentation for JavaScript and Python plugins — seven PRs total (#428, #452, #457, #458, #459, #462). The kind of contributor who makes the codebase more trustworthy across the board.

**@cdunda-perchwell** fixed two separate workflow re-injection bugs that were causing phantom plan items. **@nickperkins** shipped a clean PHPStan parser fix.

Bug reporters **@willfrey**, **@DenysAshikhin**, **@kgelpes**, and **@imetandy** filed detailed, actionable issues that made fixes straightforward. Every one of those reports saved debugging time.
Binary file modified assets/scorecard.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions desloppify/app/cli_support/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
_add_backlog_parser,
_add_config_parser,
_add_detect_parser,
_add_directives_parser,
_add_dev_parser,
_add_exclude_parser,
_add_autofix_parser,
Expand Down Expand Up @@ -139,6 +140,7 @@ def create_parser(*, langs: list[str], detector_names: list[str]) -> argparse.Ar
# configure
_add_zone_parser(sub)
_add_config_parser(sub)
_add_directives_parser(sub)
_add_langs_parser(sub)
_add_dev_parser(sub)
_add_update_skill_parser(sub)
Expand Down
2 changes: 2 additions & 0 deletions desloppify/app/cli_support/parser_groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from desloppify.app.cli_support.parser_groups_admin import ( # noqa: F401 (re-exports)
_add_config_parser,
_add_detect_parser,
_add_directives_parser,
_add_dev_parser,
_add_autofix_parser,
_add_langs_parser,
Expand All @@ -25,6 +26,7 @@
"_add_backlog_parser",
"_add_config_parser",
"_add_detect_parser",
"_add_directives_parser",
"_add_dev_parser",
"_add_exclude_parser",
"_add_autofix_parser",
Expand Down
15 changes: 14 additions & 1 deletion desloppify/app/cli_support/parser_groups_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,17 @@ def _add_config_parser(sub) -> None:
c_unset.add_argument("config_key", type=str, help="Config key name")


def _add_directives_parser(sub) -> None:
p = sub.add_parser("directives", help="View/set agent directives for phase transitions")
d_sub = p.add_subparsers(dest="directives_action")
d_sub.add_parser("show", help="Show all configured directives")
d_set = d_sub.add_parser("set", help="Set a directive for a lifecycle phase")
d_set.add_argument("phase", type=str, help="Lifecycle phase name")
d_set.add_argument("message", type=str, help="Message to show at this transition")
d_unset = d_sub.add_parser("unset", help="Remove a directive for a lifecycle phase")
d_unset.add_argument("phase", type=str, help="Lifecycle phase name")


def _fixer_help_lines(langs: list[str]) -> list[str]:
fixer_help_lines: list[str] = []
for lang_name in langs:
Expand Down Expand Up @@ -168,6 +179,8 @@ def _add_dev_parser(sub) -> None:
)
d_scaffold.set_defaults(wire_pyproject=True)

dev_sub.add_parser("test-hermes", help="Test Hermes model switching (switch and switch back)")


def _add_langs_parser(sub) -> None:
sub.add_parser("langs", help="List all available language plugins with depth and tools")
Expand All @@ -182,6 +195,6 @@ def _add_update_skill_parser(sub) -> None:
"interface",
nargs="?",
default=None,
help="Agent interface (amp, claude, codex, cursor, copilot, windsurf, gemini, opencode). "
help="Agent interface (amp, claude, codex, cursor, copilot, windsurf, gemini, hermes, droid, opencode). "
"Auto-detected on updates if omitted.",
)
54 changes: 54 additions & 0 deletions desloppify/app/commands/dev.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ def cmd_dev(args: argparse.Namespace) -> None:
except ValueError as ex:
raise CommandError(str(ex)) from ex
return
if action == "test-hermes":
_cmd_test_hermes()
return
raise CommandError("Unknown dev action. Use `desloppify dev scaffold-lang`.")


Expand Down Expand Up @@ -165,3 +168,54 @@ def _cmd_scaffold_lang(args: object) -> None:
" Next: implement real phases/commands/detectors and run pytest.", "dim"
)
)


def _cmd_test_hermes() -> None:
"""Test Hermes model switching — switch to a random model and back."""
import random
import time

from desloppify.app.commands.helpers.transition_messages import (
_hermes_available,
_hermes_get,
_hermes_send_message,
)

if not _hermes_available():
print(colorize('Hermes not enabled. Set "hermes_enabled": true in config.json', "yellow"))
return

# Get current model
info = _hermes_get("/sessions/_any")
if "error" in info:
print(colorize(f"Cannot reach Hermes: {info['error']}", "red"))
return

original_model = info.get("model", "unknown")
original_provider = info.get("provider", "unknown")
print(f" Current model: {original_provider}:{original_model}")

# Pick a random test model
test_models = [
("openrouter", "google/gemini-2.5-flash"),
("openrouter", "meta-llama/llama-4-scout"),
("openrouter", "mistralai/mistral-medium-3"),
]
test_provider, test_model = random.choice(test_models)

# Switch to test model
print(f" Switching to: {test_provider}:{test_model}")
result = _hermes_send_message(f"/model {test_provider}:{test_model}", mode="queue")
if not result.get("success"):
print(colorize(f" Switch failed: {result.get('error', '?')}", "red"))
return
print(colorize(" ✓ Switch command sent", "green"))

# Wait a moment, then switch back
time.sleep(2)
print(f" Switching back to: {original_provider}:{original_model}")
result = _hermes_send_message(f"/model {original_provider}:{original_model}", mode="queue")
if not result.get("success"):
print(colorize(f" Switch-back failed: {result.get('error', '?')}", "red"))
return
print(colorize(" ✓ Restored original model", "green"))
Loading
Loading