Cross-plugin binary lookup by relative path #635
Labels
No labels
P0
P1
P2
P3
bug
category/code-change
category/cross-repo
category/eval-run
category/investigation
category/no-test-target
category/owner-ruling
category/prose-artifact
category/recurring-chore
category/structure
category/umbrella
crc-refactor
create
delete
enhancement
filed-by/agent
filed-by/user
frozen
lint-rule
needs-info
needs-triage
next
plugin/cc-architect
plugin/os
plugin/os-adr
plugin/os-aidd-lint
plugin/os-backlog
plugin/os-context
plugin/os-doc-hygiene
plugin/os-sdlc
plugin/os-vault
project/cc-os
ready-for-agent
ready-for-human
recurring
review
rule-proposal
update
waiting
wayfinder:grilling
wayfinder:map
wayfinder:map
wayfinder:research
wayfinder:task
wayfinder:task
wontfix
worklist/deviations
worklist/lint-rule
worklist/new-implement-build
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
jared/cc-os#635
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
Three cross-plugin or cross-process binary/shell lookups are fragile: a hardcoded relative path with a silent fallback, a boolean-only system call, and a bare backtick that bypasses an existing shell abstraction. These are legitimate process boundaries (separate plugins, no shared load path) — the fix is to make the lookup and the call robust, not to merge the boundaries.
Observed
Verified on main today (2026-09-24):
plugins/os-sdlc/lib/os_sdlc/adapter/tea_tracker/body_fetch.rb:22-25os_backlog_binhardcodesFile.expand_path("../../../../os-backlog/bin/os-backlog", __dir__)and silently falls back to the bare string"os-backlog"if that path is missing. Sole caller isTeaTracker#fetch_body(tea_tracker.rb:37), which wraps anyStandardErrorintoHarnessError. No behavior test pins the current fallback.plugins/os/lib/worktree/finish_command.rb:124-126system('os-sdlc-runner', ...) || warn(...)reports only a boolean; it cannot distinguish "binary not found" from "binary found but exited nonzero." Pinned byplugins/os/tests/worktree/finish_command_sweep_failure_does_not_block_removal_test.rb, which asserts/os-sdlc-runner/appears in stderr and that the process exits 0.plugins/os-backlog/lib/backlog/config_write_command.rb:39-41uses a bare backtick,`git remote get-url origin 2>/dev/null`.strip, instead of the plugin's ownBacklog::Issues::ShellRunner#capture(*cmd), which wrapsOpen3.capture3and returns[out, success?](stderr discarded, preserving the2>/dev/nullsemantics).tests/cli_config_write_command_test.rbruns in a non-git tmpdir, so the remote resolves to nil either way — this test does not distinguish the two implementations.~/.claude/plugins/<name>/bin, and Claude Code puts those directories onPATH(which os-backlogresolves to/home/jared/.claude/plugins/os-backlog/bin/os-backlogin an installed session). The sibling relative path inbody_fetch.rbis the path that fires in a source checkout, not an installed plugin.ENVread inplugins/os-sdlc/libis a defect. This rules out a new environment-variable override such asOS_BACKLOG_BIN; resolution must go throughPATHonly.Reproduce
Approach
Add a small
BinaryLocatorper affected plugin. It searchesPATHentries for the target binary name; on a miss it raises a named error listing everyPATHentry it tried. Foros-backlog, the sibling relative path used in a source checkout remains a second candidate, tried afterPATH, and is included in the "tried" list on failure.os-sdlc-runneris resolved the same way fromfinish_command.rb: when the binary is missing, warn with "os-sdlc-runner not found on PATH (tried: ...), usage not swept"; when it is found but exits nonzero, warn with the resolved path and the exit status; sweep failure never blocks worktree removal either way.config_write_command.rb'sorigin_remotecallsBacklog::Issues::ShellRunner.new.capture('git', 'remote', 'get-url', 'origin')and treats the result as nil unless the call succeeded and the output is non-empty.Required work:
OsSdlc::Adapter::BinaryLocatorand use it inbody_fetch.rbin place of the hardcodedexpand_pathplus silent string fallback — check:grep -n 'expand_path("../../../../' plugins/os-sdlc/libreturns nothing.PATHentry tried plus the sibling-checkout candidate — check: a forced-miss test asserts the error message names the tried paths.Worktree::BinaryLocator(or reuse a shared one) and use it infinish_command.rbforos-sdlc-runner, replacing the boolean-onlysystem(...) ||— check:finish_command_sweep_failure_does_not_block_removal_test.rbstill asserts/os-sdlc-runner/in stderr and exit 0, unchanged.config_write_command.rbwithBacklog::Issues::ShellRunner#capture— check:grep -n '\git' plugins/os-backlog/lib` returns nothing.cli_config_write_command_test.rbstill passes (remote nil in its non-git tmpdir either way).Rejected: an
ENVvariable override such asOS_BACKLOG_BIN(forbidden by ADR-0213). Merging the plugins' load paths (these are deliberate process boundaries, not an accident to fix by removing them).Non-goals: changing what happens when the sweep or body-fetch legitimately fails for a reason other than a missing binary. No ADR change — this is a defect fix within a boundary ADR-0213 already governs.
Condition that would make this plan wrong: if any installed-plugin environment does not actually put
~/.claude/plugins/*/binonPATH(contrary to thewhich os-backlogcheck above), then PATH-only resolution would break the installed case; verify withwhich os-backlogandwhich os-sdlc-runnerin a real installed session before relying on it.VERDICT: CREATE
Acceptance criteria
os-backlogis onPATH, whenbody_fetch.rbresolves its binary, then it uses thePATHmatch, not the hardcoded relative path.os-backlogis on neitherPATHnor the sibling checkout location, whenbody_fetch.rbresolves its binary, then it raises an error naming every location it tried, rather than silently falling back to a bare unresolved string.os-sdlc-runneris not found onPATH, whenfinish_command.rbruns its sweep, then it warns "os-sdlc-runner not found on PATH (tried: ...), usage not swept" and does not block worktree removal.os-sdlc-runneris found but exits nonzero, whenfinish_command.rbruns its sweep, then it warns with the resolved path and the exit status, and still does not block worktree removal.config_write_command.rbneeds the origin remote, when it looks it up, then it callsBacklog::Issues::ShellRunner#capture, not a bare backtick, and returns nil under the same conditions as before (call fails or output empty).Owner sketch
Non-goals
Origin
Work started in session 082c3e8b on branch ticket-635 via /os-sdlc:implement.
Resolution
Done: Split into per-plugin children because implementation-open needs one --target per run
Evidence: #702, #703, #704
Follow-ups: #702 body_fetch.rb resolves os-backlog via PATH, fails naming paths tried — VERDICT: CREATE
#703 finish_command.rb sweep names the os-sdlc-runner path it tried — VERDICT: CREATE
#704 config_write_command.rb origin remote via Backlog::Issues::ShellRunner — VERDICT: CREATE