majrooo-mcp-devkit
# majrooo-mcp-devkit — MCP DevKit: Safe Commands + Refactoring Tools
[](https://github.com/Majrooo/majrooo-mcp-devkit/releases)
[](LICENSE)
[](https://github.com/Majrooo/majrooo-mcp-devkit/actions/workflows/ci.yml)
[](#)
[](#)
[](https://m8ven.ai/mcp/majrooo-majrooo-mcp-devkit-1o7w0p)
> **Repository Access:** PUBLIC
> **Version:** 0.2.1 · **Tests:** 354 passing · **License:** GPL-3.0-or-later
MCP server that provides safe command execution and code refactoring tools for Cline/Claude Desktop.
## Tools
### `run_safe_command`
Execute a shell command restricted to the active project root. Dangerous commands and writes outside the active root are automatically blocked. **This is the default tool** — always use this first.
| Parameter | Type | Default | Description |
|---|---|---|---|
| `command` | string | — | Command to execute |
| `cwd` | string | primary root | Working directory (must be inside `MCP_PROJECT_ROOT` / `MCP_EXTRA_ROOTS`) |
| `maxLines` | number | 200 | Max output lines before truncation |
| `timeoutMs` | number | 60000 | Command timeout (1000–600000 ms) — raise for long jest/build runs |
### `run_destructive_command`
Execute a potentially dangerous command with explicit user confirmation. Only use when `run_safe_command` blocked the command **and** the user explicitly agreed after being informed of the specific risk.
| Parameter | Type | Default | Description |
|---|---|---|---|
| `command` | string | — | Command to execute |
| `confirm` | boolean | false | Acknowledge the risk (required for dangerous commands) |
| `cwd` | string | primary root | Working directory (must be inside `MCP_PROJECT_ROOT` / `MCP_EXTRA_ROOTS`) |
| `maxLines` | number | 200 | Max output lines before truncation |
| `timeoutMs` | number | 60000 | Command timeout (1000–600000 ms) |
### `read_log_slice`
Read a portion of a previously saved log file. Use this instead of re-running a command with higher `maxLines`. Files are read directly via Node.js (not through the shell), so it also works for truncated logs in `os.tmpdir()`.
| Parameter | Type | Default | Description |
|---|---|---|---|
| `logPath` | string | — | Path to the log file |
| `startLine` | number | 0 | Starting line (0-based) |
| `lineCount` | number | 100 | Number of lines to read |
### `list_allowed_roots`
Return the registered roots configuration: the primary project (`MCP_PROJECT_ROOT`), all allowed roots (`MCP_EXTRA_ROOTS`, including globs), the concrete existing project directories under them (usable as `cwd`), and whether `MCP_BLOCK_CROSS_ROOT_READS` is enabled. Projects with a friendly name are returned as `{ path, name }` — in that case you can also use the name as `cwd`. **Call this before working in any non-primary project** to discover the exact `cwd` value to use. Runs no commands — it only reads configuration and lists directories.
No parameters.
### `resolve_cwd`
Verify whether a path (or a friendly project name from `MCP_PROJECT_NAMES`) is inside the allowed roots and get the exact `cwd` to use for commands. Pass the path you want to work in (e.g. your workspace folder) instead of guessing.
On success returns `{ ok: true, cwd, matchedRoot, exists, name? }`; on failure `{ ok: false, error, roots }`. `exists` tells whether the resolved directory actually exists on disk (relative `cwd` values are resolved against the primary project). Runs no commands — it only validates configuration.
| Parameter | Type | Description |
|---|---|---|
| `path` | string | Path to verify (absolute, e.g. the project workspace folder) |
### `run_command_grep`
Execute a command and return only lines matching a pattern (case-insensitive regex). Use instead of `run_safe_command` when you only care about specific lines (e.g., errors in build output). **This is the replacement for Unix `grep` on Windows** — filtering happens in-process, so `grep`/`head`/`tail` are not needed.
| Parameter | Type | Description |
|---|---|---|
| `command` | string | Command to execute |
| `pattern` | string | Regex pattern to filter lines (case-insensitive) |
| `cwd` | string | Working directory (default: primary root) |
| `timeoutMs` | number | 60000 | Command timeout (1000–600000 ms) — raise for long jest runs |
### `universal_find_references`
Find all occurrences of a symbol across a workspace. Returns structured output with file, line, column, context, and optional role annotations. Use this **before any refactoring session** to understand what will break when a symbol is renamed or moved.
| Parameter | Type | Default | Description |
|---|---|---|---|
| `symbol` | string | — | Symbol to search for (word-boundary match) |
| `cwd` | string | all registered roots | Workspace root to search. When omitted, all registered roots are searched (nested roots pruned, duplicate files removed) |
| `fileExtensions` | string[] | common source extensions | Restrict to these extensions |
| `excludePatterns` | string[] | `.git`, `node_modules`, `target`, ... | Directories to skip |
| `contextLines` | number | 1 | Lines of context around each match |
| `language` | string | — (disabled) | Optional: `"rust"`, `"typescript"`, `"python"`, or `"cpp"` — enables role detection (declaration/import/usage) |
**Multi-root search (no `cwd`):** every registered root is searched. A root nested inside another root is pruned — the outer root already covers it — and files are deduplicated by resolved real path, so the same match is never listed or counted twice. When more than one root is searched, the report states them and, for each file group, the root its relative path is based on:
```
Symbol: ColAlign
Total matches: 28
Searched roots (3):
- D:\W\TS
- d:\Users Data\jox\My Documents\Python
- d:\Users Data\jox\My Documents\Rust
Duplicates skipped: 2 (same file reachable through a nested root)
pixel-blaster-engine/engine_bevy/src/lib.rs (relative to d:\Users Data\jox\My Documents\Rust)
Line 72:12 [usage] — pub use ui_panel::{
```
A path like `project/src/lib.rs` therefore always belongs to exactly one root — it is never a second copy of `src/lib.rs`. With an explicit `cwd` only that root is searched and the report keeps its compact form (`path/file.rs:`).
### `extract_code_block`
Read the full text of a function, struct, class, or method from a file. Returns precise line range + content. Includes leading annotations (`#[derive]`, `@decorator`, `/// doc comments`). String/comment-aware bracket matching prevents false depth counts from braces inside strings or comments.
| Parameter | Type | Default | Description |
|---|---|---|---|
| `file` | string | — | Source file path (must resolve inside allowed root) |
| `symbol` | string | — | Symbol name to extract |
| `contextLines` | number | 0 | Extra lines before/after the block |
| `cwd` | string | primary root | Working dir for resolving relative file paths |
### `split_file_by_declarations`
Split a large file into multiple smaller files based on top-level declarations. Optionally generates a combining file (`mod.rs` / `index.ts` / `__init__.py`). Use `dryRun: true` (default) to preview the layout before writing.
| Parameter | Type | Default | Description |
|---|---|---|---|
| `file` | string | — | Source file to split |
| `grouping` | object[] | — | `[{ module, symbols }]` — module groupings |
| `targetDir` | string | dirname(file) | Where new files are written |
| `language` | string | auto-detect | `"rust"`, `"typescript"`, `"python"`, `"cpp"` |
| `generateIndex` | boolean | true | Create combining file |
| `dryRun` | boolean | true | Preview only — write nothing |
| `overwrite` | boolean | false | Allow overwriting existing targets |
| `cwd` | string | primary root | Working dir for resolving relative file paths |
### `batch_apply_edits`
Apply multiple file edits with partial rollback on failure. Validates all edits first — if any `search` string is not found or matches multiple times (without `replaceAll`), NO files are modified. On failure during application, only files modified by the failed edit and subsequent edits are reverted; earlier successful edits are preserved. Same-file chain failures revert the entire file. Relative paths without `cwd` are resolved against the primary root; if the file doesn't exist there, all other registered roots are searched automatically (unique match → use it; multiple matches → error with instructions to specify `cwd`).
| Parameter | Type | Default | Description |
|---|---|---|---|
| `edits` | object[] | — | `[{ file, search, replace, description?, replaceAll?, excludePatterns? }]` |
| `dryRun` | boolean | true | Preview all changes without writing |
| `cwd` | string | primary root | Working dir for resolving relative file paths |
#### Response (explicit outcome)
The result text starts with a one-line `message` followed by the JSON payload:
```json
{
"dryRun": false,
"totalEdits": 3,
"validated": 3,
"appliedEdits": 2,
"written": ["D:\\proj\\a.rs", "D:\\proj\\b.rs"],
"message": "Applied 2 of 2 edit(s) to 2 file(s) — all changes are on disk.",
"preview": [{ "file": "D:\\proj\\a.rs", "action": "edit", "matchCount": 1, "applied": true }]
}
```
Key fields:
| Field | Meaning |
|---|---|
| `message` | Human-readable summary — always states explicitly whether anything was written |
| `appliedEdits` | Number of edits whose changes are on disk after the run (`0` in dry-run, `0` when nothing was written) |
| `written` | Files changed on disk after the run (empty in dry-run / when nothing was written) |
| `preview[].applied` | `true` only when that specific edit's change is on disk (never `true` in dry-run or after rollback) |
| `preview[].matchCount` | Occurrences found — informational, **not** proof that anything was written (check `applied`) |
On failure the response also carries `error`, `failedAt`, `reason` (`validation_failed` \| `write_failed`), `reverted` (files rolled back) and `nothingWritten` (`true` = this run left no change on disk at all — either nothing was ever written, or everything written was rolled back). Validation failure example:
```
VALIDATION FAILED on edit #2 of 3 — NO edits were written to disk (validation-first: nothing is written until every edit validates). Reason: search string not found in D:\proj\b.rs
```
Edits that were never evaluated get an explicit `action: "error"` preview entry (`not evaluated — batch stopped at edit #N`), so `preview` always maps 1:1 to the `edits` array. When validation fails on a file that already had a successfully applied edit in the same run (chained edits), the response switches to `partial rollback: N edit(s) kept in … ; M file(s) reverted […]`. If every write from the run was rolled back (e.g. all edits chained on one file), `nothingWritten` stays `true` and the message says `NO net changes were left on disk: N edit(s) had been written and M file(s) were rolled back […]` — an honest distinction between "never wrote" and "wrote, then undid".
### `generate_module_skeleton`
Generate a new module file with extracted symbols from a source file. Returns error with `unknownSymbols` list if any symbols are not found.
| Parameter | Type | Default | Description |
|---|---|---|---|
| `modulePath` | string | — | Target file path |
| `symbols` | string[] | — | Symbol names to include |
| `sourceFile` | string | — | Original file to extract from |
| `language` | string | auto-detect | `"rust"`, `"typescript"`, `"python"` |
| `dryRun` | boolean | true | Preview only |
| `overwrite` | boolean | false | Allow overwriting existing file |
| `cwd` | string | primary root | Working dir for resolving relative file paths |
### `verify_refactor_safety`
Semantic diff between old and new code. Catches accidental deletions before compilation. Checks: function count, signatures, export count, imports, comment ratio. Intentionally conservative — renames appear as errors requiring explicit confirmation.
| Parameter | Type | Default | Description |
|---|---|---|---|
| `before` | string | — | Original code text |
| `after` | string | — | New code text |
| `language` | string | auto-detect | `"rust"`, `"typescript"`, `"python"`, `"cpp"` |
### `report_tool_feedback`
Report a bug, improvement, or feature request about a tool of **this** server (see `list_tools`). Writes structured feedback to `.mcp/FEEDBACK.md` (project-specific, gitignored). Entries are idempotent — duplicate reports are skipped.
The `tool` name is validated against this server's tool registry: an unknown name is rejected (nothing is written) with a "did you mean …?" suggestion, so feedback about another client's built-in tool no longer lands in this log. For a missing-capability report about the server as a whole, pass `allowUnknownTool: true`.
| Parameter | Type | Default | Description |
|---|---|---|---|
| `type` | string | — | `"bug"`, `"improvement"`, or `"feature_request"` |
| `tool` | string | — | Name of the MCP tool this feedback is about — must be a tool of this server (e.g. `batch_apply_edits`) |
| `title` | string | — | Short summary (1 line) |
| `description` | string | — | Detailed description |
| `reproduction` | string | — | Steps to reproduce (optional) |
| `expected` | string | — | What you expected (optional) |
| `suggestion` | string | — | Suggested fix or improvement (optional) |
| `allowUnknownTool` | boolean | false | Accept a name that is not a tool of this server (missing-capability reports only) |
### `list_feedback`
List feedback entries from `.mcp/FEEDBACK.md`. Optionally filter by type, tool name, or status. Use this to check existing feedback before creating new entries.
| Parameter | Type | Default | Description |
|---|---|---|---|
| `type` | string | — | Filter: `"bug"`, `"improvement"`, or `"feature_request"` |
| `tool` | string | — | Filter by tool name |
| `status` | string | — | Filter: `"open"` or `"closed"` |
| `archived` | boolean | false | `true` = read `.mcp/FEEDBACK_ARCHIVE.md` (closed entries moved out of the active log) instead |
### `close_feedback`
Close an existing feedback entry by ID — sets status to `"closed"` and optionally adds resolution text. Use this to mark feedback items as resolved after fixing them.
Closing also **moves every closed entry out of the active log** into `.mcp/FEEDBACK_ARCHIVE.md`, so `FEEDBACK.md` keeps holding only open items and stays small. The move is append-first (archived before removed) and idempotent, and it self-heals: entries closed before this behaviour existed are migrated along with the next close. Read them back with `list_feedback` + `archived: true`.
| Parameter | Type | Default | Description |
|---|---|---|---|
| `id` | string | — | The feedback entry ID to close (from `list_feedback` output) |
| `resolution` | string | — | Resolution note explaining how the issue was addressed (optional) |
Closed entries are stored in two files:
| File | Contents |
|---|---|
| `.mcp/FEEDBACK.md` | Header + open entries (the active log) |
| `.mcp/FEEDBACK_ARCHIVE.md` | Header + closed entries, moved verbatim (description, reproduction, resolution, `closedAt` preserved) |
### `list_tools`
List all available MCP tools with descriptions. Use this to discover tools before starting a task. Filterable by category.
| Parameter | Type | Default | Description |
|---|---|---|---|
| `category` | string | — | Filter: `"command"`, `"refactoring"`, or `"feedback"` |
### `help_tool`
Get detailed help for a specific MCP tool — parameters, types, defaults, and description.
| Parameter | Type | Default | Description |
|---|---|---|---|
| `tool` | string | — | Tool name to get help for |
## Supported Languages
The refactoring tools support four languages for declaration parsing, symbol extraction, and role detection:
| Language | Status | Role detection | Split | Skeleton |
|---|---|---|---|---|
| **Rust** | ✅ Fully tested | ✅ | ✅ | ✅ |
| TypeScript | Experimental | ✅ | ✅ | ✅ |
| Python | Experimental | ✅ | ✅ | ✅ |
| C++ | Experimental | ✅ | ✅ | ❌ |
Tools without a `language` parameter (`batch_apply_edits`, `extract_code_block`) are **language-agnostic** — they operate on plain text and work with any language.
> **Note:** Rust has been tested in production refactoring scenarios. Other languages are structurally supported but have not been tested against real-world codebases yet.
## Configuration
The server supports **one instance, many projects**. Projects are selected per command via the `cwd` parameter; the active project also acts as the "lockbox" for write/read checks.
| Env var | Description |
|---|---|
| `MCP_PROJECT_ROOT` | Primary project root (default `cwd` when omitted). If unset, the server's own directory is used (derived from the module location, **not** `process.cwd()`). |
| `MCP_EXTRA_ROOTS` | Additional roots, semicolon separated. |
| `MCP_PROJECT_NAMES` | Friendly names for projects, semicolon separated `path=name` pairs (see below). Alias paths are also included in the allowed roots, so projects registered only via this variable can be used with file-based tools. |
| `MCP_BLOCK_CROSS_ROOT_READS` | `1` or `true` → opt-in best-effort blocking of obvious reads outside the active root. |
Entry forms supported in both variables:
- **Plain path** `D:\W\TS\majrooo-mcp-devkit` → prefix: the directory itself **and everything below it** are allowed. Registering `D:\W` covers all projects under it.
- **Glob** `D:\W\TS\*` (`*`, `**`, `?`) → any path matching the pattern (and its subtree) is allowed.
Example — **one instance, many projects**, with friendly names:
```json
{
"mcpServers": {
"majrooo-mcp-devkit": {
"command": "node",
"args": ["D:\\W\\TS\\majrooo-mcp-devkit\\build\\index.js"],
"env": {
"MCP_PROJECT_ROOT": "D:\\W\\TS\\majrooo-mcp-devkit",
"MCP_EXTRA_ROOTS": "D:\\W;D:\\python",
"MCP_PROJECT_NAMES": "D:\\W\\TS\\cb=ZbaľSa;D:\\W\\TS\\nase-zasoby=Naše zásoby"
}
}
}
}
```
`MCP_PROJECT_NAMES` maps a real project path to a readable name. This is useful when the folder name had to be shortened (e.g. Gradle path-length limits) or the project was renamed. The name can be used directly as `cwd` (e.g. `"cwd": "ZbaľSa"`), and `list_allowed_roots` will show such projects as `{ "path": "D:\\W\\TS\\cb", "name": "ZbaľSa" }`.
Switching projects is done via the `cwd` parameter, never via `cd` in the command. `cd ..`, `cd ~`, `cd C:\...`, and Windows `cd /d D:\...` are always rejected.
### Running tests / long commands (the anti-freeze workflow)
Never run test suites (`jest`/`npm test`), typecheck or builds through the Cline built-in terminal — it has no timeout and can freeze the whole window. Use the MCP tools instead:
1. Always pass the project's `cwd` (or friendly name, e.g. `"cwd": "ZbaľSa"`).
2. To filter output (e.g. jest summary), use `run_command_grep` — filtering happens in-process, so `cmd /c "... | findstr ... & echo DONE"` is **not needed and discouraged**:
```json
{
"tool": "run_command_grep",
"cwd": "ZbaľSa",
"command": "npx jest src/app/__tests__/catalog.test.tsx 2>&1",
"pattern": "Tests:|Test Suites:|FAIL|PASS|✕",
"timeoutMs": 180000
}
```
3. For full output use `run_safe_command` with a small `maxLines` — the full output is saved to a temp log for `read_log_slice`:
```json
{
"tool": "run_safe_command",
"cwd": "ZbaľSa",
"command": "npm run typecheck 2>&1",
"maxLines": 100,
"timeoutMs": 180000
}
```
4. If a run exceeds 10 minutes, run it in the background, redirect to a log file, and poll the log via `run_command_grep` — do not watch live terminal output.
### Typical workflow
1. Call `list_allowed_roots` to see the primary root, the allowed roots (including globs), and the concrete projects under them.
2. If you need to confirm a specific path, call `resolve_cwd` with your workspace folder — it returns the exact `cwd` to use and the matched root.
3. If the task targets a project other than the primary one, pass the resolved path as `cwd` on every command (`run_safe_command`, `run_destructive_command`, `run_command_grep`).
4. Otherwise, omit `cwd` — commands run in the primary root.
> **Relative paths:** File-based refactoring tools (`batch_apply_edits`, `extract_code_block`, `split_file_by_declarations`, `generate_module_skeleton`) resolve relative paths against the primary root when `cwd` is omitted. If the file doesn't exist in the primary root (or in the `cwd` root when `cwd` is provided), all other registered roots are searched automatically — a unique match is used directly, while multiple matches produce an error with instructions to specify `cwd`.
## Safety Mechanisms
| Layer | Description |
|---|---|
| **Registered roots** | `MCP_PROJECT_ROOT` / `MCP_EXTRA_ROOTS` / `MCP_PROJECT_NAMES` define the allowed project registry (prefix or glob). `cwd` must match one of them. |
| **Directory restriction** | Commands execute with `cwd` set to the resolved project root. `cd ..`, `cd ~`, absolute-path `cd`, and Windows `cd /d` are rejected. |
| **Dangerous pattern detection** | Regex blacklist blocks destructive commands (`rm -rf`, `format`, `shutdown`, `git push --force`, fork bombs, pipe-to-shell, etc.). |
| **Write-target check** | Best-effort detection of writes outside the active root (`>`, `>>`, `2>`, `copy`, `move`, `mkdir`, `tee`, `curl -o`, ...). Writing from project A into project B is blocked even if B is registered — pick B via `cwd` instead. |
| **Cross-root read check (opt-in)** | `MCP_BLOCK_CROSS_ROOT_READS=1` blocks obvious reads outside the active root (`type`/`cat`/`Get-Content`/`git -C`/Node/Python path reads...). Best-effort heuristic. |
| **Explicit confirmation** | `run_destructive_command` requires `confirm: true` for dangerous commands. |
| **Missing destructive target** | A confirmed destructive command (`rmdir`/`del`/`erase`/`Remove-Item`) whose target does not exist in the active `cwd` is rejected with a "set the `cwd` parameter" message (reason `missing_destructive_target`) instead of a raw `The system cannot find the file specified`. |
| **Buffer & timeout limits** | 50 MB max output, 60-second timeout. |
| **Output truncation** | Long outputs are saved to `os.tmpdir()` for later inspection via `read_log_slice`. |
| **Audit log** | All executions logged to `os.tmpdir()/mcp-command-audit.log` (rotated to `.old` once it exceeds 5 MB). |
### Limitations
> The dangerous-pattern blacklist and the write/read target checks are **best-effort** layers, not security guarantees. Shell features (variables, command substitution, encoding) can bypass them. For production isolation use Docker/VM sandboxing.
## Windows Notes
- Commands run through `cmd.exe`. Unix-only tools (`grep`, `head`, `tail`, ...) **do not exist** — the server returns a friendly error with alternatives instead of a raw "not recognized" blob.
- Use `run_command_grep` instead of `grep`, and `read_log_slice` or PowerShell (`Get-Content out.log -TotalCount 30`) instead of `head`.
- Long-running processes (dev server, watch mode) exceed the 60s timeout — use the built-in terminal for those.
### Output normalization
On Windows the server automatically prefixes commands with `chcp 65001 > NUL &&` so the child process emits UTF-8 instead of the legacy OEM codepage (which would otherwise decode into U+FFFD replacement characters, e.g. around thousands separators in `dir` output). ANSI color codes from tools like vitest/jest are stripped as a fallback (`NO_COLOR=1` / `FORCE_COLOR=0` are also injected into the environment), and `read_log_slice` cleans ANSI codes defensively when reading older logs.
### Redirected output reporting
When a command redirects its output into a file (`npm test > test.log 2>&1`, `>> out.log`, `2> err.log`, ...), the response reports where the output went and shows the tail of the written file instead of an empty response or a bare `Command failed: ...`. On failure the response also includes the exit code (or timeout/signal) and the captured `stdout`/`stderr`. Discard targets (`NUL`, `/dev/null`), wildcard patterns and fd-duplication tokens (`2>&1`, `>&-`) are skipped; very large files are read only from the end.
## Agent Behavior Rules
The `.clinerules` file in the project root defines how AI agents should use these tools:
1. **Always try `run_safe_command` first** — never start with `run_destructive_command`.
2. **`run_destructive_command` only after explicit user confirmation** in the current conversation — general consent ("do what you need") is not sufficient.
3. **Never bypass `directory_escape` rejections** — no chaining, absolute paths, or cwd tricks. Use the `cwd` parameter to pick a registered project.
4. **Prefer `run_command_grep` / `read_log_slice`** over increasing `maxLines` for long output, and instead of Unix `grep`/`head`/`tail`.
5. **Use the built-in terminal only for quick interactive checks** (e.g., `git status`). Large-output commands (`npm install`, build, tests) must go through `run_safe_command`.
6. **Run tests before reporting task as complete**.
## License
This project is licensed under the GNU General Public License v3.0 or later - see the [LICENSE](LICENSE) file for details.
## Development
```bash
npm run build # Compile TypeScript
npm test # Run unit tests
npm run test:watch # Watch mode
```
The server communicates over **STDIO** using the [Model Context Protocol](https://modelcontextprotocol.io).TDQS
Scored across 17 tools
Most tools have clearly separated jobs (command execution vs code extraction vs refactoring vs feedback), and paired tools like run_safe_command/run_destructive_command are explicitly distinguished. The main overlap is between list_allowed_roots and resolve_cwd, which both address allowed-root/cwd discovery, though their descriptions do enough to mostly keep them apart.
All tool names follow a consistent snake_case verb_noun pattern, with predictable families like run_*, list_*, and *feedback. There is no style mixing or vague generic naming, and the command tools (run_safe_command, run_destructive_command, run_command_grep) form a particularly coherent set.
17 tools is slightly above the ideal 3-15 range, but the server covers several distinct subdomains: command execution, code analysis, refactoring, and feedback management. The count feels a bit heavy due to near-redundant helpers like list_allowed_roots and resolve_cwd, but most tools have a clear purpose.
The tool surface covers major workflows well: command execution (safe/destructive/grep/log reading), code analysis (find references/extract), refactoring (split/generate/batch edits/verify), and a full feedback lifecycle. Minor gaps exist, such as no direct arbitrary-file read/write or explicit symbol rename tool, but these can be worked around with existing tools.