os: FinishCommand takes git: and after_merge: collaborators; Invocation from bin/worktree; remove env: and thread-local #717
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#717
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?
Blocked-by: #716
Problem
plugins/os/lib/worktree/cli.rb:7-8,31-32stashes a caller-suppliedenv:hash inThread.current[:worktree_cli_env]and falls back toENVwhen nothing is threaded.FinishCommand#initialize(finish_command.rb:14) defaultsenv: ENVand threads@envto every git spawn (Open3/systemcalls at lines 51, 60, 80, 84, 94, 95, 105, 109, 114, 119, 121, 125), toConflictScan.new(..., env:),Removal.new(..., env:),RepoRoot.new(env:), and toUsageSweep.new(..., env:).UsageSweep#resolve_runner(usage_sweep.rb:22-23) callsBinaryLocator.resolve('os-sdlc-runner', env: @env), which readsenv.fetch('PATH', '')(binary_locator.rb:8) to find the siblingos-sdlc-runnerbinary by scanningPATH.This is the shape #716 supersedes: library code should take explicit collaborators, not an env hash threaded through a thread-local.
base_branch(finish_command.rb:59-61) also runsgit symbolic-ref --short HEADon every call site that needs it (up to 3 times per finish) because it is not memoized; this ticket'sgit:collaborator change is the dependency ticket 4 needs before it can assert "called once" against a fake.Approach
Required work:
Worktree::Gittoplugins/os/lib/worktree/git.rb: the only class inplugins/os/libthat spawnsgit, modeled on os-sdlc'sAdapter::Git::Repo(plugins/os-sdlc/lib/os_sdlc/adapter/git/repo.rb); methodsread(*args),succeeds?(*args),run(*args), each doingOpen3.capture3('git', '-C', @root, *args)(orOpen3.capture2ewhere the caller needs combined output, matchingConflictScan#report's currentcapture2emerge-tree call), noenv:parameter.Worktree::InvocationasData.define(:cwd, :stdout, :stderr), built inbin/worktreefromDir.pwd,$stdout,$stderr, and passed intoWorktree::CLI.start(ARGV, invocation: Worktree::Invocation.new(...)); commands fetch it viaconfig.fetch(:invocation)(mirror os-sdlc's ADR-0207 Invocation wiring), replacing theThread.current[:worktree_cli_env]mechanism incli.rb.Worktree::AfterMerge(plugins/os/lib/worktree/after_merge.rb): owns the three post-merge effects now spread acrossfinish_command.rb:101-126. ConstructorAfterMerge.new(root:, git:, sweep:, stop_server:)wheresweepis aUsageSweepandstop_serveris a callable taking the worktree path (production default:->(path) { system('rubocop', '--stop-server', chdir: path) }). Single public methodcall(worktree_path:, pre_merge_head:, implementation_id:): runsbin/refresh-pluginsunderrootwhengit.read('diff', '--name-only', pre_merge_head, 'HEAD')lists aplugins/path, thenstop_server.call(worktree_path), thensweep.call(worktree_path, implementation_id)whenimplementation_idis non-nil.FinishCommandcalls only@after_merge.call(...); FinishCommand tests pass a recording double that records those three keyword arguments; AfterMerge's own tests pass aFakeGit, a recordingsweep, and a recordingstop_serverlambda, so no realrubocopruns anywhere in the suite.UsageSweep.new(runner_path:): production resolves the siblingos-sdlc-runnerby fixed relative path from the running binary's location (copy os-sdlc'sAdapter::BinaryLocator.default_siblingidea,plugins/os-sdlc/lib/os_sdlc/adapter/binary_locator.rb), not by scanningPATH; deleteWorktree::BinaryLocator(plugins/os/lib/worktree/binary_locator.rb) since nothing else inplugins/os/libuses it.FinishCommand.new(branch, root:, git:, after_merge:, implementation_id: nil); delete theenv:keyword;merge_into_base,worktree_path_for_branch,base_branch,ancestor?,verify_ancestry!,conflicted_filesroute throughgit:instead ofOpen3/systemcalls against@env.ConflictScan.new(root, base, branch, git:),Removal.new(root, path, git:),RepoRoot.new(git: Worktree::Git.new)(or equivalent) to takegit:instead ofenv:; delete theenv:keyword from each.StartCommand.new(name, root:, repo_root:)to sourcerootfrom the passedrepo_root/git:rather than a freshRepoRoot.new.pathdefault that readsENVtransitively.env:keyword and everyENV/Thread.currentreference fromplugins/os/lib(verify with the acceptance-criteria grep below).base_branchwith@base_branch ||=insideFinishCommand.FakeGittoplugins/os/tests/support(mirror os-sdlc'sFakeGit,plugins/os-sdlc/tests/test_helper.rb:209:run/read/succeeds?plus recorded calls) and a recordingafter_mergedouble; convert the finish tests listed under Retired tests to useFakeGitand the double instead of PATH-planted fake binaries; keepfinish_command_conflicts_test.rb,finish_command_clean_path_test.rb, andfinish_command_removal_failure_test.rbas real-git integration tests throughRepoSeeder(clean merge, conflict, removal-failure paths) so the suite still exercises real git at least once per path.Acceptance criteria
grep -rn "ENV\|Thread.current" plugins/os/lib, then the command prints no matches.grep -rln "'git'" plugins/os/lib, then onlyplugins/os/lib/worktree/git.rbmatches (no other file spawnsgitdirectly).rubocopprocesses (the rubocop-server test asserts against theafter_merge/AfterMergedouble, not a PATH-plantedrubocopscript).Owner sketch
Retired tests
plugins/os/tests/worktree/cli_env_threading_test.rb
plugins/os/tests/worktree/finish_command_stops_rubocop_server_test.rb
plugins/os/tests/worktree/finish_command_sweep_binary_resolution_test.rb
Non-goals
Extracting
Worktree::Gitinto a shared gem consumed by bothosandos-sdlc(#699 tracks the cross-plugin duplication survey; ADR-0025 item 4 forbids source coupling for now).plugins/os-backlogchanges (ticket 2, separate ticket).Folding ticket 4's
base_branchmemoization observability check into this ticket's acceptance criteria (ticket 4 is a small follow-on that asserts call count against theFakeGitthis ticket introduces).Origin
Session: 89876543-c418-46b0-b994-dd6dc6887802
Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/89876543-c418-46b0-b994-dd6dc6887802.jsonl
Trigger: review of #634/#707/#708 (commits
8ef12ae8,e2817ffe, ADR-0218)Improvised this session: none
Chain: env:/thread-local in lib ← ADR-0218 item 1 chose spawn-env-hash + Thread.current ← DESIGN (docs/adr/ADR-0218…, contradicts ADR-0213 and ADR-0207)
Root candidate: #716 (the superseding ADR)
Work started on branch ticket-717 via /os-sdlc:implement (session 89876543). #718 merged first (
289a1654).Resolution
Done: Worktree::Git is the sole git spawner; AfterMerge(root:, git:, sweep:, stop_server:) owns refresh-plugins, rubocop stop and usage sweep; FinishCommand/ConflictScan/Removal/RepoRoot/StartCommand take git:; Invocation built in bin/worktree and fetched with config.fetch(:invocation); env:, Thread.current and BinaryLocator deleted; base_branch memoized; FakeGit and RecordingAfterMerge doubles added
Evidence: branch ticket-717 commit
aa3475d2; os suite 26 runs 0 failures; grep ENV|Thread.current|Dir.pwd in plugins/os/lib empty; zero real rubocop spawns verified with a PATH-planted logger; pipeline impl 43 ran through programmer green then looped red/review 5 times because criteria were already satisfied (#724), hand-finishedFollow-ups: #719 (base_branch memoization) already satisfied by this change, close by verification; #724 map cannot converge after verifier fail when production is already green; #725 shared scratchpad payload file name; Worktree::Git duplication noted on #699