code.path is the target path; no lib/ default #655

Closed
opened 2026-09-23 21:36:22 +00:00 by jared · 3 comments
Owner

Problem

A target has two directory keys: code_path (default lib) and root_path (ADR-0208, default File.dirname(code_path)). code_path now only anchors test mirroring. root_path sets the read scope. Two keys for one idea confuse agents and users. The Ruby-only rule in Codebase#code? also hides non-Ruby edits from the behavior-verifier. On #627 implementation 12 the verifier failed the increment because code_changes had no hunk for plugins/os-sdlc/agents/wiring.md (#629).

User decision (planning session d1aa78d1, reconfirmed 2026-09-23): a target's code directory IS the target directory. project.code.path is an alias of a new project.target.path, for example plugins/os-sdlc, or . for a single-target project. There is no lib/ default. Every pipeline role sees all code in the target, including bin/ and non-Ruby files. This supersedes ADR-0208 and closes #629.

Approach

Record the decision first. Run /os-adr:create for an ADR that supersedes ADR-0208. It records: one path per target, code_path as a load-time alias, root_path and Code#root removed, non-Ruby target files in code_changes, and the mirroring rule below.

Model. Add Project::Target = Data.define(:path) with default ".". Project holds target in place of code. Project#code returns Project::Code.new(path: target.path). Code loses root. Code#include? and Code#relative treat "." as the whole repository (no "./" prefix).

Config keys. The loader reads only the target key path. code_path and root_path are not read at all: unknown keys are ignored, with no alias, no conflict error, and no removal error. project.code.path stays as a Ruby-only alias method of project.target.path. A flat, single-target .sdlc/project.yaml (no targets: map) defaults path to "." when the key is absent. A named target under targets: is different: if it has no path: key, Project.load still defaults it to "." (so bare project loading never raises), but Validation treats a named target's missing path: as a problem, and implementation-open surfaces it as a hard failure before any pipeline role runs: target "<name>" has no path: key; add path: <target directory> to .sdlc/project.yaml (see /os-sdlc:setup-project). This is deliberately louder than the missing-directory problem (AC 18), because a named target with no path: was previously silently scoped to the whole repository — the exact defect ADR-0208 introduced and this ticket removes. The stored implementation config blob (the JSON snapshot on an open implementation) also reads path only; a blob without it loads with path "." and is not re-validated — this widens scope for in-flight implementations only, which is acceptable.

Existing values. This change migrates the cc-os .sdlc/project.yaml: every target gets path: plugins/<x>, and the code_path/root_path lines are removed. Other repos are out of scope: they keep whatever keys they have, and a multi-target repo there with no path: keys loads every target with path "." (wider scope, no failure). Cross-project follow-up issues will be filed for cc-plugins and cc-plugins/api-wrapper.

Classification. Codebase#code?(path) is true for any file under the target that is not under test_path and has no segment in EXCLUDED_SEGMENTS. The Ruby check moves out of code?. Changes#code_paths and Changes#code_diff therefore include agents/*.md, skills/**, hooks/*, and bin/*. Changes#unmapped_code_paths keeps only Ruby files (RubyFile.ruby?). A non-Ruby edit therefore appears in code_changes but does not widen diff-test to the full suite. This keeps ADR-0208 D6's cost limit. Codebase#tests_for returns nil for a non-Ruby file.

Test mapping: S2, longest suffix plus scenario siblings. For a changed Ruby production file under the target, glob <test_path>/**/<basename>_test.rb and <test_path>/**/<basename>_*_test.rb. Only regular files count; a directory that happens to share the basename (for example tests/evidence) never matches. For each hit, take its path relative to test_path, remove the _test suffix, and for a scenario hit (the _*_test.rb glob) also remove the _<scenario> part, so the hit's directory plus <basename>.rb is what gets compared. Keep the hit only if that relative path is a path-segment suffix of the production file's path. Among the surviving hits, keep the exact hit or hits with the longest matching suffix, plus any scenario hits that sit in that same directory. No hit at all means unmapped, and the file falls back to the Ruby full-suite trigger (ADR-0208 D4).

Measured 2026-09-23 over 9 targets in 4 repos: this rule never selects a different test than today's lib/app/src-strip rule, only supersets (the scenario siblings). On os-sdlc it maps 149 of 428 production files, against 142 today. Median 1 test per file, p90 2, max 20. hooks/*.rb files now map to their real hook tests instead of falling back unmapped. Glob cost is about 0.5 ms per file; no index is needed.

Lint worklist. WorklistTarget#lint_path is [File.join(root, target.path), *configured_lint_paths]. For "." it is [root, *configured_lint_paths]. bin_files, conventional_test_dirs, and root_subtree are deleted, because the target directory already contains them. RuboCop's own file discovery includes ruby-shebang files.

Surface. to_flat_h, to_config_h, and status_line emit path (path=plugins/os-sdlc) in place of code_path and root_path. Yaml::Document::FLAT_KEYS reads only path; code_path and root_path are removed from the flat-key list, so a yaml file that still has them loads with those lines simply ignored. project-config accepts --path PATH only; --code-path and --root-path are removed. Validation reports a missing target directory as path "<value>" does not exist for target <name> and drops root_path_problem.

Prose. The charters and skills that name root_path or code_path say "the target path (path in .sdlc/project.yaml)". These are change-auditor, reviewer, seam-designer, test-writer, programmer, setup-project, responsibility-audit, code-probe SKILL.md, and cheatsheet.

Required work:

  1. Record the superseding ADR with /os-adr:create; set ADR-0208 superseded-by — check: grep -l "supersedes: \[\?\"\?0208" /home/jared/dev/cc-os/docs/adr/*.md
  2. Add Project::Target, remove Code#root, and make Project#code alias the target path (proven by AC 1, AC 2)
  3. Read only path (default "." for a flat project or a named target at load time), ignoring code_path and root_path (proven by AC 3, AC 4, AC 5)
  4. Keep a stored implementation blob without path loadable at "." (proven by AC 6)
  5. Widen Codebase#code? to every file under the target; restrict the unmapped fallback to Ruby files (proven by AC 7, AC 8, AC 9)
  6. Implement S2 (longest suffix plus scenario siblings) in Test#covering (proven by AC 10, AC 11, AC 12, AC 13, AC 14)
  7. Collapse WorklistTarget#lint_path to the target directory plus lint_paths (proven by AC 15)
  8. Emit path in to_flat_h, status_line, and project-config --show; accept --path; drop --code-path and --root-path (proven by AC 16, AC 17)
  9. Report a missing target directory by the path key (proven by AC 18)
  10. Fail implementation-open validation for a named target with no path: key, with the message target "<name>" has no path: key; add path: <target directory> to .sdlc/project.yaml (see /os-sdlc:setup-project); a flat single-target project with no path: key still opens, defaulted to "." (proven by AC 19, AC 20)
  11. Migrate the cc-os .sdlc/project.yaml to path: per target — check: cd /home/jared/dev/cc-os && ! grep -n "code_path\|root_path" .sdlc/project.yaml && plugins/os-sdlc/bin/os-sdlc project-config --show --target os-sdlc | grep -q "path=plugins/os-sdlc "
  12. Replace root_path/code_path wording in charters and skills — check: ! grep -rn "root_path\|code_path" /home/jared/dev/cc-os/plugins/os-sdlc/agents /home/jared/dev/cc-os/plugins/os-sdlc/skills
  13. Run bin/refresh-plugins and append one History row about the target path to docs/implementation-status/os-sdlc.md — check: tail -n 1 /home/jared/dev/cc-os/docs/implementation-status/os-sdlc.md | grep -qi "target path"

Rejected: keep root_path next to code_path (ADR-0208) — the user decided one directory per target. Rejected: make every target file a fallback trigger (ADR-0208 D6-B) — each .md edit would run the full suite.

Acceptance criteria

  1. Given a target os-sdlc with path: plugins/os-sdlc, when the project loads, then project.target.path and project.code.path both return plugins/os-sdlc.
  2. Given a flat .sdlc/project.yaml with no path key, when the project loads, then project.code.path returns . and project.code.include?("lib/widget.rb") is true.
  3. Given a target with code_path: plugins/x/lib and no path key, when the project loads, then project.target.path returns . (the code_path key is ignored).
  4. Given a target with path: plugins/x and code_path: plugins/x/lib, when the project loads, then it loads without error and project.target.path returns plugins/x (the code_path key is ignored).
  5. Given a target with root_path: plugins/x and no path key, when the project loads, then it loads without error and project.target.path returns . (the root_path key is ignored).
  6. Given an implementation whose stored project_config JSON has no path key, when implementation.project(root) runs, then it returns a project with code.path . and does not raise.
  7. Given target path plugins/os-sdlc and a changed plugins/os-sdlc/agents/wiring.md, when the behavior-verifier brief resolves code_changes, then the diff contains a hunk for plugins/os-sdlc/agents/wiring.md.
  8. Given target path plugins/os-sdlc and only a changed plugins/os-sdlc/agents/wiring.md, when the diff-test gate command is built, then it is not the full-suite command.
  9. Given target path plugins/os-sdlc and a changed plugins/os-sdlc/bin/os-sdlc-runner with a ruby shebang, when the diff-test gate command is built, then it is the full-suite command.
  10. Given target path plugins/os-sdlc with test path plugins/os-sdlc/tests, when tests_for("plugins/os-sdlc/lib/os_sdlc/model/project/changes.rb") runs, then it returns ["plugins/os-sdlc/tests/model/project/changes_test.rb"].
  11. Given target path plugins/os-backlog with test path plugins/os-backlog/tests, when tests_for("plugins/os-backlog/lib/backlog/checkout.rb") runs, then it returns ["plugins/os-backlog/tests/checkout_test.rb"].
  12. Given target path plugins/os with test path plugins/os/tests, when tests_for("plugins/os/lib/worktree/finish_command.rb") runs, then it returns plugins/os/tests/worktree/finish_command_test.rb plus every plugins/os/tests/worktree/finish_command_*_test.rb scenario file in that directory.
  13. Given target path plugins/os-sdlc with test path plugins/os-sdlc/tests and two fixture tests <test_path>/hook/widget_test.rb and <test_path>/hooks/widget_test.rb for a production file at <target>/hooks/widget.rb, when tests_for runs, then it returns only <test_path>/hooks/widget_test.rb (the hook/ candidate does not suffix-match, since hook is not hooks).
  14. Given target path plugins/os-sdlc with test path plugins/os-sdlc/tests, when tests_for("plugins/os-sdlc/bin/evidence") runs, then it is unmapped, even though plugins/os-sdlc/tests/evidence/ exists as a directory; and given two fixture tests <dir>/widget_test.rb and <dir>/sub/widget_test.rb for a production file at <dir>/sub/widget.rb, tests_for returns only <dir>/sub/widget_test.rb, the longer suffix.
  15. Given target path plugins/os-sdlc and lint_paths: [extra], when WorklistTarget#lint_path runs, then it returns the absolute target directory followed by the absolute extra path, and nothing else.
  16. Given target path plugins/os-sdlc, when project-config --show --target os-sdlc runs, then the output contains path=plugins/os-sdlc and contains neither code_path= nor root_path=.
  17. Given a project directory, when project-config --path app runs, then the written .sdlc/project.yaml has path: app; project-config --code-path and project-config --root-path are not recognized flags.
  18. Given a target whose path directory does not exist, when implementation-open validates the project, then the problem line starts with path "<value>" does not exist for target <name>.
  19. Given a targets: map with a named target x that has no path: key, when implementation-open validates the project, then it fails and the problem line is exactly target "x" has no path: key; add path: <target directory> to .sdlc/project.yaml (see /os-sdlc:setup-project).
  20. Given a flat, single-target .sdlc/project.yaml with no targets: map and no path: key, when implementation-open validates the project, then it succeeds and the opened implementation's project has target.path ..

Owner sketch

class OsSdlc::Model::Project
  Target = Data.define(:path)            # default "."
  Code   = Data.define(:path)            # root removed
  def code = Code.new(path: target.path)
end

class OsSdlc::Model::Project::FlatAttrs  # reads path only, default "."; code_path/root_path ignored
class OsSdlc::Model::Project::Codebase
  def code?(path)                        # any file under target, not test, not excluded
  def tests_for(code_file)               # nil for non-Ruby
end
class OsSdlc::Model::Project::Test
  def covering(code_relative)            # S2: longest path-segment suffix, plus scenario siblings
end
class OsSdlc::Model::Project::Changes
  def unmapped_code_paths                # Ruby files only
end
class OsSdlc::Lint::WorklistTarget
  def lint_path                          # [target dir, *lint_paths]
end

Non-goals

  • Files outside the target, for example repo-root .claude/settings.json from #627 case 7. They stay out of code_changes.
  • A bin/<name> → <name>_bin_test.rb mapping (ADR-0208 D4-B follow-up).
  • Migrating other repos' .sdlc/project.yaml files. A flat single-target project elsewhere with no path: key keeps working, defaulted to ".". A repo with a targets: map whose named targets have no path: key now fails loudly at implementation-open with the target "<name>" has no path: key... message (AC 19) instead of silently widening scope — this is the intended tightening, not a regression to work around here. cc-plugins and cc-plugins/api-wrapper are both named-target projects without path: keys today, so they will fail this way until migrated; cross-project follow-up issues will be filed for each.
  • Renaming Project#root (see #657).

Retired tests

plugins/os-sdlc/tests/model/project/code_root_test.rb
plugins/os-sdlc/tests/model/project/validation_root_path_test.rb
plugins/os-sdlc/tests/lint_worklist_target_root_path_test.rb
plugins/os-sdlc/tests/lint_worklist_target_path_test.rb
plugins/os-sdlc/tests/model/project/codebase_code_test.rb#test_readme_is_not_code
plugins/os-sdlc/tests/cli_project_config_test.rb#test_project_config_updates_an_existing_project_yaml
plugins/os-sdlc/tests/model/project_defaults_test.rb#test_load_applies_plain_gem_defaults_when_keys_absent
plugins/os-sdlc/tests/model/project_defaults_test.rb#test_new_without_the_new_keys_applies_the_same_defaults

Origin

  • Trigger: #627 implementation 12, dispatch 150 (behavior-verifier) failed on a missing agents/wiring.md hunk (#629)
  • Improvised this session: none
  • Chain: verifier reports a missing file ← code_changes drops non-Ruby files (Codebase#code?, model/project/codebase.rb:22-24) ← code scope is split across code_path and root_path ← DESIGN (ADR-0208 D1, D6)
  • Root candidate: this ticket
  • Where: plugins/os-sdlc/lib/os_sdlc/model/project/code.rb, codebase.rb, test.rb, flat_attrs.rb, lint/worklist_target.rb
  • Session: a5f43be9-5476-418e-99f0-2c4d0e5fa57d (decision: d1aa78d1)
  • Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/a5f43be9-5476-418e-99f0-2c4d0e5fa57d.jsonl
## Problem A target has two directory keys: `code_path` (default `lib`) and `root_path` (ADR-0208, default `File.dirname(code_path)`). `code_path` now only anchors test mirroring. `root_path` sets the read scope. Two keys for one idea confuse agents and users. The Ruby-only rule in `Codebase#code?` also hides non-Ruby edits from the behavior-verifier. On #627 implementation 12 the verifier failed the increment because `code_changes` had no hunk for `plugins/os-sdlc/agents/wiring.md` (#629). User decision (planning session d1aa78d1, reconfirmed 2026-09-23): a target's code directory IS the target directory. `project.code.path` is an alias of a new `project.target.path`, for example `plugins/os-sdlc`, or `.` for a single-target project. There is no `lib/` default. Every pipeline role sees all code in the target, including `bin/` and non-Ruby files. This supersedes ADR-0208 and closes #629. ## Approach Record the decision first. Run `/os-adr:create` for an ADR that supersedes ADR-0208. It records: one `path` per target, `code_path` as a load-time alias, `root_path` and `Code#root` removed, non-Ruby target files in `code_changes`, and the mirroring rule below. Model. Add `Project::Target = Data.define(:path)` with default `"."`. `Project` holds `target` in place of `code`. `Project#code` returns `Project::Code.new(path: target.path)`. `Code` loses `root`. `Code#include?` and `Code#relative` treat `"."` as the whole repository (no `"./"` prefix). Config keys. The loader reads only the target key `path`. `code_path` and `root_path` are not read at all: unknown keys are ignored, with no alias, no conflict error, and no removal error. `project.code.path` stays as a Ruby-only alias method of `project.target.path`. A flat, single-target `.sdlc/project.yaml` (no `targets:` map) defaults `path` to `"."` when the key is absent. A *named* target under `targets:` is different: if it has no `path:` key, `Project.load` still defaults it to `"."` (so bare project loading never raises), but `Validation` treats a named target's missing `path:` as a problem, and implementation-open surfaces it as a hard failure before any pipeline role runs: `target "<name>" has no path: key; add path: <target directory> to .sdlc/project.yaml (see /os-sdlc:setup-project)`. This is deliberately louder than the missing-directory problem (AC 18), because a named target with no `path:` was previously silently scoped to the whole repository — the exact defect ADR-0208 introduced and this ticket removes. The stored implementation config blob (the JSON snapshot on an open implementation) also reads `path` only; a blob without it loads with path `"."` and is not re-validated — this widens scope for in-flight implementations only, which is acceptable. Existing values. This change migrates the cc-os `.sdlc/project.yaml`: every target gets `path: plugins/<x>`, and the `code_path`/`root_path` lines are removed. Other repos are out of scope: they keep whatever keys they have, and a multi-target repo there with no `path:` keys loads every target with path `"."` (wider scope, no failure). Cross-project follow-up issues will be filed for cc-plugins and cc-plugins/api-wrapper. Classification. `Codebase#code?(path)` is true for any file under the target that is not under `test_path` and has no segment in `EXCLUDED_SEGMENTS`. The Ruby check moves out of `code?`. `Changes#code_paths` and `Changes#code_diff` therefore include `agents/*.md`, `skills/**`, `hooks/*`, and `bin/*`. `Changes#unmapped_code_paths` keeps only Ruby files (`RubyFile.ruby?`). A non-Ruby edit therefore appears in `code_changes` but does not widen diff-test to the full suite. This keeps ADR-0208 D6's cost limit. `Codebase#tests_for` returns `nil` for a non-Ruby file. Test mapping: S2, longest suffix plus scenario siblings. For a changed Ruby production file under the target, glob `<test_path>/**/<basename>_test.rb` and `<test_path>/**/<basename>_*_test.rb`. Only regular files count; a directory that happens to share the basename (for example `tests/evidence`) never matches. For each hit, take its path relative to `test_path`, remove the `_test` suffix, and for a scenario hit (the `_*_test.rb` glob) also remove the `_<scenario>` part, so the hit's directory plus `<basename>.rb` is what gets compared. Keep the hit only if that relative path is a path-segment suffix of the production file's path. Among the surviving hits, keep the exact hit or hits with the longest matching suffix, plus any scenario hits that sit in that same directory. No hit at all means unmapped, and the file falls back to the Ruby full-suite trigger (ADR-0208 D4). Measured 2026-09-23 over 9 targets in 4 repos: this rule never selects a different test than today's lib/app/src-strip rule, only supersets (the scenario siblings). On os-sdlc it maps 149 of 428 production files, against 142 today. Median 1 test per file, p90 2, max 20. `hooks/*.rb` files now map to their real hook tests instead of falling back unmapped. Glob cost is about 0.5 ms per file; no index is needed. Lint worklist. `WorklistTarget#lint_path` is `[File.join(root, target.path), *configured_lint_paths]`. For `"."` it is `[root, *configured_lint_paths]`. `bin_files`, `conventional_test_dirs`, and `root_subtree` are deleted, because the target directory already contains them. RuboCop's own file discovery includes ruby-shebang files. Surface. `to_flat_h`, `to_config_h`, and `status_line` emit `path` (`path=plugins/os-sdlc`) in place of `code_path` and `root_path`. `Yaml::Document::FLAT_KEYS` reads only `path`; `code_path` and `root_path` are removed from the flat-key list, so a yaml file that still has them loads with those lines simply ignored. `project-config` accepts `--path PATH` only; `--code-path` and `--root-path` are removed. `Validation` reports a missing target directory as `path "<value>" does not exist for target <name>` and drops `root_path_problem`. Prose. The charters and skills that name `root_path` or `code_path` say "the target path (`path` in `.sdlc/project.yaml`)". These are change-auditor, reviewer, seam-designer, test-writer, programmer, setup-project, responsibility-audit, code-probe SKILL.md, and cheatsheet. Required work: 1. Record the superseding ADR with `/os-adr:create`; set ADR-0208 `superseded-by` — check: `grep -l "supersedes: \[\?\"\?0208" /home/jared/dev/cc-os/docs/adr/*.md` 2. Add `Project::Target`, remove `Code#root`, and make `Project#code` alias the target path (proven by AC 1, AC 2) 3. Read only `path` (default `"."` for a flat project or a named target at load time), ignoring `code_path` and `root_path` (proven by AC 3, AC 4, AC 5) 4. Keep a stored implementation blob without `path` loadable at `"."` (proven by AC 6) 5. Widen `Codebase#code?` to every file under the target; restrict the unmapped fallback to Ruby files (proven by AC 7, AC 8, AC 9) 6. Implement S2 (longest suffix plus scenario siblings) in `Test#covering` (proven by AC 10, AC 11, AC 12, AC 13, AC 14) 7. Collapse `WorklistTarget#lint_path` to the target directory plus `lint_paths` (proven by AC 15) 8. Emit `path` in `to_flat_h`, `status_line`, and `project-config --show`; accept `--path`; drop `--code-path` and `--root-path` (proven by AC 16, AC 17) 9. Report a missing target directory by the `path` key (proven by AC 18) 10. Fail implementation-open validation for a named target with no `path:` key, with the message `target "<name>" has no path: key; add path: <target directory> to .sdlc/project.yaml (see /os-sdlc:setup-project)`; a flat single-target project with no `path:` key still opens, defaulted to `"."` (proven by AC 19, AC 20) 11. Migrate the cc-os `.sdlc/project.yaml` to `path:` per target — check: `cd /home/jared/dev/cc-os && ! grep -n "code_path\|root_path" .sdlc/project.yaml && plugins/os-sdlc/bin/os-sdlc project-config --show --target os-sdlc | grep -q "path=plugins/os-sdlc "` 12. Replace `root_path`/`code_path` wording in charters and skills — check: `! grep -rn "root_path\|code_path" /home/jared/dev/cc-os/plugins/os-sdlc/agents /home/jared/dev/cc-os/plugins/os-sdlc/skills` 13. Run `bin/refresh-plugins` and append one History row about the target path to `docs/implementation-status/os-sdlc.md` — check: `tail -n 1 /home/jared/dev/cc-os/docs/implementation-status/os-sdlc.md | grep -qi "target path"` Rejected: keep `root_path` next to `code_path` (ADR-0208) — the user decided one directory per target. Rejected: make every target file a fallback trigger (ADR-0208 D6-B) — each `.md` edit would run the full suite. ## Acceptance criteria 1. Given a target `os-sdlc` with `path: plugins/os-sdlc`, when the project loads, then `project.target.path` and `project.code.path` both return `plugins/os-sdlc`. 2. Given a flat `.sdlc/project.yaml` with no `path` key, when the project loads, then `project.code.path` returns `.` and `project.code.include?("lib/widget.rb")` is true. 3. Given a target with `code_path: plugins/x/lib` and no `path` key, when the project loads, then `project.target.path` returns `.` (the `code_path` key is ignored). 4. Given a target with `path: plugins/x` and `code_path: plugins/x/lib`, when the project loads, then it loads without error and `project.target.path` returns `plugins/x` (the `code_path` key is ignored). 5. Given a target with `root_path: plugins/x` and no `path` key, when the project loads, then it loads without error and `project.target.path` returns `.` (the `root_path` key is ignored). 6. Given an implementation whose stored project_config JSON has no `path` key, when `implementation.project(root)` runs, then it returns a project with `code.path` `.` and does not raise. 7. Given target path `plugins/os-sdlc` and a changed `plugins/os-sdlc/agents/wiring.md`, when the behavior-verifier brief resolves `code_changes`, then the diff contains a hunk for `plugins/os-sdlc/agents/wiring.md`. 8. Given target path `plugins/os-sdlc` and only a changed `plugins/os-sdlc/agents/wiring.md`, when the diff-test gate command is built, then it is not the full-suite command. 9. Given target path `plugins/os-sdlc` and a changed `plugins/os-sdlc/bin/os-sdlc-runner` with a ruby shebang, when the diff-test gate command is built, then it is the full-suite command. 10. Given target path `plugins/os-sdlc` with test path `plugins/os-sdlc/tests`, when `tests_for("plugins/os-sdlc/lib/os_sdlc/model/project/changes.rb")` runs, then it returns `["plugins/os-sdlc/tests/model/project/changes_test.rb"]`. 11. Given target path `plugins/os-backlog` with test path `plugins/os-backlog/tests`, when `tests_for("plugins/os-backlog/lib/backlog/checkout.rb")` runs, then it returns `["plugins/os-backlog/tests/checkout_test.rb"]`. 12. Given target path `plugins/os` with test path `plugins/os/tests`, when `tests_for("plugins/os/lib/worktree/finish_command.rb")` runs, then it returns `plugins/os/tests/worktree/finish_command_test.rb` plus every `plugins/os/tests/worktree/finish_command_*_test.rb` scenario file in that directory. 13. Given target path `plugins/os-sdlc` with test path `plugins/os-sdlc/tests` and two fixture tests `<test_path>/hook/widget_test.rb` and `<test_path>/hooks/widget_test.rb` for a production file at `<target>/hooks/widget.rb`, when `tests_for` runs, then it returns only `<test_path>/hooks/widget_test.rb` (the `hook/` candidate does not suffix-match, since `hook` is not `hooks`). 14. Given target path `plugins/os-sdlc` with test path `plugins/os-sdlc/tests`, when `tests_for("plugins/os-sdlc/bin/evidence")` runs, then it is unmapped, even though `plugins/os-sdlc/tests/evidence/` exists as a directory; and given two fixture tests `<dir>/widget_test.rb` and `<dir>/sub/widget_test.rb` for a production file at `<dir>/sub/widget.rb`, `tests_for` returns only `<dir>/sub/widget_test.rb`, the longer suffix. 15. Given target path `plugins/os-sdlc` and `lint_paths: [extra]`, when `WorklistTarget#lint_path` runs, then it returns the absolute target directory followed by the absolute `extra` path, and nothing else. 16. Given target path `plugins/os-sdlc`, when `project-config --show --target os-sdlc` runs, then the output contains `path=plugins/os-sdlc` and contains neither `code_path=` nor `root_path=`. 17. Given a project directory, when `project-config --path app` runs, then the written `.sdlc/project.yaml` has `path: app`; `project-config --code-path` and `project-config --root-path` are not recognized flags. 18. Given a target whose `path` directory does not exist, when implementation-open validates the project, then the problem line starts with `path "<value>" does not exist for target <name>`. 19. Given a `targets:` map with a named target `x` that has no `path:` key, when implementation-open validates the project, then it fails and the problem line is exactly `target "x" has no path: key; add path: <target directory> to .sdlc/project.yaml (see /os-sdlc:setup-project)`. 20. Given a flat, single-target `.sdlc/project.yaml` with no `targets:` map and no `path:` key, when implementation-open validates the project, then it succeeds and the opened implementation's project has `target.path` `.`. ## Owner sketch ```ruby class OsSdlc::Model::Project Target = Data.define(:path) # default "." Code = Data.define(:path) # root removed def code = Code.new(path: target.path) end class OsSdlc::Model::Project::FlatAttrs # reads path only, default "."; code_path/root_path ignored class OsSdlc::Model::Project::Codebase def code?(path) # any file under target, not test, not excluded def tests_for(code_file) # nil for non-Ruby end class OsSdlc::Model::Project::Test def covering(code_relative) # S2: longest path-segment suffix, plus scenario siblings end class OsSdlc::Model::Project::Changes def unmapped_code_paths # Ruby files only end class OsSdlc::Lint::WorklistTarget def lint_path # [target dir, *lint_paths] end ``` ## Non-goals - Files outside the target, for example repo-root `.claude/settings.json` from #627 case 7. They stay out of `code_changes`. - A `bin/<name>` → `<name>_bin_test.rb` mapping (ADR-0208 D4-B follow-up). - Migrating other repos' `.sdlc/project.yaml` files. A flat single-target project elsewhere with no `path:` key keeps working, defaulted to `"."`. A repo with a `targets:` map whose named targets have no `path:` key now fails loudly at implementation-open with the `target "<name>" has no path: key...` message (AC 19) instead of silently widening scope — this is the intended tightening, not a regression to work around here. `cc-plugins` and `cc-plugins/api-wrapper` are both named-target projects without `path:` keys today, so they will fail this way until migrated; cross-project follow-up issues will be filed for each. - Renaming `Project#root` (see #657). ## Retired tests plugins/os-sdlc/tests/model/project/code_root_test.rb plugins/os-sdlc/tests/model/project/validation_root_path_test.rb plugins/os-sdlc/tests/lint_worklist_target_root_path_test.rb plugins/os-sdlc/tests/lint_worklist_target_path_test.rb plugins/os-sdlc/tests/model/project/codebase_code_test.rb#test_readme_is_not_code plugins/os-sdlc/tests/cli_project_config_test.rb#test_project_config_updates_an_existing_project_yaml plugins/os-sdlc/tests/model/project_defaults_test.rb#test_load_applies_plain_gem_defaults_when_keys_absent plugins/os-sdlc/tests/model/project_defaults_test.rb#test_new_without_the_new_keys_applies_the_same_defaults ## Origin - Trigger: #627 implementation 12, dispatch 150 (behavior-verifier) failed on a missing `agents/wiring.md` hunk (#629) - Improvised this session: none - Chain: verifier reports a missing file ← `code_changes` drops non-Ruby files (`Codebase#code?`, `model/project/codebase.rb:22-24`) ← code scope is split across `code_path` and `root_path` ← DESIGN (ADR-0208 D1, D6) - Root candidate: this ticket - Where: `plugins/os-sdlc/lib/os_sdlc/model/project/code.rb`, `codebase.rb`, `test.rb`, `flat_attrs.rb`, `lint/worklist_target.rb` - Session: a5f43be9-5476-418e-99f0-2c4d0e5fa57d (decision: d1aa78d1) - Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/a5f43be9-5476-418e-99f0-2c4d0e5fa57d.jsonl
Author
Owner

Work started on branch ticket-655 (session 557d47dd) via /os-sdlc:implement --target os-sdlc. Step 1: superseding ADR for ADR-0208, then implementation-open.

Work started on branch ticket-655 (session 557d47dd) via /os-sdlc:implement --target os-sdlc. Step 1: superseding ADR for ADR-0208, then implementation-open.
Author
Owner

Deviation 1: stale tests outside the Retired list (#665)

Problem. After test-reviewer accepted the tests (round 4) and programmer implemented ADR-0210, the diff-test gate stayed red through two programmer-repair rounds (dispatches 223, 224). Every red was in a pre-existing test file that asserts the removed API: code_path: fixtures, Code#root, --root-path, bin_files and conventional_test_dirs lint paths, and tests_for raising instead of returning nil. The ticket's Retired tests list named three files; the change invalidated about twenty. The map routes a red diff-test only to programmer, who may not edit tests, so the pipeline could not converge.

Round Dispatch Result
programmer 222 23 new AC tests green; 36 reds, all in untouched test files
programmer-repair 223 no production change can satisfy both; no edit
programmer-repair 224 same

Solution. A Sonnet agent updated only test files in the worktree: three files deleted (behavior removed outright), the rest rewritten from code_path: to path: and from project.code to project.target, shared fixtures in tests/support/ updated once. Commit e38e71b1. Suite after: 1419 runs, 3221 assertions, 0 failures. The pipeline re-enters at programmer-repair so the gate, verifier, and auditors still run through the map.

Pipeline defect. #665 already covers the missing route. A second symptom surfaced here: the ticket-open Retired tests check verifies that listed entries exist but cannot detect entries that are missing.

Deviation 2: gate output cut at 4000 characters (#670)

Problem. In test-reviewer rounds 1 to 3 (dispatches 216, 218, 220) the brief's diff-test output stopped at 4000 of 9000 to 14000 characters. The reviewer could not see which new tests were red and recorded a GAP item each round instead of a verdict it could stand behind.

Solution. #670 filed against BriefFacts::MAX_GATE_OUTPUT_CHARS. For round 4 the reviewer was told it may run a single test file directly to observe a red reason the brief does not show; it then passed the set.

## Deviation 1: stale tests outside the Retired list (#665) **Problem.** After test-reviewer accepted the tests (round 4) and programmer implemented ADR-0210, the diff-test gate stayed red through two programmer-repair rounds (dispatches 223, 224). Every red was in a pre-existing test file that asserts the removed API: `code_path:` fixtures, `Code#root`, `--root-path`, `bin_files` and `conventional_test_dirs` lint paths, and `tests_for` raising instead of returning nil. The ticket's Retired tests list named three files; the change invalidated about twenty. The map routes a red diff-test only to programmer, who may not edit tests, so the pipeline could not converge. | Round | Dispatch | Result | |---|---|---| | programmer | 222 | 23 new AC tests green; 36 reds, all in untouched test files | | programmer-repair | 223 | no production change can satisfy both; no edit | | programmer-repair | 224 | same | **Solution.** A Sonnet agent updated only test files in the worktree: three files deleted (behavior removed outright), the rest rewritten from `code_path:` to `path:` and from `project.code` to `project.target`, shared fixtures in `tests/support/` updated once. Commit e38e71b1. Suite after: 1419 runs, 3221 assertions, 0 failures. The pipeline re-enters at programmer-repair so the gate, verifier, and auditors still run through the map. **Pipeline defect.** #665 already covers the missing route. A second symptom surfaced here: the ticket-open `Retired tests` check verifies that listed entries exist but cannot detect entries that are missing. ## Deviation 2: gate output cut at 4000 characters (#670) **Problem.** In test-reviewer rounds 1 to 3 (dispatches 216, 218, 220) the brief's diff-test output stopped at 4000 of 9000 to 14000 characters. The reviewer could not see which new tests were red and recorded a GAP item each round instead of a verdict it could stand behind. **Solution.** #670 filed against `BriefFacts::MAX_GATE_OUTPUT_CHARS`. For round 4 the reviewer was told it may run a single test file directly to observe a red reason the brief does not show; it then passed the set.
Author
Owner

Resolution

Done: One path per target (ADR-0210 supersedes ADR-0208): Project::Target with default '.'; project.code.path aliases target.path; Code#root removed; loader reads only path: (code_path/root_path ignored); stored blob without path loads at '.'; Codebase#code? covers every non-test file under the target while unmapped fallback stays Ruby-only; S2 test mapping (longest suffix plus scenario siblings) in Test::Candidates; WorklistTarget#lint_path = [target, *lint_paths]; project-config emits path= and accepts --path only; missing-directory and missing-path: validation messages per AC 18-20; cc-os .sdlc/project.yaml migrated; charters and skills reworded; plugins refreshed; History row added.

Evidence: Pipeline implementation 19, 19 dispatches, implementation_complete (change-auditor, seam-designer, 4 test-writer/test-reviewer rounds, programmer + 3 repair, behavior-verifier pass, suite-check pass x2, contract-auditor -> refactorer -> pass). Commits f01d5984 (ADR-0210), e38e71b1 (hand-finished stale tests, #665), 753d1123 (implementation), merge commit on main 157da599. os-sdlc suite after merge: 1419 runs, 3219 assertions, 0 failures, 0 errors, 40.8 s. Ticket checks 11 and 12 pass (no code_path/root_path in .sdlc/project.yaml, agents, or skills). Deviation write-up: #655 (comment)

Follow-ups: Pipeline defects cited: #665 (no repair route for red test-file diffs; hit again here on ~20 stale tests outside the Retired list), #670 (gate output cut at 4000 chars blinds test-reviewer; filed this session). Cross-project: jaredswanson/cc-plugins#74 (migrate named targets to path:). Retired-tests check cannot detect missing entries: dropped, absorbed into #665's route discussion. #629 closes with this ticket per its Problem statement.

## Resolution **Done:** One path per target (ADR-0210 supersedes ADR-0208): Project::Target with default '.'; project.code.path aliases target.path; Code#root removed; loader reads only path: (code_path/root_path ignored); stored blob without path loads at '.'; Codebase#code? covers every non-test file under the target while unmapped fallback stays Ruby-only; S2 test mapping (longest suffix plus scenario siblings) in Test::Candidates; WorklistTarget#lint_path = [target, *lint_paths]; project-config emits path= and accepts --path only; missing-directory and missing-path: validation messages per AC 18-20; cc-os .sdlc/project.yaml migrated; charters and skills reworded; plugins refreshed; History row added. **Evidence:** Pipeline implementation 19, 19 dispatches, implementation_complete (change-auditor, seam-designer, 4 test-writer/test-reviewer rounds, programmer + 3 repair, behavior-verifier pass, suite-check pass x2, contract-auditor -> refactorer -> pass). Commits f01d5984 (ADR-0210), e38e71b1 (hand-finished stale tests, #665), 753d1123 (implementation), merge commit on main 157da599. os-sdlc suite after merge: 1419 runs, 3219 assertions, 0 failures, 0 errors, 40.8 s. Ticket checks 11 and 12 pass (no code_path/root_path in .sdlc/project.yaml, agents, or skills). Deviation write-up: https://forgejo.swansoncloud.com/jared/cc-os/issues/655#issuecomment-7196 **Follow-ups:** Pipeline defects cited: #665 (no repair route for red test-file diffs; hit again here on ~20 stale tests outside the Retired list), #670 (gate output cut at 4000 chars blinds test-reviewer; filed this session). Cross-project: jaredswanson/cc-plugins#74 (migrate named targets to path:). Retired-tests check cannot detect missing entries: dropped, absorbed into #665's route discussion. #629 closes with this ticket per its Problem statement.
jared closed this issue 2026-09-24 14:30:28 +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#655
No description provided.