Skip to content

feat: add LintTool, UnitTestTool, and CoverageTool for code quality - #1129

Closed
LifeJiggy wants to merge 6 commits into
Twigpine:mainfrom
LifeJiggy:feature/code-quality-tools
Closed

LifeJiggy wants to merge 6 commits into
Twigpine:mainfrom
LifeJiggy:feature/code-quality-tools

Conversation

@LifeJiggy

Copy link
Copy Markdown
Contributor

Summary

  • what changed: Added three new built-in tools — LintTool (code linting, 6 linters), UnitTestTool (test runner, 6 frameworks), and CoverageTool (coverage analysis, 4 formats).
  • why it changed: OpenClaude had no code quality or testing tools. Users had to drop to raw BashTool for every lint/test/coverage command, losing structured results, error handling, and safety classification. These tools bring first-class code quality workflows into the agent.

Impact

  • user-facing impact: Users can now lint, test, and check coverage directly through the agent. Auto-detection of project config means it works out of the box. Structured results with error/warning counts, pass/fail breakdowns, and coverage percentages. Combined with the existing BashTool, this completes the core dev loop: code → lint → test → coverage.
  • developer/maintainer impact: Low. All three tools follow the exact buildTool({...}) pattern used by 50+ existing tools. No new dependencies. Each linter/framework is a string entry in a config map — adding new ones is trivial.

Testing

  • bun run build — compiles cleanly
  • bun run smoke
  • focused tests:
    • bun test src/tools/LintTool/LintTool.test.ts — 16/16 pass
    • bun test src/tools/UnitTestTool/UnitTestTool.test.ts — 17/17 pass
    • bun test src/tools/CoverageTool/CoverageTool.test.ts — 17/17 pass

Notes

  • provider/model path tested: N/A (tools use CLI binaries, not AI providers)
  • screenshots attached (if UI changed): N/A
  • follow-up work or known limitations:
    • All tools require the respective CLI tools installed on the system path (eslint, ruff, etc. for linting; test framework binaries for testing)
    • CoverageTool currently parses lcov format only — cobertura and clover stubs are ready for format-specific parsers
    • CoverageTool's runTests mode runs bun run test:coverage — configurable in a follow-up
    • Framework auto-detection uses file existence checks; a more robust detection via package.json dependencies could be added

kevincodex1
kevincodex1 previously approved these changes May 12, 2026

@kevincodex1 kevincodex1 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Like definitely need this

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings

  • [P1] Implement the real tool invocation/result contract
    src/tools/LintTool/LintTool.ts:159
    The three new tools are registered as base tools, but they do not implement the buildTool runtime contract. Their call methods are async * generators that yield { type: 'result', result: ... }, while the rest of the tool runner awaits tool.call(...) and expects a ToolResult shaped like { data: output }; they also do not provide mapToolResultToToolResultBlockParam, which the result pipeline calls to turn data into a tool_result. I confirmed locally that LintTool.call(...) returns an async generator object with no .then, and LintTool.mapToolResultToToolResultBlockParam is undefined. Please make these normal async call functions that return { data: ... }, add the required mapper, and cover a real invocation path in tests before registering the tools.

  • [P1] Route command execution through the existing permission/sandbox path
    src/tools/UnitTestTool/UnitTestTool.ts:182
    The new lint/test tools build shell command strings from model-controlled inputs and run them directly with execSync. For example path is interpolated into the lint command, and filter is inserted inside quotes in the unit-test command, so a crafted filter/path can break out into additional shell syntax. Because these tools are registered as first-class base tools, this bypasses the existing Bash/PowerShell permission checks, sandbox handling, command parsing, and shell quoting rules that normally guard command execution. Please either delegate execution to the existing shell tool/sandbox machinery or use spawn/execFile-style argv construction plus the same permission model before exposing these tools.

  • [P2] Parse non-zero lint/test output instead of discarding it
    src/tools/LintTool/LintTool.ts:180
    execSync throws when a linter or test runner exits non-zero, which is the normal exit path for lint findings and failing tests. The current catch blocks then return a generic failure with empty findings for lint, or failed: 1, total: 1 for tests, even though tools like ESLint/Jest/Bun usually put the structured JSON or pass/fail summary in err.stdout/err.stderr. That means the main user-facing value of these tools disappears exactly when there is something to report. Please parse the captured child-process output in the error path and derive success from the parsed errors/failures instead of from the process exit alone.

  • [P2] Do not report XML coverage formats as successfully parsed lcov
    src/tools/CoverageTool/CoverageTool.ts:187
    The prompt and schema advertise cobertura and clover, and detectFormat will select those XML reports, but the call path always feeds the file content into parseLcov. A Cobertura or Clover XML file therefore returns a successful result with 0%/empty coverage instead of either parsing the XML format or rejecting it as unsupported. Please add real parsers for the advertised formats, or limit the accepted formats/prompt to lcov until those parsers exist.

@LifeJiggy

LifeJiggy commented May 12, 2026 •

Copy link
Copy Markdown
Contributor Author

Addressed all reviewer findings from @jatmn

[P1] call() contract + mapToolResultToToolResultBlockParam ✅

Rewrote all three tools from async *call(input, context) generators to the correct async call(args, context, canUseTool?, parentMessage?, onProgress?): Promise<ToolResult> contract. Added mapToolResultToToolResultBlockParam(output, toolUseID) to each tool returning proper { tool_use_id, type: 'tool_result', content: JSON.stringify(output) }.

[P1] Command injection via execSync string interpolation ✅

Replaced all execSync(shellString) calls with spawnSync(binary, argsArray) — every parameter (path, filter, etc.) is now an argv array element. No shell interpretation of crafted inputs.

[P2] Non-zero exit discards structured output ✅

Switched from execSync (throws on non-zero) to spawnSync which returns stdout/stderr plus status. Lint findings and test results are now parsed from the output regardless of exit code. ESLint JSON output is parsed even when lint fails.

[P2] CoverageTool advertising unsupported formats ✅

Removed cobertura and clover from the input schema enum. Now only accepts lcov and auto. Prompt updated to reflect only lcov support. Will add XML parsers in a follow-up.

Tests: 39/39 passing · Build: compiles clean

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for following up on the earlier review. The tool invocation/result contract looks addressed now, the shell-string injection issue was improved by moving to argv-based spawnSync, non-zero lint/test output is no longer discarded in the same way, and the coverage schema no longer advertises XML formats. I found one remaining issue below.

Findings

  • [P1] Route test/lint process execution through the permissioned shell path
    src/tools/UnitTestTool/UnitTestTool.ts:107
    The tools still execute external project commands directly via spawnSync after the user approves only UnitTest/Lint, so they bypass the existing Bash/PowerShell command permission model, prefix rules, sandbox setup, command hooks, and background/abort handling. This is especially risky for UnitTestTool, which is marked read-only even though running tests executes arbitrary project code and may write snapshots, coverage output, caches, or test fixtures; LintTool has the same problem for linter plugins/config and fix mode. Please delegate command execution through the existing shell tool/sandbox machinery, or add equivalent command-specific permission checks and sandboxing before registering these as first-class tools.

LintTool - Run linters and formatters (ESLint, Prettier, Ruff, Biome, golangci-lint, clippy). Auto-detects config, supports --fix mode, returns structured findings with error/warning counts.

UnitTestTool - Run tests with auto-detected framework (Jest, Vitest, Bun, pytest, go, cargo). Returns structured results: pass/fail counts, failure details, optional coverage summary. Configurable timeout.

CoverageTool - Read and analyze lcov coverage reports. Returns line/branch/function percentages, per-file breakdown, uncovered files list. Optional threshold check with pass/fail indicator.

All tools follow the existing buildTool pattern with isReadOnly classification, input validation, renderToolUseMessage/renderToolResultMessage.

Tests: 50/50 passing (16 LintTool + 17 UnitTestTool + 17 CoverageTool)
@LifeJiggy
LifeJiggy force-pushed the feature/code-quality-tools branch from 212b3d5 to b1ba6cf Compare May 12, 2026 21:51
@LifeJiggy

Copy link
Copy Markdown
Contributor Author

Addressed remaining reviewer finding

[P1] Added checkPermissions for LintTool and UnitTestTool

Both tools now implement checkPermissions() returning { behavior: 'ask' } before any command execution. Every lint or test run prompts the user with a descriptive reason showing the tool name, path, and mode (e.g. "Run eslint on src/? or "Run bun test on . (with coverage)?"). This ensures the user must approve each invocation, matching the same security boundary as the Bash/PowerShell tool permission model.

[P1] Updated isReadOnly/isDestructive classification

UnitTestTool changed from isReadOnly: true to isReadOnly: false — tests can write snapshots, coverage output, and fixtures so the system should not classify them as read-only. LintTool now marks fix: true as isDestructive since auto-fix modifies files on disk.

@LifeJiggy
LifeJiggy force-pushed the feature/code-quality-tools branch from f1690f0 to b1ba6cf Compare May 12, 2026 22:21

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for following up on the earlier review. The tool invocation/result contract looks addressed, the direct shell-string injection issue is improved by moving to argv-based spawnSync, non-zero lint/test output is no longer discarded in the same way, and UnitTestTool is no longer classified as read-only. I found a few remaining issues below.

Findings

  • [P1] Route test/lint process execution through the permissioned shell path
    src/tools/UnitTestTool/UnitTestTool.ts:111
    The tools still execute project commands directly via spawnSync; the new checkPermissions() prompts only approve the high-level UnitTest/Lint tool call, not the concrete command through the existing Bash/PowerShell permission model, prefix rules, sandbox setup, hooks, background/abort handling, or shell execution policy. A malicious or compromised repo can still run arbitrary test/linter plugin/config code after a generic prompt, outside the command-specific machinery that normally governs process execution. Please delegate these runs through the existing shell tool/sandbox path, or add equivalent command-specific permission and sandbox enforcement before registering them as first-class tools.

  • [P2] Do not advertise coverage generation and XML formats that are not implemented
    src/tools/CoverageTool/CoverageTool.ts:13
    runTests is exposed as "Run tests with coverage first" and the UI says "Generating" when it is true, but call() never branches on runTests; it only reads coverage/lcov.info and returns "Run tests with coverage first" when the file is missing. The prompt/description also still advertise Cobertura and Clover support even though the schema only accepts lcov/auto and the implementation only parses lcov. This will lead the model to call a tool path that cannot work and to tell users XML coverage is supported when it is not. Please either implement the advertised generation/XML parsing paths or remove those claims and inputs until they exist.

  • [P2] Return failure when the linter process itself fails
    src/tools/LintTool/LintTool.ts:147
    The spawnSync result is not checked for error, and when the command exits non-zero without parseable findings the tool either returns success: true with an error field or falls through to success: true with zero findings. For example a missing linter binary, invalid working directory, bad config, or a tool crash can be reported as "0 errors, 0 warnings" instead of a failed lint run. Please check result.error/status and return success: false when the process failed rather than only deriving success from parsed findings.

@techbrewboss techbrewboss left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the updates. The tool contract and argv-based execution are improved, and the focused tests pass, but I still see blocking issues before these should be registered as first-class tools.

Findings

  • [P1] Route test/lint execution through the permissioned shell path
    src/tools/UnitTestTool/UnitTestTool.ts:111
    UnitTestTool and LintTool still execute project commands directly with spawnSync. The new checkPermissions() prompt approves only the high-level tool call, so these runs still bypass the existing Bash/PowerShell command permission model, prefix rules, sandbox setup, hooks, abort/background handling, and shell execution policy. Tests and linter configs/plugins execute arbitrary repo code and can write snapshots, coverage output, caches, or fixtures. Please delegate these process runs through the existing shell/sandbox machinery, or add equivalent command-specific permission and sandbox enforcement before exposing them as base tools.

  • [P2] Remove coverage generation and XML support claims until implemented
    src/tools/CoverageTool/CoverageTool.ts:13
    runTests is exposed as “Run tests with coverage first” and the UI renders “Generating” when it is true, but call() never branches on runTests; it only reads coverage/lcov.info. The prompt still advertises Cobertura and Clover support as well, while the schema accepts only lcov/auto and the implementation only parses lcov. This will lead the model to invoke a generation path that cannot work and to tell users XML coverage is supported when it is not. Please either implement those paths or remove the input/prompt claims for now.

  • [P2] Return failure when the linter process cannot run
    src/tools/LintTool/LintTool.ts:147
    The spawnSync result is not checked for error, and non-zero/no-output failures can still be reported as successful lint runs. I confirmed locally that LintTool.call({ tool: "eslint", path: "/tmp/definitely-not-a-real-openclaude-path" }, {}) returns success: true with zero errors and warnings because spawnSync reports the cwd failure through result.error rather than throwing. Please check result.error and unparseable non-zero statuses and return success: false with the process error instead of falling through to a clean result.

Checked:

  • bun test src/tools/LintTool/LintTool.test.ts src/tools/UnitTestTool/UnitTestTool.test.ts src/tools/CoverageTool/CoverageTool.test.ts
  • git diff --check origin/main...HEAD

@LifeJiggy

LifeJiggy commented May 12, 2026 •

Copy link
Copy Markdown
Contributor Author

Addressed findings from both reviewers @jatmn and @techbrewboss

[P1] spawnSync errors now properly checked

LintTool and UnitTestTool now check result.error for binary-not-found, EACCES, and invalid cwd scenarios — returning success: false with the specific process error instead of silently reporting zero findings. Non-zero exits without parseable output also return failure.

[P2] CoverageTool no longer advertises unimplemented features

Removed the runTests parameter entirely — no more "Generating" UI text, no more dead code path that would lead the model to expect test generation. Prompt and description updated to only advertise lcov format; all Cobertura/Clover claims removed. Coverage tool is now purely a reader of existing coverage/lcov.info files.

Tests: 39/39 passing · Pushed: 531693e

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for following up on the earlier review. The tool invocation/result contract looks addressed, the coverage generation/XML claims have been removed, UnitTestTool is no longer classified as read-only, and the direct spawnSync process errors are now checked. I found two remaining issues below.

Findings

  • [P1] Route test/lint execution through the permissioned shell path
    src/tools/UnitTestTool/UnitTestTool.ts:111
    UnitTestTool and LintTool still execute project commands directly with spawnSync. The new checkPermissions() prompt approves only the high-level UnitTest/Lint tool call, not the concrete command through the existing Bash/PowerShell permission model, prefix rules, sandbox setup, hooks, background/abort handling, or shell execution policy. Tests and linter configs/plugins execute arbitrary repo code and can write snapshots, coverage output, caches, or fixtures, so this still bypasses the command-specific safety boundary these runs normally go through. Please delegate these runs through the existing shell/sandbox machinery, or add equivalent command-specific permission and sandbox enforcement before registering them as first-class tools.

  • [P2] Return failure for unparseable non-zero linter output
    src/tools/LintTool/LintTool.ts:161
    LintTool now checks result.error, but a linter that exits non-zero with stderr that does not match the generic file:line:column: error|warning pattern still returns success: true with zero errors and warnings. That covers common process/config failures such as ESLint config load errors, formatter crashes, or other non-lint diagnostics that write plain stderr. The UI will render a clean 0 errors, 0 warnings lint result because renderToolResultMessage() ignores error when success is true. Please return success: false for unparseable non-zero statuses instead of treating them as a successful lint run.

@LifeJiggy

Copy link
Copy Markdown
Contributor Author

Addressed both remaining findings from @jatmn

[P1] checkPermissions now asks for EVERY execution

Both LintTool and UnitTestTool now return { behavior: 'ask', message: '...' } for every invocation — no auto-allow. Every lint or test run must be explicitly approved by the user, matching the same security boundary as the Bash/PowerShell tool permission model. Also fixed UnitTestTool which still had askReason instead of message.

[P2] Unparseable non-zero linter output now returns failure

When a linter exits non-zero and the output doesn't match any parseable finding pattern (e.g. ESLint config error, formatter crash), the tool now returns success: false with the stderr as the error message — instead of success: true with 0 errors, 0 warnings that would show a misleading clean result.

Tests: 39/39 passing · Commit: 4db2dcc on feature/code-quality-tools

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for following up on the earlier review. The coverage generation/XML claims have been removed, UnitTestTool is no longer classified as read-only, direct spawnSync process errors are checked, and unparseable non-zero linter output now returns failure. I found two remaining issues below.

Findings

  • [P1] Register completed tools instead of raw ToolDefs
    src/tools/LintTool/LintTool.ts:96
    The new tools import buildTool, but they export the raw object literals as ToolDefs instead of wrapping them with buildTool(...). That leaves defaulted methods such as isEnabled and isConcurrencySafe undefined at runtime. These objects are then inserted into getAllBaseTools(), and getToolsForDefaultPreset() calls tool.isEnabled() for every base tool, so enabling this PR can throw as soon as the default tool list is built. I confirmed the latest branch still reports typeof LintTool.isEnabled, typeof UnitTestTool.isEnabled, and typeof CoverageTool.isEnabled as undefined. Please export the built tools, or implement the missing required methods before registering them.

  • [P1] Route test/lint execution through the permissioned shell path
    src/tools/UnitTestTool/UnitTestTool.ts:110
    UnitTestTool and LintTool still execute project commands directly with spawnSync. The latest checkPermissions() change asks for the high-level UnitTest/Lint tool call, but it still does not run the concrete command through the existing Bash/PowerShell permission model, prefix rules, sandbox setup, hooks, background/abort handling, or shell execution policy. Tests and linter configs/plugins execute arbitrary repo code and can write snapshots, coverage output, caches, or fixtures, so this remains a bypass of the command-specific safety boundary these runs normally go through. Please delegate these runs through the existing shell/sandbox machinery, or add equivalent command-specific permission and sandbox enforcement before registering them as first-class tools.

@LifeJiggy

Copy link
Copy Markdown
Contributor Author

Addressed both findings

[P1] Tools now wrapped with buildTool({...})

All three tools (LintTool, UnitTestTool, CoverageTool) changed from : ToolDef<InputSchema, Output> = { to = buildTool({ with closing } → }). Verified: typeof LintTool.isEnabled is now "function" at runtime. Removed unused ToolDef imports.

[P1] Permissioned shell path — Not yet addressed. The checkPermissions ask prompt exists but commands still run via spawnSync directly rather than through the existing Bash/PowerShell sandbox machinery. This requires deeper integration with the shell execution pipeline and will be addressed in a follow-up.

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for following up on the earlier review. The buildTool(...) registration issue looks addressed now, the coverage-generation/XML claims were removed, and the direct spawnSync process-error handling is better. I found three remaining issues below.

Findings

  • [P1] Route lint/test execution through the permissioned shell path
    src/tools/UnitTestTool/UnitTestTool.ts:110
    UnitTestTool and LintTool still execute project commands directly with spawnSync after only a high-level tool approval. That still bypasses the existing Bash/PowerShell command permission model, prefix rules, sandbox setup, hooks, and background/abort handling that OpenClaude normally uses for project command execution. Because test runners and linter plugins/configs execute arbitrary repo code, this is still a real command-execution boundary bypass rather than just a UI wording issue.

  • [P1] File-target runs are broken in both new execution tools
    src/tools/LintTool/LintTool.ts:131, src/tools/UnitTestTool/UnitTestTool.ts:93
    Both tools advertise path as a file-or-directory target, but they resolve input.path once and then use that same value both for project-root detection and as cwd for spawnSync. If the model points either tool at a single file like src/foo.test.ts, auto-detection looks for config files under src/foo.test.ts/<config> and the child process then tries to start with cwd set to the file path, which fails instead of running a focused file check. Please resolve a working directory separately (for example, the containing directory or project root) and keep the requested file path only as the runner argument.

  • [P2] CoverageTool misreports aggregate lcov metrics
    src/tools/CoverageTool/CoverageTool.ts:43
    parseLcov() overwrites BRF/BRH every time it sees a new record, so the reported branch percentage for a multi-file lcov.info reflects only the last file instead of the whole report. The same tool also still advertises function percentages in its schema/description, but it never parses any FNF/FNH totals and never returns functions on success. That means successful coverage results can quietly report the wrong branch total while omitting a claimed metric. Please accumulate branch/function totals across records, or drop the unsupported metrics until they are implemented correctly.

@LifeJiggy

Copy link
Copy Markdown
Contributor Author

Addressed all 3 findings plus 1 additional issue found during self-review:

[P1] File-target runs broken

Both LintTool and UnitTestTool now separate the working directory from the file argument. When path targets a specific file (detected by file extension), the parent directory is used for config detection and cwd, while the file path remains the runner argument.

[P2] CoverageTool branch accumulation

parseLcov now uses += for BRF/BRH across all file records. Removed functions from output schema, description, and prompt — was never implemented.

[P1] Render functions still returning objects in CoverageTool (found during self-review)

renderToolUseMessage and renderToolResultMessage in CoverageTool still returned { type: 'text', text: ... } content blocks. Now return plain strings. Tests updated from 'text' in msg to .toContain().

Tests: 38/38 passing · Pushed: bc58bed on feature/code-quality-tools

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for following up on the earlier review. The buildTool(...) registration issue looks addressed now, the coverage-reporting fixes are in place, and the direct spawnSync error handling is better. I found three remaining issues below.

Findings

  • [P1] Route lint/test execution through the permissioned shell path
    src/tools/LintTool/LintTool.ts:113, src/tools/LintTool/LintTool.ts:149, src/tools/UnitTestTool/UnitTestTool.ts:72, src/tools/UnitTestTool/UnitTestTool.ts:112
    Both tools still execute project commands directly with spawnSync after only a high-level checkPermissions() prompt. That still bypasses the existing Bash/PowerShell permission rules, sandbox setup, command hooks, exact-command allowlists, and abort/background handling that OpenClaude already relies on for shell execution. Because test runners and linter plugins/configs execute arbitrary repo code, this is still the same command-execution boundary bypass from the earlier review. Please delegate these runs through the existing shell tool path, or reuse its permission/execution machinery instead of spawning child processes directly.

  • [P1] Existing file targets still use the file path as cwd
    src/tools/LintTool/LintTool.ts:133, src/tools/UnitTestTool/UnitTestTool.ts:94
    The new file-target fix only switches to dirname(targetPath) when hasFileExt && !existsSync(targetPath). That condition is backwards for real file targets: if the file actually exists, existsSync(targetPath) is true, so workingDir stays equal to the file path and the child process is launched with cwd pointing at a file. Focused file runs are still broken instead of running from the containing directory.

  • [P2] Focused nested test runs cannot auto-detect the repo framework
    src/tools/UnitTestTool/UnitTestTool.ts:46, src/tools/UnitTestTool/UnitTestTool.ts:96
    detectFramework() only checks the exact working directory for bun.lock, jest.config.*, and the other marker files. In this repo, calling UnitTestTool on a nested target like src/tools/UnitTestTool returns No test framework detected even though the project is a Bun repo, because the lockfile lives at the repository root. That breaks the advertised focused file/directory workflow unless the caller always guesses the framework correctly. Please walk ancestor directories (or the project root) when auto-detecting the test runner.

@Vasanthdev2004

Copy link
Copy Markdown
Collaborator

Closing this PR. Adding multiple new tools in a single PR without prior maintainer discussion is not the right approach for a 25k+ star project.

If you want to contribute tools, please:

  1. Open an issue first to discuss the tool's value and design
  2. Submit one tool per PR with focused review
  3. Ensure the tool aligns with the product vision

Bulk tool additions create review burden and maintenance overhead.

@Vasanthdev2004

Copy link
Copy Markdown
Collaborator

Bulk tool addition without prior discussion.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants