os-sdlc project-config --help treats --help as a project name and writes .sdlc/project.yaml #860

Closed
opened 2026-09-30 20:59:27 +00:00 by jared · 7 comments
Owner

Problem

Evidence: running it inside ~/dev/cc-os/plugins/os-sdlc wrote that file (reverted). Reproduced again on main b80613ec.

os-sdlc project-config --help (and -h) treats --help as the project name and writes .sdlc/project.yaml with name: "--help".

The cause is the design, not the call order. bin/os-sdlc is a Thor CLI (ADR-0123), but project-config declares no Thor options. It takes a *args splat, so Thor passes every token through unchecked. Then three hand parsers share the argv: ArgvOption.take! removes --target, call sniffs @args.first == "--show", validated_name shifts the first token, and a private OptionParser (OptionParserBuilder) parses the rest. No layer rejects an unknown switch, so --help becomes a name.

plugins/os-sdlc/lib/os_sdlc/controller/project/onboarding.rb:37  def project_config(*args)
plugins/os-sdlc/lib/os_sdlc/controller/project/onboarding.rb:38  target = ArgvOption.take!(args, "--target NAME")
plugins/os-sdlc/lib/os_sdlc/command/project/config_command.rb:22  return show if @args.first == "--show"
plugins/os-sdlc/lib/os_sdlc/command/project/config_command.rb:30  name = validated_name
plugins/os-sdlc/lib/os_sdlc/command/project/config_command.rb:76  @args.shift
plugins/os-sdlc/lib/os_sdlc/parser/project/option_parser_builder.rb:15  parser.parse!(@args)

A second symptom of the same design: os-sdlc help project-config prints only os-sdlc project-config NAME. It lists none of the six flags, because Thor does not know them.

ADR-0123 chose Thor for bin/os-sdlc and rejected a hand-rolled OptionParser because it re-implements flag parsing and help. ADR-0232 keeps bin/os-sdlc on Thor because humans read its help.

Approach

Declare the command's interface in Thor and delete the hand parsers. This is the shape of the sibling Thor controllers Evidence::Collection and Worktree::CLI: check_unknown_options! plus one method_option per flag.

  • Onboarding calls check_unknown_options!. An unknown switch such as --bogus is refused before any command runs. The refusal fails through ProcessIO#guarded with exit 3 and writes no file (ADR-0246: every failure on this path exits 3).
  • project_config declares --help (alias -h) as a boolean option. When it is set, the command prints Thor's help for project-config (the same text as os-sdlc help project-config) and writes no file. ADR-0232 kept Thor for this CLI because humans read its --help; a refusal would break that.
  • project_config(name = nil) declares --path, --test-path, --test-command, --lint-config, --test-framework, --target (strings) and --show (boolean).
  • ConfigCommand receives name:, target:, show: and the option hash. It no longer reads argv. A missing name keeps the existing LoudFailure message.
  • Delete OptionParserBuilder (one caller). ArgvOption stays for the non-Thor scripts (Boot::Inputs, LintWorklist); only the Onboarding call goes.
  • os-sdlc help project-config and project-config --help print the same help. Thor lists every declared option.

Required work:

  1. Onboarding refuses unknown switches. check: present check_unknown_options! in plugins/os-sdlc/lib/os_sdlc/controller/project/onboarding.rb
  2. project_config declares its flags as Thor options. check: present method_option :test_command in plugins/os-sdlc/lib/os_sdlc/controller/project/onboarding.rb
  3. project_config takes a declared name, not a splat. check: absent def project_config(*args) in plugins/os-sdlc/lib/os_sdlc/controller/project/onboarding.rb
  4. --target comes from Thor options, not ArgvOption. check: absent ArgvOption in plugins/os-sdlc/lib/os_sdlc/controller/project/onboarding.rb
  5. ConfigCommand no longer parses argv. check: absent @args in plugins/os-sdlc/lib/os_sdlc/command/project/config_command.rb
  6. Delete OptionParserBuilder. check: absent OptionParserBuilder in plugins/os-sdlc/lib/**/*.rb
  7. Add tests for --help, -h, and help project-config. Update tests/model/project_test_path_default_test.rb:50, which builds ConfigCommand with an argv array.

Sketch:

class Onboarding < Thor
  check_unknown_options!

  desc "project-config NAME", "create or update .sdlc/project.yaml"
  method_option :show, type: :boolean, default: false, desc: "print the current configuration"
  method_option :target, type: :string, desc: "named target in a targets-shaped project.yaml"
  method_option :path, type: :string
  method_option :test_command, type: :string
  # ... test_path, lint_config, test_framework
  def project_config(name = nil)
    OsSdlc::Project::ConfigCommand.new(name: name, options: options, invocation: @invocation).call
  end
end

Acceptance criteria

  1. Given a directory with no .sdlc/project.yaml, when os-sdlc project-config --help runs, then it exits 0, its output names --path and --test-command, and no .sdlc/project.yaml exists afterward.
  2. Given the same directory, when os-sdlc project-config -h runs, then the result is the same as criterion 1.
    2a. Given the same directory, when os-sdlc project-config --bogus runs, then it exits 3, stderr names --bogus as an unknown switch, and no .sdlc/project.yaml exists afterward.
  3. When os-sdlc help project-config runs, then it exits 0 and its output names --path, --test-path, --test-command, --lint-config, --test-framework, --target and --show.
  4. Given os-sdlc project-config widgets --path app, when it runs, then it writes .sdlc/project.yaml for widgets as it does today.
  5. Given os-sdlc project-config with no name, when it runs, then it fails with the existing missing-name message and exit 3.
  6. Given a targets-shaped project, when project-config --show --target a and project-config --show --target=a run, then both select target a as --target=a does today.

Owner sketch

class OsSdlc::Project::Onboarding < Thor
  def project_config(name = nil) end
end

class OsSdlc::Project::ConfigCommand
  def initialize(name:, options:, invocation:) end
  def call; end
end

Changes since filing (verified 2026-10-02 on main b80613ec)

  • The bug reproduces on main b80613ec.
  • #829 (858349ec) and #830 (e6961519) moved the files. The paths and lines in this ticket are still valid.
  • 2026-10-02: the approach changed from a call-order swap in ConfigCommand#upsert to Thor-declared options. A swap leaves three hand parsers next to Thor and keeps unknown switches silent (ADR-0123).
  • Thor 1.5.0 does not intercept project-config --help after the subcommand; with check_unknown_options! it raises Unknown switches "--help". Verified in a scratch Thor class on 2026-10-02.

Blast radius

  • Production: Project::Onboarding#project_config, Project::ConfigCommand, Project::OptionParserBuilder (deleted, one caller). check_unknown_options! is class-level, so lint-init also refuses unknown switches; InitCommand takes only a positional directory.
  • Tests that pin the current flow: tests/controller/project/cli_project_config_test.rb, tests/controller/project/cli_project_config_target_path_test.rb, tests/model/project_test_path_default_test.rb:50, tests/command/project/config_command_missing_message_test.rb:12, tests/hardening_verdict_lint_project_test.rb:31, tests/adapter/gate/evaluator_project_shell_test.rb:40. tests/controller/argv_option_test.rb is unaffected.
  • Callers: skills/setup-project/SKILL.md uses project-config <name> only.
  • ADRs: 0123 and 0232 (Thor for bin/os-sdlc; this change conforms), 0246 (exit 3), 0177/0178. None conflicts.

Out of scope

  • --target filtering of --show output (#861).
## Problem Evidence: running it inside ~/dev/cc-os/plugins/os-sdlc wrote that file (reverted). Reproduced again on main b80613ec. `os-sdlc project-config --help` (and `-h`) treats `--help` as the project name and writes `.sdlc/project.yaml` with `name: "--help"`. The cause is the design, not the call order. `bin/os-sdlc` is a Thor CLI (ADR-0123), but `project-config` declares no Thor options. It takes a `*args` splat, so Thor passes every token through unchecked. Then three hand parsers share the argv: `ArgvOption.take!` removes `--target`, `call` sniffs `@args.first == "--show"`, `validated_name` shifts the first token, and a private `OptionParser` (`OptionParserBuilder`) parses the rest. No layer rejects an unknown switch, so `--help` becomes a name. ``` plugins/os-sdlc/lib/os_sdlc/controller/project/onboarding.rb:37 def project_config(*args) plugins/os-sdlc/lib/os_sdlc/controller/project/onboarding.rb:38 target = ArgvOption.take!(args, "--target NAME") plugins/os-sdlc/lib/os_sdlc/command/project/config_command.rb:22 return show if @args.first == "--show" plugins/os-sdlc/lib/os_sdlc/command/project/config_command.rb:30 name = validated_name plugins/os-sdlc/lib/os_sdlc/command/project/config_command.rb:76 @args.shift plugins/os-sdlc/lib/os_sdlc/parser/project/option_parser_builder.rb:15 parser.parse!(@args) ``` A second symptom of the same design: `os-sdlc help project-config` prints only `os-sdlc project-config NAME`. It lists none of the six flags, because Thor does not know them. ADR-0123 chose Thor for `bin/os-sdlc` and rejected a hand-rolled `OptionParser` because it re-implements flag parsing and help. ADR-0232 keeps `bin/os-sdlc` on Thor because humans read its help. ## Approach Declare the command's interface in Thor and delete the hand parsers. This is the shape of the sibling Thor controllers `Evidence::Collection` and `Worktree::CLI`: `check_unknown_options!` plus one `method_option` per flag. - `Onboarding` calls `check_unknown_options!`. An unknown switch such as `--bogus` is refused before any command runs. The refusal fails through `ProcessIO#guarded` with exit 3 and writes no file (ADR-0246: every failure on this path exits 3). - `project_config` declares `--help` (alias `-h`) as a boolean option. When it is set, the command prints Thor's help for `project-config` (the same text as `os-sdlc help project-config`) and writes no file. ADR-0232 kept Thor for this CLI because humans read its `--help`; a refusal would break that. - `project_config(name = nil)` declares `--path`, `--test-path`, `--test-command`, `--lint-config`, `--test-framework`, `--target` (strings) and `--show` (boolean). - `ConfigCommand` receives `name:`, `target:`, `show:` and the option hash. It no longer reads argv. A missing name keeps the existing `LoudFailure` message. - Delete `OptionParserBuilder` (one caller). `ArgvOption` stays for the non-Thor scripts (`Boot::Inputs`, `LintWorklist`); only the `Onboarding` call goes. - `os-sdlc help project-config` and `project-config --help` print the same help. Thor lists every declared option. Required work: 1. `Onboarding` refuses unknown switches. check: present `check_unknown_options!` in `plugins/os-sdlc/lib/os_sdlc/controller/project/onboarding.rb` 2. `project_config` declares its flags as Thor options. check: present `method_option :test_command` in `plugins/os-sdlc/lib/os_sdlc/controller/project/onboarding.rb` 3. `project_config` takes a declared name, not a splat. check: absent `def project_config(*args)` in `plugins/os-sdlc/lib/os_sdlc/controller/project/onboarding.rb` 4. `--target` comes from Thor options, not `ArgvOption`. check: absent `ArgvOption` in `plugins/os-sdlc/lib/os_sdlc/controller/project/onboarding.rb` 5. `ConfigCommand` no longer parses argv. check: absent `@args` in `plugins/os-sdlc/lib/os_sdlc/command/project/config_command.rb` 6. Delete `OptionParserBuilder`. check: absent `OptionParserBuilder` in `plugins/os-sdlc/lib/**/*.rb` 7. Add tests for `--help`, `-h`, and `help project-config`. Update `tests/model/project_test_path_default_test.rb:50`, which builds `ConfigCommand` with an argv array. Sketch: ```ruby class Onboarding < Thor check_unknown_options! desc "project-config NAME", "create or update .sdlc/project.yaml" method_option :show, type: :boolean, default: false, desc: "print the current configuration" method_option :target, type: :string, desc: "named target in a targets-shaped project.yaml" method_option :path, type: :string method_option :test_command, type: :string # ... test_path, lint_config, test_framework def project_config(name = nil) OsSdlc::Project::ConfigCommand.new(name: name, options: options, invocation: @invocation).call end end ``` ## Acceptance criteria 1. Given a directory with no `.sdlc/project.yaml`, when `os-sdlc project-config --help` runs, then it exits 0, its output names `--path` and `--test-command`, and no `.sdlc/project.yaml` exists afterward. 2. Given the same directory, when `os-sdlc project-config -h` runs, then the result is the same as criterion 1. 2a. Given the same directory, when `os-sdlc project-config --bogus` runs, then it exits 3, stderr names `--bogus` as an unknown switch, and no `.sdlc/project.yaml` exists afterward. 3. When `os-sdlc help project-config` runs, then it exits 0 and its output names `--path`, `--test-path`, `--test-command`, `--lint-config`, `--test-framework`, `--target` and `--show`. 4. Given `os-sdlc project-config widgets --path app`, when it runs, then it writes `.sdlc/project.yaml` for `widgets` as it does today. 5. Given `os-sdlc project-config` with no name, when it runs, then it fails with the existing missing-name message and exit 3. 6. Given a targets-shaped project, when `project-config --show --target a` and `project-config --show --target=a` run, then both select target `a` as `--target=a` does today. ## Owner sketch ```ruby class OsSdlc::Project::Onboarding < Thor def project_config(name = nil) end end class OsSdlc::Project::ConfigCommand def initialize(name:, options:, invocation:) end def call; end end ``` ## Changes since filing (verified 2026-10-02 on main b80613ec) - The bug reproduces on main b80613ec. - #829 (858349ec) and #830 (e6961519) moved the files. The paths and lines in this ticket are still valid. - 2026-10-02: the approach changed from a call-order swap in `ConfigCommand#upsert` to Thor-declared options. A swap leaves three hand parsers next to Thor and keeps unknown switches silent (ADR-0123). - Thor 1.5.0 does not intercept `project-config --help` after the subcommand; with `check_unknown_options!` it raises `Unknown switches "--help"`. Verified in a scratch Thor class on 2026-10-02. ## Blast radius - Production: `Project::Onboarding#project_config`, `Project::ConfigCommand`, `Project::OptionParserBuilder` (deleted, one caller). `check_unknown_options!` is class-level, so `lint-init` also refuses unknown switches; `InitCommand` takes only a positional directory. - Tests that pin the current flow: `tests/controller/project/cli_project_config_test.rb`, `tests/controller/project/cli_project_config_target_path_test.rb`, `tests/model/project_test_path_default_test.rb:50`, `tests/command/project/config_command_missing_message_test.rb:12`, `tests/hardening_verdict_lint_project_test.rb:31`, `tests/adapter/gate/evaluator_project_shell_test.rb:40`. `tests/controller/argv_option_test.rb` is unaffected. - Callers: `skills/setup-project/SKILL.md` uses `project-config <name>` only. - ADRs: 0123 and 0232 (Thor for `bin/os-sdlc`; this change conforms), 0246 (exit 3), 0177/0178. None conflicts. ## Out of scope - `--target` filtering of `--show` output (#861).
Author
Owner

Body revised 2026-10-02 after blast-radius re-check on main b80613ec: bug reproduced; parse options first, then name, then test-command check; pinning tests and --help test listed.

Body revised 2026-10-02 after blast-radius re-check on main b80613ec: bug reproduced; parse options first, then name, then test-command check; pinning tests and --help test listed.
Author
Owner

PREDICTED RULING: REVISE

a. No.

b. ADR-0123 picked Thor for bin/os-sdlc and rejected a hand-rolled OptionParser. ADR-0232 keeps project-config on Thor because humans read its help. feedback-lens-request-is-a-design-symptom and feedback-adr-conflict-is-a-symptom say: fix the design that let the defect exist, not the symptom.

c. The line swap leaves three hand parsers next to Thor (ArgvOption, the --show sniff, OptionParserBuilder). Unknown switches stay silent, and os-sdlc help project-config lists no flags. Research: the sibling Thor controllers Evidence::Collection and Worktree::CLI use check_unknown_options! plus method_option. A scratch probe on Thor 1.5.0 showed that declared options refuse --help and accept --target a and --target=a.

d. Declare the flags in Thor, add check_unknown_options!, remove the ArgvOption call and the --show sniff from Onboarding, and delete OptionParserBuilder. Unknown switches fail with exit 3 (ADR-0246). The help path is os-sdlc help project-config. This conforms to ADR-0123, so no ADR ruling is needed. #861 space form: this is not a defect. On main, --show --target a selects target a. The failed repro passed "--target a" as one argv token (unsplit in zsh).

e. Not read: the ADR-0177/0178 bodies, the os-backlog CLI's help handling, and any user statement that project-config --help must print usage and exit 0. If such a statement exists, add a help alias.

PREDICTED RULING: REVISE a. No. b. ADR-0123 picked Thor for bin/os-sdlc and rejected a hand-rolled OptionParser. ADR-0232 keeps project-config on Thor because humans read its help. feedback-lens-request-is-a-design-symptom and feedback-adr-conflict-is-a-symptom say: fix the design that let the defect exist, not the symptom. c. The line swap leaves three hand parsers next to Thor (ArgvOption, the `--show` sniff, OptionParserBuilder). Unknown switches stay silent, and `os-sdlc help project-config` lists no flags. Research: the sibling Thor controllers Evidence::Collection and Worktree::CLI use `check_unknown_options!` plus `method_option`. A scratch probe on Thor 1.5.0 showed that declared options refuse `--help` and accept `--target a` and `--target=a`. d. Declare the flags in Thor, add `check_unknown_options!`, remove the ArgvOption call and the `--show` sniff from Onboarding, and delete OptionParserBuilder. Unknown switches fail with exit 3 (ADR-0246). The help path is `os-sdlc help project-config`. This conforms to ADR-0123, so no ADR ruling is needed. #861 space form: this is not a defect. On main, `--show --target a` selects target a. The failed repro passed `"--target a"` as one argv token (unsplit in zsh). e. Not read: the ADR-0177/0178 bodies, the os-backlog CLI's help handling, and any user statement that `project-config --help` must print usage and exit 0. If such a statement exists, add a help alias.
Author
Owner

Orchestrator correction to the predicted approach, 2026-10-02: project-config --help / -h must print the command's help and exit 0, not be refused as an unknown switch. Reason: ADR-0232 kept Thor for this CLI because 'its --help is read' by humans; a refusal breaks that. check_unknown_options! stays for other unknown switches (criterion 2a: --bogus exits 3). The rest of the predicted approach is unchanged.

Orchestrator correction to the predicted approach, 2026-10-02: `project-config --help` / `-h` must print the command's help and exit 0, not be refused as an unknown switch. Reason: ADR-0232 kept Thor for this CLI because 'its --help is read' by humans; a refusal breaks that. `check_unknown_options!` stays for other unknown switches (criterion 2a: `--bogus` exits 3). The rest of the predicted approach is unchanged.
Author
Owner

Work started 2026-10-02 on branch ticket-860 (os-sdlc pipeline, map poodr-ticket-implementation, target os-sdlc).

Work started 2026-10-02 on branch ticket-860 (os-sdlc pipeline, map poodr-ticket-implementation, target os-sdlc).
Author
Owner

Resolution

Done: project-config declares its flags as Thor method_options with check_unknown_options!. --help/-h print the command help and exit 0 with no file written; an unknown switch (--bogus) exits 3 with no file written; both --target forms are parsed by Thor. ConfigCommand takes name:/options: and reads no argv; OptionParserBuilder deleted. Approach revised before implementation (predicted ruling REVISE: ADR-0123/0232) and corrected so --help prints help (ADR-0232: its --help is read).

Evidence: Commits 5d0a1cd6 (feat, implementation 25: 8 dispatches, 0 retries, suite green, contract-auditor pass), 62f28820 (History row), afab5688 (merge of main, History-table conflict with #890 resolved keeping both rows). Live check on the worktree bin in an empty dir: --help exit 0 usage lists all flags; -h same; --bogus exit 3 'os-sdlc-Unknown switches "--bogus"'; no .sdlc written in any case.

Follow-ups: none filed. Observed, left as is: the unknown-switch refusal reads 'os-sdlc-Unknown switches' without the command name (LoudFailure#stderr_line prefix); it names the switch and exits 3, so it is not a defect.

## Resolution **Done:** project-config declares its flags as Thor method_options with check_unknown_options!. --help/-h print the command help and exit 0 with no file written; an unknown switch (--bogus) exits 3 with no file written; both --target forms are parsed by Thor. ConfigCommand takes name:/options: and reads no argv; OptionParserBuilder deleted. Approach revised before implementation (predicted ruling REVISE: ADR-0123/0232) and corrected so --help prints help (ADR-0232: its --help is read). **Evidence:** Commits 5d0a1cd6 (feat, implementation 25: 8 dispatches, 0 retries, suite green, contract-auditor pass), 62f28820 (History row), afab5688 (merge of main, History-table conflict with #890 resolved keeping both rows). Live check on the worktree bin in an empty dir: --help exit 0 usage lists all flags; -h same; --bogus exit 3 'os-sdlc-Unknown switches "--bogus"'; no .sdlc written in any case. **Follow-ups:** none filed. Observed, left as is: the unknown-switch refusal reads 'os-sdlc-Unknown switches' without the command name (LoudFailure#stderr_line prefix); it names the switch and exits 3, so it is not a defect.
jared closed this issue 2026-10-02 20:20:31 +00:00
Author
Owner

Correction to the PREDICTED RULING comment, recorded 2026-10-03. The comment says that declared options refuse --help and that the help path is os-sdlc help project-config. The shipped code (5d0a1cd6) declares --help/-h as an option and prints the help with exit 0. Live check on main, 2026-10-03, in an empty directory: project-config --help exit 0, -h exit 0, project-config widgets --bogus exit 3, no file written. How much does this matter: not materially. The shipped behavior matches the ticket's acceptance criteria and ADR-0232. No derivative ticket.

Correction to the PREDICTED RULING comment, recorded 2026-10-03. The comment says that declared options refuse `--help` and that the help path is `os-sdlc help project-config`. The shipped code (5d0a1cd6) declares `--help`/`-h` as an option and prints the help with exit 0. Live check on main, 2026-10-03, in an empty directory: `project-config --help` exit 0, `-h` exit 0, `project-config widgets --bogus` exit 3, no file written. How much does this matter: not materially. The shipped behavior matches the ticket's acceptance criteria and ADR-0232. No derivative ticket.
Author
Owner

Ticket-skeptic DROP for the unknown-switch error line (os-sdlc-Unknown switches "--bogus", no command name): the line comes from Onboarding.dispatch_refusing_bad_switches, which #860 added, and shares its root with #920. Record it as a finding under #920 instead of a separate ticket.

Ticket-skeptic DROP for the unknown-switch error line (`os-sdlc-Unknown switches "--bogus"`, no command name): the line comes from Onboarding.dispatch_refusing_bad_switches, which #860 added, and shares its root with #920. Record it as a finding under #920 instead of a separate ticket.
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#860
No description provided.