Apply ExitOutsideEntryPoint to os, os-backlog, and os-me #1411

Closed
opened 2026-10-26 21:26:10 +00:00 by jared · 5 comments
Owner

Problem

Ticket #1402 added the cop Sdlc/Structural/ExitOutsideEntryPoint (plugins/os-sdlc/lib/os_sdlc/cops/structural/exit_outside_entry_point.rb).
The cop reports exit, exit!, and abort (bare, Kernel., or Process.) outside an entry point.
An entry point is a file with a #! first line or a file in a bin directory.
An exit! lexically inside a fork block is allowed.
Only plugins/os-sdlc/.rubocop.yml enables the cop.
Eleven non-entry exit calls remain in the plugins os, os-backlog, and os-me.
An exit call deep in a library hides the process boundary: a caller cannot reuse the code without the process ending.

Observed

The call sites on main 367156a8 (2026-10-26), verified by grep:

plugins/os-backlog/lib/backlog/cli_exit.rb:7:      exit 1
plugins/os-backlog/lib/backlog/cli_startup.rb:11:      exit 1 if given_args.nil? || given_args.empty?
plugins/os-backlog/lib/backlog/decision_sweep/command.rb:21:        exit result.exit_code
plugins/os-backlog/lib/backlog/cli/project_commands.rb:52:        exit result.exit_code
plugins/os-backlog/lib/backlog/cli/issue_commands.rb:103:          exit 1
plugins/os/lib/handoff/cli.rb:41:      exit 1
plugins/os/lib/worktree/cli.rb:24:      exit 1
plugins/os/lib/worktree/cli.rb:33:      exit(outcome.code) unless outcome.code.zero?
plugins/os-me/lib/os_me/cli.rb:24:    def held_out(session_id) = exit(Split.new.held_out?(session_id) ? 0 : 1)
plugins/os-me/lib/os_me/cli.rb:27:    def sample(key) = exit(RandomSample.new.pick?(key) ? 0 : 1)
plugins/os-me/lib/os_me/cli.rb:39:    def blind(session_id, args = '') = exit(build_blind_marker.draw(session_id, args, $stdout) ? 0 : 1)

The earlier list named plugins/os/lib/worktree/cli.rb:31; the second call is now at line 33.
plugins/os-backlog/.rubocop.yml does not load the os-sdlc cops.
The repo-root .rubocop.yml sets only AllCops and Style/Documentation, so it does not load the Sdlc cops.
plugins/os and plugins/os-me have no plugin .rubocop.yml.

Reproduce

n/a: design and lint-config ticket; the offenses appear only after the cop is loaded.

Approach

Several sites are Thor CLI classes.
Thor start returns no status, so each CLI must return an Integer status from a run method, and the bin stub calls exit.
The design question per plugin is how each Thor command reports its status to run.
Each plugin may choose its own answer; the observable exit status must not change.

Required work:

  1. Load the Sdlc cops loader and enable Sdlc/Structural/ExitOutsideEntryPoint in plugins/os-backlog/.rubocop.yml — check: present Sdlc/Structural/ExitOutsideEntryPoint in plugins/os-backlog/.rubocop.yml
  2. Add plugins/os/.rubocop.yml that loads the Sdlc cops loader and enables the cop — check: present Sdlc/Structural/ExitOutsideEntryPoint in plugins/os/.rubocop.yml
  3. Add plugins/os-me/.rubocop.yml that loads the Sdlc cops loader and enables the cop — check: present Sdlc/Structural/ExitOutsideEntryPoint in plugins/os-me/.rubocop.yml
  4. Remove the five exit calls in plugins/os-backlog/lib; the bin stub calls exit Backlog::CLI.run(ARGV, ...) — check: absent exit in plugins/os-backlog/lib/backlog/cli_exit.rb
  5. Remove the three exit calls in plugins/os/lib/handoff/cli.rb and plugins/os/lib/worktree/cli.rb — check: absent exit(outcome.code) in plugins/os/lib/worktree/cli.rb
  6. Remove the three exit calls in plugins/os-me/lib/os_me/cli.rb — check: absent exit( in plugins/os-me/lib/os_me/cli.rb
  7. For a site that must keep an exit, justify it in the ticket comment and exempt it in the lint config; a file-list exclusion is not allowed.

Rejected: leave the cop in os-sdlc only — the other plugins keep the hidden process boundary.

Acceptance criteria

  1. Given plugins/os-backlog/.rubocop.yml, when rubocop runs with it, then it reports no Sdlc/Structural/ExitOutsideEntryPoint offense.
  2. Given plugins/os/.rubocop.yml, when rubocop runs with it, then it reports no Sdlc/Structural/ExitOutsideEntryPoint offense.
  3. Given plugins/os-me/.rubocop.yml, when rubocop runs with it, then it reports no Sdlc/Structural/ExitOutsideEntryPoint offense.
  4. Given os-backlog with no arguments, when it runs, then it exits 1 as before.
  5. Given os-backlog and a failing issue-create that exited 1 before, when it runs, then it still exits 1.
  6. Given plugins/os/bin/worktree and a command whose outcome code is nonzero, when it runs, then the process exits with that code.
  7. Given os-me held-out for a held-out session, when it runs, then it exits 0; for any other session it exits 1.

Owner sketch

#!/usr/bin/env ruby
require_relative "../lib/backlog"
exit Backlog::CLI.run(ARGV, state_dir: state_dir, runner: Backlog::ShellRunner.new)

module Backlog
  class CLI < Thor
    def self.run(argv, **options)
      start(argv, **options)
      0
    rescue CliExit::Failure => e
      warn e.message
      e.status
    end
  end
end

Non-goals

Changes to the cop itself; plugins other than os, os-backlog, and os-me.

Origin

  • Trigger: #1402 body lists this work as a Non-goal "follow-up ticket per plugin".
  • Improvised this session: none
  • Chain: eleven exit calls in os, os-backlog, os-me libraries ← the cop loads only in plugins/os-sdlc/.rubocop.yml ← DESIGN (#1402, commit 367156a8)
  • Root candidate: none found; searched ExitOutsideEntryPoint, exit, abort, cli_exit, worktree/cli, os_me/cli, os-backlog .rubocop.yml (only #1402 matched, the parent)
  • Where: plugins/os-backlog/lib/backlog/cli_exit.rb, plugins/os/lib/worktree/cli.rb, plugins/os-me/lib/os_me/cli.rb
  • Session: 006abca1-0754-41ab-a8f7-9209dfff5f62
  • Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/006abca1-0754-41ab-a8f7-9209dfff5f62.jsonl

VERDICT: CREATE

## Problem Ticket #1402 added the cop `Sdlc/Structural/ExitOutsideEntryPoint` (`plugins/os-sdlc/lib/os_sdlc/cops/structural/exit_outside_entry_point.rb`). The cop reports `exit`, `exit!`, and `abort` (bare, `Kernel.`, or `Process.`) outside an entry point. An entry point is a file with a `#!` first line or a file in a `bin` directory. An `exit!` lexically inside a `fork` block is allowed. Only `plugins/os-sdlc/.rubocop.yml` enables the cop. Eleven non-entry exit calls remain in the plugins `os`, `os-backlog`, and `os-me`. An exit call deep in a library hides the process boundary: a caller cannot reuse the code without the process ending. ## Observed The call sites on main 367156a8 (2026-10-26), verified by grep: ``` plugins/os-backlog/lib/backlog/cli_exit.rb:7: exit 1 plugins/os-backlog/lib/backlog/cli_startup.rb:11: exit 1 if given_args.nil? || given_args.empty? plugins/os-backlog/lib/backlog/decision_sweep/command.rb:21: exit result.exit_code plugins/os-backlog/lib/backlog/cli/project_commands.rb:52: exit result.exit_code plugins/os-backlog/lib/backlog/cli/issue_commands.rb:103: exit 1 plugins/os/lib/handoff/cli.rb:41: exit 1 plugins/os/lib/worktree/cli.rb:24: exit 1 plugins/os/lib/worktree/cli.rb:33: exit(outcome.code) unless outcome.code.zero? plugins/os-me/lib/os_me/cli.rb:24: def held_out(session_id) = exit(Split.new.held_out?(session_id) ? 0 : 1) plugins/os-me/lib/os_me/cli.rb:27: def sample(key) = exit(RandomSample.new.pick?(key) ? 0 : 1) plugins/os-me/lib/os_me/cli.rb:39: def blind(session_id, args = '') = exit(build_blind_marker.draw(session_id, args, $stdout) ? 0 : 1) ``` The earlier list named `plugins/os/lib/worktree/cli.rb:31`; the second call is now at line 33. `plugins/os-backlog/.rubocop.yml` does not load the os-sdlc cops. The repo-root `.rubocop.yml` sets only `AllCops` and `Style/Documentation`, so it does not load the Sdlc cops. `plugins/os` and `plugins/os-me` have no plugin `.rubocop.yml`. ## Reproduce n/a: design and lint-config ticket; the offenses appear only after the cop is loaded. ## Approach Several sites are Thor CLI classes. Thor `start` returns no status, so each CLI must return an Integer status from a `run` method, and the bin stub calls `exit`. The design question per plugin is how each Thor command reports its status to `run`. Each plugin may choose its own answer; the observable exit status must not change. Required work: 1. Load the Sdlc cops loader and enable `Sdlc/Structural/ExitOutsideEntryPoint` in `plugins/os-backlog/.rubocop.yml` — check: present `Sdlc/Structural/ExitOutsideEntryPoint` in `plugins/os-backlog/.rubocop.yml` 2. Add `plugins/os/.rubocop.yml` that loads the Sdlc cops loader and enables the cop — check: present `Sdlc/Structural/ExitOutsideEntryPoint` in `plugins/os/.rubocop.yml` 3. Add `plugins/os-me/.rubocop.yml` that loads the Sdlc cops loader and enables the cop — check: present `Sdlc/Structural/ExitOutsideEntryPoint` in `plugins/os-me/.rubocop.yml` 4. Remove the five exit calls in `plugins/os-backlog/lib`; the bin stub calls `exit Backlog::CLI.run(ARGV, ...)` — check: absent `exit` in `plugins/os-backlog/lib/backlog/cli_exit.rb` 5. Remove the three exit calls in `plugins/os/lib/handoff/cli.rb` and `plugins/os/lib/worktree/cli.rb` — check: absent `exit(outcome.code)` in `plugins/os/lib/worktree/cli.rb` 6. Remove the three exit calls in `plugins/os-me/lib/os_me/cli.rb` — check: absent `exit(` in `plugins/os-me/lib/os_me/cli.rb` 7. For a site that must keep an exit, justify it in the ticket comment and exempt it in the lint config; a file-list exclusion is not allowed. Rejected: leave the cop in os-sdlc only — the other plugins keep the hidden process boundary. ## Acceptance criteria 1. Given `plugins/os-backlog/.rubocop.yml`, when rubocop runs with it, then it reports no `Sdlc/Structural/ExitOutsideEntryPoint` offense. 2. Given `plugins/os/.rubocop.yml`, when rubocop runs with it, then it reports no `Sdlc/Structural/ExitOutsideEntryPoint` offense. 3. Given `plugins/os-me/.rubocop.yml`, when rubocop runs with it, then it reports no `Sdlc/Structural/ExitOutsideEntryPoint` offense. 4. Given `os-backlog` with no arguments, when it runs, then it exits 1 as before. 5. Given `os-backlog` and a failing `issue-create` that exited 1 before, when it runs, then it still exits 1. 6. Given `plugins/os/bin/worktree` and a command whose outcome code is nonzero, when it runs, then the process exits with that code. 7. Given `os-me held-out` for a held-out session, when it runs, then it exits 0; for any other session it exits 1. ## Owner sketch ```ruby #!/usr/bin/env ruby require_relative "../lib/backlog" exit Backlog::CLI.run(ARGV, state_dir: state_dir, runner: Backlog::ShellRunner.new) module Backlog class CLI < Thor def self.run(argv, **options) start(argv, **options) 0 rescue CliExit::Failure => e warn e.message e.status end end end ``` ## Non-goals Changes to the cop itself; plugins other than `os`, `os-backlog`, and `os-me`. ## Origin - Trigger: #1402 body lists this work as a Non-goal "follow-up ticket per plugin". - Improvised this session: none - Chain: eleven exit calls in `os`, `os-backlog`, `os-me` libraries ← the cop loads only in `plugins/os-sdlc/.rubocop.yml` ← DESIGN (#1402, commit 367156a8) - Root candidate: none found; searched ExitOutsideEntryPoint, exit, abort, cli_exit, worktree/cli, os_me/cli, os-backlog .rubocop.yml (only #1402 matched, the parent) - Where: plugins/os-backlog/lib/backlog/cli_exit.rb, plugins/os/lib/worktree/cli.rb, plugins/os-me/lib/os_me/cli.rb - Session: 006abca1-0754-41ab-a8f7-9209dfff5f62 - Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/006abca1-0754-41ab-a8f7-9209dfff5f62.jsonl VERDICT: CREATE
Author
Owner

Work started (AFK unit, fast-batch route): the os plugin part. Three ExitOutsideEntryPoint hits: plugins/os/lib/handoff/cli.rb:41, plugins/os/lib/worktree/cli.rb:24,33. os-me (2e871a2b) and os-backlog (1cee5a2f) parts are already on main.

Work started (AFK unit, fast-batch route): the os plugin part. Three ExitOutsideEntryPoint hits: plugins/os/lib/handoff/cli.rb:41, plugins/os/lib/worktree/cli.rb:24,33. os-me (2e871a2b) and os-backlog (1cee5a2f) parts are already on main.
Author
Owner

Not filed (skeptic DROP, depth-1): six ExitOutsideEntryPoint hits stay outside os/os-me/os-backlog (plugins/cc-architect/scripts/lib/validate_marketplaces.rb:127,139,141,143; docs/system-design-format/spike/extract.rb:4; docs/triage/2026-10-10-backfill-ticket-categories.rb:31); no plugin target lints them today.

Not filed (skeptic DROP, depth-1): six ExitOutsideEntryPoint hits stay outside os/os-me/os-backlog (plugins/cc-architect/scripts/lib/validate_marketplaces.rb:127,139,141,143; docs/system-design-format/spike/extract.rb:4; docs/triage/2026-10-10-backfill-ticket-categories.rb:31); no plugin target lints them today.
Author
Owner

Resolution

Done: os part merged as fast-batch-1411 (cf9f6c04, merge a3097e8e): Handoff::CLI and Worktree::CLI raise Handoff::ExitStatus / Worktree::ExitStatus in place of exit; bin/handoff and bin/worktree rescue it and exit e.status. os-me part 2e871a2b and os-backlog part 1cee5a2f were already on main. Design model regenerated with 'system-model build plugins/os' (dcb1a591).

Evidence: lint-worklist --target os: 0 ExitOutsideEntryPoint (was 3). ruby plugins/os/tests/all.rb: 65 runs, 180 assertions, 0 failures. After refresh-plugins: 'worktree help' and 'handoff help' answer; 'handoff complete /nonexistent' prints the error and exits 1; 'worktree finish no-such-branch' exits 3 as on main. Required-work items 1-3 (.rubocop.yml files naming the cop) not added: lint-worklist already applies the Sdlc cops to the os, os-me and os-backlog targets; adding a plugins/os/.rubocop.yml would change rubocop config resolution for the os target and needs a ruling.

Follow-ups: Ruling for Jared: keep or drop required-work items 1-3 (per-plugin .rubocop.yml naming the cop). Predicted row R1 (keep one ExitStatus per namespace, 74) in docs/crc/os/fast-batch-1411/rulings.tsv.

## Resolution **Done:** os part merged as fast-batch-1411 (cf9f6c04, merge a3097e8e): Handoff::CLI and Worktree::CLI raise Handoff::ExitStatus / Worktree::ExitStatus in place of exit; bin/handoff and bin/worktree rescue it and exit e.status. os-me part 2e871a2b and os-backlog part 1cee5a2f were already on main. Design model regenerated with 'system-model build plugins/os' (dcb1a591). **Evidence:** lint-worklist --target os: 0 ExitOutsideEntryPoint (was 3). ruby plugins/os/tests/all.rb: 65 runs, 180 assertions, 0 failures. After refresh-plugins: 'worktree help' and 'handoff help' answer; 'handoff complete /nonexistent' prints the error and exits 1; 'worktree finish no-such-branch' exits 3 as on main. Required-work items 1-3 (.rubocop.yml files naming the cop) not added: lint-worklist already applies the Sdlc cops to the os, os-me and os-backlog targets; adding a plugins/os/.rubocop.yml would change rubocop config resolution for the os target and needs a ruling. **Follow-ups:** Ruling for Jared: keep or drop required-work items 1-3 (per-plugin .rubocop.yml naming the cop). Predicted row R1 (keep one ExitStatus per namespace, 74) in docs/crc/os/fast-batch-1411/rulings.tsv.
jared closed this issue 2026-10-27 21:31:21 +00:00
Author
Owner

Note from the AFK orchestrator: #1411 was closed with required-work items 1-3 (a .rubocop.yml per plugin that names the cop) not done, because a new plugins/os/.rubocop.yml changes which rubocop config the os target uses and needs Jared's ruling. Reopen if those items still matter.

Note from the AFK orchestrator: #1411 was closed with required-work items 1-3 (a `.rubocop.yml` per plugin that names the cop) not done, because a new `plugins/os/.rubocop.yml` changes which rubocop config the os target uses and needs Jared's ruling. Reopen if those items still matter.
Author
Owner

Ruling 2026-10-28 (Jared): drop items 1-3. lint/worklist_rubocop_config.rb:10 makes plugins/os-sdlc/.rubocop.yml the first config link for every target, so Sdlc/Structural/ExitOutsideEntryPoint already runs on os, os-me and os-backlog (lint-worklist --target os: 0 offenses). Per-plugin files would change only a bare rubocop run inside a plugin, would need project.yaml lint_config entries to affect the worklist, and would then drop the root AllCops Exclude list. Target scoping is unchanged either way.

Ruling 2026-10-28 (Jared): drop items 1-3. lint/worklist_rubocop_config.rb:10 makes plugins/os-sdlc/.rubocop.yml the first config link for every target, so Sdlc/Structural/ExitOutsideEntryPoint already runs on os, os-me and os-backlog (lint-worklist --target os: 0 offenses). Per-plugin files would change only a bare rubocop run inside a plugin, would need project.yaml lint_config entries to affect the worklist, and would then drop the root AllCops Exclude list. Target scoping is unchanged either way.
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#1411
No description provided.