os-backlog: bin/os-backlog resolves CC_OS_HOME once into state_dir; library takes paths as parameters; remove CliEnvContext #718

Closed
opened 2026-09-25 15:04:08 +00:00 by jared · 2 comments
Owner

Blocked-by: #716

Problem

plugins/os-backlog/lib/backlog/cli_env_context.rb:6,21,26 stashes a caller-supplied env: hash in Thread.current[:backlog_cli_env] and falls back to ENV when nothing is threaded; Backlog::CLI.start(env:) sets it around every dispatch.
It is consumed only by ProjectsCommand.new(env:) (cli.rb:33) and ConfigWriteCommand.new(repo_root:, env:) (cli_command_helpers.rb:207), and both use it only to compute ProjectIndex.default_path(env:) (project_index.rb:10, reads env["CC_OS_HOME"] or falls back to env.fetch("HOME", Dir.home), then .cc-os/projects.json).
Wakeup::State.default_path (wakeup/state.rb:9) has the identical CC_OS_HOME/HOME fallback but was not threaded through the CLI context at all, even though #707 intended to thread it — bin/wakeup-poll:48,71,72 construct Backlog::ProjectIndex.new and Backlog::Wakeup::State.new with no path, so they always read live ENV.
This is the shape #716 supersedes: only bin/ should read process globals; the library should take an explicit path.

Approach

Required work:

  1. bin/os-backlog and bin/wakeup-poll each resolve state_dir from ENV["CC_OS_HOME"] with a ~/.cc-os fallback and pass it as state_dir:/path:; no resolver class under lib/.
  2. Change bin/os-backlog to compute state_dir = Backlog::StateDir.from(ENV) and call Backlog::CLI.start(ARGV, state_dir: state_dir); Backlog::CLI.start stores it in Thor config (mirror ADR-0207's config.fetch(:invocation) pattern) instead of Thread.current[:backlog_cli_env]; delete cli_env_context.rb and the extend Backlog::CliEnvContext line in cli.rb.
  3. Change Backlog::ProjectIndex.new(path:): delete self.default_path(env:); callers pass File.join(state_dir, "projects.json") explicitly. Same for Backlog::Wakeup::State.new(path:): delete self.default_path(env:); callers pass File.join(state_dir, "wakeup-state.json").
  4. Change ProjectsCommand.new(index:) and ConfigWriteCommand.new(repo_root:, index:) (both currently build their own ProjectIndex from env:) to receive an already-constructed Backlog::ProjectIndex instead; cli.rb:33 and cli_command_helpers.rb:207 build the index once from config.fetch(:state_dir) and pass it in.
  5. Change bin/wakeup-poll to compute state_dir = Backlog::StateDir.from(ENV) once (line 48 area) and construct Backlog::ProjectIndex.new(path: File.join(state_dir, "projects.json")) and Backlog::Wakeup::State.new(path: File.join(state_dir, "wakeup-state.json")) explicitly, replacing the no-arg .new calls at lines 48 and 71; the summary_path derivation at line 72 (currently File.dirname(Backlog::Wakeup::State.default_path)) becomes File.dirname(state.instance_variable_get...)-free by deriving it from state_dir directly.
  6. Update the 6 test files listed under Retired tests to pass path:/state_dir: explicitly instead of env:; plugins/os-backlog/tests/projects_cli_contract_test.rb keeps spawning the binary with CC_OS_HOME= in its shell string — that test exercises bin/ itself, which remains the one place that reads ENV.

Acceptance criteria

  1. Given the finished change, when a maintainer runs grep -rn "CC_OS_HOME\|Thread.current" plugins/os-backlog/lib, then the command prints no matches.
  2. Given the finished change, when a maintainer runs grep -rln "CC_OS_HOME" plugins/os-backlog/bin, then bin/os-backlog and bin/wakeup-poll are the only matches.
  3. Given bin/wakeup-poll run against a CC_OS_HOME pointing at a temp directory, when it runs, then it reads and writes wakeup-state.json under that directory rather than the real ~/.cc-os.
  4. Given the test suite, when it runs to completion, then it is green and still runs in parallel (no new serialized chdir requirement introduced by this change).

Owner sketch

# plugins/os-backlog/bin/os-backlog  (the only ENV read; duplicated verbatim in bin/wakeup-poll)
state_dir = ENV["CC_OS_HOME"] || File.join(ENV.fetch("HOME", Dir.home), ".cc-os")
Backlog::CLI.start(ARGV, state_dir: state_dir)

# plugins/os-backlog/lib/backlog/cli_context.rb
def start(given_args = ARGV, config = {})
  config.fetch(:state_dir)
  ...
end

# plugins/os-backlog/lib/backlog/project_index.rb
module Backlog
  class ProjectIndex
    def initialize(path:, today: -> { Time.now.strftime("%Y-%m-%d") })
      @json_file = JsonFile.new(path)
      @today = today
    end
  end
end

Retired tests

plugins/os-backlog/tests/cli_env_threading_test.rb
plugins/os-backlog/tests/project_index_test.rb#test_default_path_honors_cc_os_home_override
plugins/os-backlog/tests/project_index_test.rb#test_default_path_falls_back_to_home

Non-goals

plugins/os-backlog/lib/backlog/checkout.rb:30's ENV["USER"] fallback stays untouched (out of scope for this ticket).
plugins/os changes (ticket 1, separate ticket).

A shared StateDir resolver: two identical lines in bin/ are acceptable until #699 decides on shared code.

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)

Blocked-by: #716 ## Problem `plugins/os-backlog/lib/backlog/cli_env_context.rb:6,21,26` stashes a caller-supplied `env:` hash in `Thread.current[:backlog_cli_env]` and falls back to `ENV` when nothing is threaded; `Backlog::CLI.start(env:)` sets it around every dispatch. It is consumed only by `ProjectsCommand.new(env:)` (`cli.rb:33`) and `ConfigWriteCommand.new(repo_root:, env:)` (`cli_command_helpers.rb:207`), and both use it only to compute `ProjectIndex.default_path(env:)` (`project_index.rb:10`, reads `env["CC_OS_HOME"]` or falls back to `env.fetch("HOME", Dir.home)`, then `.cc-os/projects.json`). `Wakeup::State.default_path` (`wakeup/state.rb:9`) has the identical `CC_OS_HOME`/`HOME` fallback but was not threaded through the CLI context at all, even though #707 intended to thread it — `bin/wakeup-poll:48,71,72` construct `Backlog::ProjectIndex.new` and `Backlog::Wakeup::State.new` with no path, so they always read live `ENV`. This is the shape #716 supersedes: only `bin/` should read process globals; the library should take an explicit path. ## Approach Required work: 1. bin/os-backlog and bin/wakeup-poll each resolve state_dir from ENV["CC_OS_HOME"] with a ~/.cc-os fallback and pass it as state_dir:/path:; no resolver class under lib/. 2. Change `bin/os-backlog` to compute `state_dir = Backlog::StateDir.from(ENV)` and call `Backlog::CLI.start(ARGV, state_dir: state_dir)`; `Backlog::CLI.start` stores it in Thor config (mirror ADR-0207's `config.fetch(:invocation)` pattern) instead of `Thread.current[:backlog_cli_env]`; delete `cli_env_context.rb` and the `extend Backlog::CliEnvContext` line in `cli.rb`. 3. Change `Backlog::ProjectIndex.new(path:)`: delete `self.default_path(env:)`; callers pass `File.join(state_dir, "projects.json")` explicitly. Same for `Backlog::Wakeup::State.new(path:)`: delete `self.default_path(env:)`; callers pass `File.join(state_dir, "wakeup-state.json")`. 4. Change `ProjectsCommand.new(index:)` and `ConfigWriteCommand.new(repo_root:, index:)` (both currently build their own `ProjectIndex` from `env:`) to receive an already-constructed `Backlog::ProjectIndex` instead; `cli.rb:33` and `cli_command_helpers.rb:207` build the index once from `config.fetch(:state_dir)` and pass it in. 5. Change `bin/wakeup-poll` to compute `state_dir = Backlog::StateDir.from(ENV)` once (line 48 area) and construct `Backlog::ProjectIndex.new(path: File.join(state_dir, "projects.json"))` and `Backlog::Wakeup::State.new(path: File.join(state_dir, "wakeup-state.json"))` explicitly, replacing the no-arg `.new` calls at lines 48 and 71; the `summary_path` derivation at line 72 (currently `File.dirname(Backlog::Wakeup::State.default_path)`) becomes `File.dirname(state.instance_variable_get...)`-free by deriving it from `state_dir` directly. 6. Update the 6 test files listed under Retired tests to pass `path:`/`state_dir:` explicitly instead of `env:`; `plugins/os-backlog/tests/projects_cli_contract_test.rb` keeps spawning the binary with `CC_OS_HOME=` in its shell string — that test exercises `bin/` itself, which remains the one place that reads `ENV`. ## Acceptance criteria 1. Given the finished change, when a maintainer runs `grep -rn "CC_OS_HOME\|Thread.current" plugins/os-backlog/lib`, then the command prints no matches. 2. Given the finished change, when a maintainer runs `grep -rln "CC_OS_HOME" plugins/os-backlog/bin`, then `bin/os-backlog` and `bin/wakeup-poll` are the only matches. 3. Given `bin/wakeup-poll` run against a `CC_OS_HOME` pointing at a temp directory, when it runs, then it reads and writes `wakeup-state.json` under that directory rather than the real `~/.cc-os`. 4. Given the test suite, when it runs to completion, then it is green and still runs in parallel (no new serialized `chdir` requirement introduced by this change). ## Owner sketch # plugins/os-backlog/bin/os-backlog (the only ENV read; duplicated verbatim in bin/wakeup-poll) state_dir = ENV["CC_OS_HOME"] || File.join(ENV.fetch("HOME", Dir.home), ".cc-os") Backlog::CLI.start(ARGV, state_dir: state_dir) # plugins/os-backlog/lib/backlog/cli_context.rb def start(given_args = ARGV, config = {}) config.fetch(:state_dir) ... end # plugins/os-backlog/lib/backlog/project_index.rb module Backlog class ProjectIndex def initialize(path:, today: -> { Time.now.strftime("%Y-%m-%d") }) @json_file = JsonFile.new(path) @today = today end end end ## Retired tests plugins/os-backlog/tests/cli_env_threading_test.rb plugins/os-backlog/tests/project_index_test.rb#test_default_path_honors_cc_os_home_override plugins/os-backlog/tests/project_index_test.rb#test_default_path_falls_back_to_home ## Non-goals `plugins/os-backlog/lib/backlog/checkout.rb:30`'s `ENV["USER"]` fallback stays untouched (out of scope for this ticket). `plugins/os` changes (ticket 1, separate ticket). A shared StateDir resolver: two identical lines in bin/ are acceptable until #699 decides on shared code. ## 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)
Author
Owner

Work started on branch ticket-718 via /os-sdlc:implement (session 89876543).

Work started on branch ticket-718 via /os-sdlc:implement (session 89876543).
Author
Owner

Resolution

Done: bin/os-backlog and bin/wakeup-poll resolve CC_OS_HOME once; ProjectIndex/Wakeup::State take path:, ProjectsCommand/ConfigWriteCommand take index:; CliEnvContext, thread-local and default_path(env:) deleted; CLI.start requires config state_dir

Evidence: branch ticket-718 commit 289a1654; os-backlog suite 248 runs 0 failures; grep CC_OS_HOME|Thread.current in lib empty; pipeline impl 38 ran to contract-auditor which errored on the ticket's own StateDir-in-lib sketch, sketch corrected and remaining shim hand-finished

Follow-ups: checkout.rb:30 ENV[USER] read stays (non-goal, ADR-0213 shape); shared StateDir resolver deferred to #699

## Resolution **Done:** bin/os-backlog and bin/wakeup-poll resolve CC_OS_HOME once; ProjectIndex/Wakeup::State take path:, ProjectsCommand/ConfigWriteCommand take index:; CliEnvContext, thread-local and default_path(env:) deleted; CLI.start requires config state_dir **Evidence:** branch ticket-718 commit 289a1654; os-backlog suite 248 runs 0 failures; grep CC_OS_HOME|Thread.current in lib empty; pipeline impl 38 ran to contract-auditor which errored on the ticket's own StateDir-in-lib sketch, sketch corrected and remaining shim hand-finished **Follow-ups:** checkout.rb:30 ENV[USER] read stays (non-goal, ADR-0213 shape); shared StateDir resolver deferred to #699
jared closed this issue 2026-09-25 15:26:00 +00:00
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#718
No description provided.