Audit shell calls in cc-os plugins and replace them with native Ruby or gems #845

Open
opened 2026-09-30 16:43:42 +00:00 by jared · 2 comments
Owner

Problem

The cc-os plugins start shell processes where native Ruby or a gem can do the same work. A shell call parses free text, depends on the PATH, and fails with process errors such as Errno::ENOENT. It also hides errors behind an exit code. #843 found one case: the Required-work check runs ticket text through ProjectShell.capture2e, although each check is a text search in a file. Other shell calls can have the same problem. Git commands are the largest known group.

Reproduce

n/a: audit ticket; nothing to reproduce.

Approach

Audit each shell call in plugins/*/lib and plugins/*/bin: ProjectShell.capture2e, Open3.*, system, backticks, %x, spawn, and IO.popen.

For each call site, record:

  • path:line and the command it runs;
  • what the result is used for;
  • the proposed replacement: native Ruby (File, Dir, FileUtils), the git gem, or another library; or the reason to keep the shell.

Known inputs:

  • Git commands move to the git gem. The user is adding the gem in session f531ed01.
  • Running tests and linters in another project can stay a shell call, because that project has its own bundle and binaries.
  • The Required-work check is out of scope. #843 replaces it with typed checks.

Output: an audit report as a comment on this ticket, and one follow-up ticket for each group of replacements.

Required work: none beyond the acceptance criteria.

Acceptance criteria

  1. Given the audit report, when you compare it with a search for the listed shell forms, then each call site is in the report.
  2. Given each call site in the report, when you read its entry, then it names a replacement or gives a reason to keep the shell.
  3. Given each group of replacements, when the audit is complete, then a follow-up ticket exists for it.

Origin

  • Trigger: #843 discussion; the user asked for this audit on 2026-09-30.
  • Improvised this session: none
  • Chain: Required-work check crashes on /os-adr:find ← check text runs as a shell command (adapter/required_work_check.rb:16) ← DESIGN: ADR-0209, ADR-0244 decision 4
  • Root candidate: this ticket
  • Where: plugins/os-sdlc/lib/os_sdlc/adapter/project_shell.rb and the other shell call sites
  • Session: fc9f2295-3034-4595-9344-2d94ad4e3ee1
  • Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/fc9f2295-3034-4595-9344-2d94ad4e3ee1.jsonl
## Problem The cc-os plugins start shell processes where native Ruby or a gem can do the same work. A shell call parses free text, depends on the PATH, and fails with process errors such as `Errno::ENOENT`. It also hides errors behind an exit code. #843 found one case: the Required-work check runs ticket text through `ProjectShell.capture2e`, although each check is a text search in a file. Other shell calls can have the same problem. Git commands are the largest known group. ## Reproduce n/a: audit ticket; nothing to reproduce. ## Approach Audit each shell call in `plugins/*/lib` and `plugins/*/bin`: `ProjectShell.capture2e`, `Open3.*`, `system`, backticks, `%x`, `spawn`, and `IO.popen`. For each call site, record: - `path:line` and the command it runs; - what the result is used for; - the proposed replacement: native Ruby (`File`, `Dir`, `FileUtils`), the `git` gem, or another library; or the reason to keep the shell. Known inputs: - Git commands move to the `git` gem. The user is adding the gem in session f531ed01. - Running tests and linters in another project can stay a shell call, because that project has its own bundle and binaries. - The Required-work check is out of scope. #843 replaces it with typed checks. Output: an audit report as a comment on this ticket, and one follow-up ticket for each group of replacements. Required work: none beyond the acceptance criteria. ## Acceptance criteria 1. Given the audit report, when you compare it with a search for the listed shell forms, then each call site is in the report. 2. Given each call site in the report, when you read its entry, then it names a replacement or gives a reason to keep the shell. 3. Given each group of replacements, when the audit is complete, then a follow-up ticket exists for it. ## Origin - Trigger: #843 discussion; the user asked for this audit on 2026-09-30. - Improvised this session: none - Chain: Required-work check crashes on `/os-adr:find` ← check text runs as a shell command (`adapter/required_work_check.rb:16`) ← DESIGN: ADR-0209, ADR-0244 decision 4 - Root candidate: this ticket - Where: plugins/os-sdlc/lib/os_sdlc/adapter/project_shell.rb and the other shell call sites - Session: fc9f2295-3034-4595-9344-2d94ad4e3ee1 - Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/fc9f2295-3034-4595-9344-2d94ad4e3ee1.jsonl
Author
Owner

Audit report: shell calls in plugins//lib and plugins//bin (2026-09-30)

Forms searched: ProjectShell.*, Open3.*, system, backticks, %x, spawn, IO.popen, exec. There are 27 direct call sites. Backticks, %x, IO.popen, and exec have no real hits.

path:line command replacement or reason to keep
os/lib/worktree/git.rb:16 git git gem (session f531ed01)
os/lib/worktree/git.rb:24 git git gem (session f531ed01)
os/lib/worktree/git.rb:30 git git gem (session f531ed01)
os/lib/worktree/after_merge.rb:6 rubocop --stop-server KEEP: stops a daemon; no Ruby API
os/lib/worktree/after_merge.rb:29 bin/refresh-plugins KEEP: repository script
os-aidd-lint/lib/aidd_lint/gate.rb:50 git show HEAD:<path> git gem (session f531ed01)
os-backlog/lib/backlog/issues/shell_runner.rb:8 tea, gh, git remote, which KEEP for tea/gh (tracker CLI). git remote → #853. which → #852
os-backlog/bin/wakeup-poll:40 tea/gh issue list --json KEEP: tracker CLI
os-backlog/bin/wakeup-poll:117 tmux new-session ... claude KEEP: starts a tmux session
os-sdlc/lib/os_sdlc/adapter/git/repo.rb:32 git git gem (session f531ed01)
os-sdlc/lib/os_sdlc/adapter/lint_changed/changed_files.rb:35 git diff, git ls-files git gem (session f531ed01)
os-sdlc/lib/os_sdlc/adapter/required_work_check.rb:16 ticket text REMOVED by #843
os-sdlc/lib/os_sdlc/adapter/test_runner.rb:6 project test command KEEP: another project's bundle
os-sdlc/lib/os_sdlc/adapter/gate/evaluator.rb:20 gate command KEEP: another project's bundle
os-sdlc/lib/os_sdlc/command/project/config_command.rb:59 candidate test command KEEP: another project's bundle
os-sdlc/lib/os_sdlc/adapter/autocorrect_prepass/capture_runner.rb:15 rubocop KEEP the shell; fix the environment → #850
os-sdlc/lib/os_sdlc/query/worklist/report.rb:18 rubocop KEEP: lints the invoking project (ADR-0082)
os-sdlc/lib/os_sdlc/adapter/lint_changed/autocorrect.rb:15 rubocop -a KEEP: lints the hook event's repository
os-sdlc/bin/dup-sweep:24 flay in-process gem → #851
os-sdlc/bin/dead-code-sweep:14 debride in-process gem → #851
os-sdlc/lib/os_sdlc/model/resolver/token_query.rb:38 ast-grep KEEP: Rust binary
os-sdlc/lib/os_sdlc/model/resolver/token_resolution.rb:51 ast-grep KEEP: Rust binary
os-sdlc/lib/os_sdlc/adapter/lsp_session/symbols_request/ast_definition.rb:34 ast-grep KEEP: Rust binary
os-sdlc/lib/os_sdlc/adapter/probe.rb:64 ast-grep KEEP: Rust binary
os-sdlc/lib/os_sdlc/command/lsp_session.rb:20 ruby-lsp KEEP: server process on stdio pipes
os-sdlc/lib/os_sdlc/adapter/mirror_tickle.rb:20 pbm sync KEEP: external binary
os-sdlc/lib/os_sdlc/adapter/issue_source/shell.rb:7 tea, gh KEEP for now: tracker CLI (see below)

Indirect git calls through generic runners: adapter/git/diff.rb:34 (git gem, f531ed01), os-backlog config_write_command.rb:57 and inspect_command.rb:16 (#853).

Follow-up tickets

  • #850: autocorrect pre-pass capture runs outside the unbundled environment (os-sdlc).
  • #851: flay and debride in-process (os-sdlc).
  • #852: which → PATH check (os-backlog).
  • #853: origin remote through the git gem (os-backlog). Blocked by the git gem work.

Pending decision (no ticket yet)

tea and gh stay as shell calls until a decision is made. The git gem cannot manage issues. A Forgejo REST client (and octokit for GitHub) would put ticket text in a JSON request body in place of argv, and it would return JSON. tea issues create (tea 0.13.0) has no --output json, so the create path cannot stop text parsing through tea. This change needs an ADR before tickets.

## Audit report: shell calls in plugins/*/lib and plugins/*/bin (2026-09-30) Forms searched: `ProjectShell.*`, `Open3.*`, `system`, backticks, `%x`, `spawn`, `IO.popen`, `exec`. There are 27 direct call sites. Backticks, `%x`, `IO.popen`, and `exec` have no real hits. | path:line | command | replacement or reason to keep | |---|---|---| | os/lib/worktree/git.rb:16 | git | git gem (session f531ed01) | | os/lib/worktree/git.rb:24 | git | git gem (session f531ed01) | | os/lib/worktree/git.rb:30 | git | git gem (session f531ed01) | | os/lib/worktree/after_merge.rb:6 | `rubocop --stop-server` | KEEP: stops a daemon; no Ruby API | | os/lib/worktree/after_merge.rb:29 | `bin/refresh-plugins` | KEEP: repository script | | os-aidd-lint/lib/aidd_lint/gate.rb:50 | `git show HEAD:<path>` | git gem (session f531ed01) | | os-backlog/lib/backlog/issues/shell_runner.rb:8 | tea, gh, `git remote`, `which` | KEEP for tea/gh (tracker CLI). `git remote` → #853. `which` → #852 | | os-backlog/bin/wakeup-poll:40 | `tea/gh issue list --json` | KEEP: tracker CLI | | os-backlog/bin/wakeup-poll:117 | `tmux new-session ... claude` | KEEP: starts a tmux session | | os-sdlc/lib/os_sdlc/adapter/git/repo.rb:32 | git | git gem (session f531ed01) | | os-sdlc/lib/os_sdlc/adapter/lint_changed/changed_files.rb:35 | `git diff`, `git ls-files` | git gem (session f531ed01) | | os-sdlc/lib/os_sdlc/adapter/required_work_check.rb:16 | ticket text | REMOVED by #843 | | os-sdlc/lib/os_sdlc/adapter/test_runner.rb:6 | project test command | KEEP: another project's bundle | | os-sdlc/lib/os_sdlc/adapter/gate/evaluator.rb:20 | gate command | KEEP: another project's bundle | | os-sdlc/lib/os_sdlc/command/project/config_command.rb:59 | candidate test command | KEEP: another project's bundle | | os-sdlc/lib/os_sdlc/adapter/autocorrect_prepass/capture_runner.rb:15 | rubocop | KEEP the shell; fix the environment → #850 | | os-sdlc/lib/os_sdlc/query/worklist/report.rb:18 | rubocop | KEEP: lints the invoking project (ADR-0082) | | os-sdlc/lib/os_sdlc/adapter/lint_changed/autocorrect.rb:15 | `rubocop -a` | KEEP: lints the hook event's repository | | os-sdlc/bin/dup-sweep:24 | flay | in-process gem → #851 | | os-sdlc/bin/dead-code-sweep:14 | debride | in-process gem → #851 | | os-sdlc/lib/os_sdlc/model/resolver/token_query.rb:38 | ast-grep | KEEP: Rust binary | | os-sdlc/lib/os_sdlc/model/resolver/token_resolution.rb:51 | ast-grep | KEEP: Rust binary | | os-sdlc/lib/os_sdlc/adapter/lsp_session/symbols_request/ast_definition.rb:34 | ast-grep | KEEP: Rust binary | | os-sdlc/lib/os_sdlc/adapter/probe.rb:64 | ast-grep | KEEP: Rust binary | | os-sdlc/lib/os_sdlc/command/lsp_session.rb:20 | ruby-lsp | KEEP: server process on stdio pipes | | os-sdlc/lib/os_sdlc/adapter/mirror_tickle.rb:20 | `pbm sync` | KEEP: external binary | | os-sdlc/lib/os_sdlc/adapter/issue_source/shell.rb:7 | tea, gh | KEEP for now: tracker CLI (see below) | Indirect git calls through generic runners: `adapter/git/diff.rb:34` (git gem, f531ed01), `os-backlog config_write_command.rb:57` and `inspect_command.rb:16` (#853). ## Follow-up tickets - #850: autocorrect pre-pass capture runs outside the unbundled environment (os-sdlc). - #851: flay and debride in-process (os-sdlc). - #852: `which` → PATH check (os-backlog). - #853: origin remote through the git gem (os-backlog). Blocked by the git gem work. ## Pending decision (no ticket yet) tea and gh stay as shell calls until a decision is made. The git gem cannot manage issues. A Forgejo REST client (and octokit for GitHub) would put ticket text in a JSON request body in place of argv, and it would return JSON. `tea issues create` (tea 0.13.0) has no `--output json`, so the create path cannot stop text parsing through tea. This change needs an ADR before tickets.
Author
Owner

Pending decision resolved: ADR-0256 (81331418). Tracker backends call the Forgejo and GitHub REST APIs in-process; tokens come from FORGEJO_TOKEN and GITHUB_TOKEN. Follow-up tickets: #855 forgejo-client gem, #856 os-backlog tea → forgejo-client, #857 os-backlog gh → octokit, #858 os-sdlc tea/gh → forgejo-client/octokit. Status: #850, #851, #852 closed; #853 waits for the git gem.

Pending decision resolved: ADR-0256 (81331418). Tracker backends call the Forgejo and GitHub REST APIs in-process; tokens come from FORGEJO_TOKEN and GITHUB_TOKEN. Follow-up tickets: #855 forgejo-client gem, #856 os-backlog tea → forgejo-client, #857 os-backlog gh → octokit, #858 os-sdlc tea/gh → forgejo-client/octokit. Status: #850, #851, #852 closed; #853 waits for the git gem.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
jared/cc-os#845
No description provided.