Pipeline briefs read the caller's directory, not the bound worktree root #1164

Closed
opened 2026-10-19 18:34:46 +00:00 by jared · 9 comments
Owner

Problem

os-sdlc-brief derives the project root from the caller's cwd (boot/inputs.rb:10 Input.new(dir: io.cwd) -> boot/project_bind.rb:11 RootSearch.outermost(dir)). A subagent orchestrator runs in the main checkout, so dispatch/brief/facts.rb:118 and :122 read test_changes and code_changes from the main checkout, not the bound worktree. Hook admission (admission.rb:7) and the submit path have the same cwd dependence.

Example: implementation 78 (ticket #1159, dispatch 734) bound worktree /home/jared/dev/cc-os/.claude/worktrees/crc-token-levers. The test-reviewer got MISSING: test_changes. and verdict:error.

Ruling

Option C, decided AFK by the orchestrator on 2026-10-27 (reversible). Store the worktree root on the implementations row at implementation open. ADR-0055 scopes "stores NO paths" to .sdlc/project.yaml: "Project.load(dir) walks up ... to find .sdlc/project.yaml, and root is the directory containing that .sdlc/". It does not govern pipeline.db. No superseding ADR. Option A (match branch ticket-<n>) fails because the #1159 branch is crc-token-levers. Option B is not needed.

Approach

Slice 1 of 2. Store the bound root on the implementations row and make the brief read it. Every other cwd-dependent path moves in #1436.

Add a nullable worktree_root column to implementations. Implementation::Open writes the root it bound: Open#root (implementation/open.rb:25), which Boot::ProjectBind#call found with Project::RootSearch.outermost(cwd). That search stops at the git top level, so an open from W/lib stores W, not W/lib. Implementation#work_root(fallback) returns worktree_root, or fallback when the column is null. Dispatch::Brief.for builds the project from implementation.work_root(root), so Brief::Facts (facts.rb:118, :122) reads test_changes and code_changes from the stored worktree. Brief.for is the one composition point for both os-sdlc-brief (Dispatch::BriefLookup) and the SubagentStart brief payload (Dispatch::SubagentStart#hook_payload), so both briefs get the stored root. Admission's other root uses stay in #1436.

Migration mechanism: today Db.file_db archives and recreates a file whose PRAGMA user_version differs from Db::SCHEMA_VERSION (30), and refuses while an implementation is live. A version bump is therefore destructive, and a worktree binary that bumps it wipes the shared .sdlc/pipeline.db through the symlink (ADR-0253). Do not bump SCHEMA_VERSION. Instead:

  • Db.add_implementation_binding_columns adds t.String :worktree_root (nullable, no default), so new files and :memory: databases have the column.
  • Db.file_db, after the stale-version check, runs db.alter_table(:implementations) { add_column :worktree_root, String } only when db[:implementations].columns lacks :worktree_root. SQLite ADD COLUMN is additive: it keeps every row and sets the new column to NULL.
  • The step is idempotent and needs no version change, so no binary (main or worktree) takes the archive path because of it. A binary at HEAD that opens a file with the extra column ignores the column.

ADR check done: /os-adr:create judged the column a habit (only os-sdlc reads it); ADR-0109 (SQLite via Sequel) allows it. ADR-0055 is not affected (see Ruling).

Required work:

  1. Db.add_implementation_binding_columns adds a nullable worktree_root String column (proven by AC 1).
  2. Db.file_db adds worktree_root to an existing current-version file that lacks it, with alter_table ... add_column, and keeps its rows (proven by AC 1).
  3. Keep the schema version unchanged — check: present SCHEMA_VERSION = 30 in lib/os_sdlc/db.rb
  4. Implementation::Open#open_implementation passes worktree_root: root to Implementation.open (proven by AC 2).
  5. Implementation::Targeting adds work_root(fallback): the stored worktree_root, else fallback (proven by AC 3).
  6. Dispatch::Brief.for builds the project from implementation.work_root(root) (proven by AC 3).
  7. Add the three test files in the Owner sketch.

Acceptance criteria

  1. Given a .sdlc/pipeline.db file stamped with the current user_version whose implementations table has no worktree_root column and holds one row, when Db.open opens it, then the table has a worktree_root column, the row is still there, and no pipeline.db.stale file is made.
  2. Given a git repository W with .sdlc/project.yaml at its top level, when implementation-open runs with cwd W/lib, then the new implementations row has worktree_root equal to W.
  3. Given an implementation opened in worktree W, a pending dispatch whose node enriches test_changes, and an uncommitted test file test/foo_test.rb in W, when os-sdlc-brief --dispatch-id <id> runs with cwd set to a different checkout M that has no changes, then the brief's test_changes names foo_test.rb.

Owner sketch

Object The one job it owns in this slice
Db Owns the pipeline schema: creates implementations.worktree_root in new databases, and adds it in place to an existing current-version file that lacks it, without a version bump.
Implementation::Open Records the root it bound (Open#root) as worktree_root when it creates the row.
Implementation (Implementation::Targeting) Owns the row's root: work_root(fallback) answers the stored worktree_root, else the fallback.
Dispatch::Brief.for Builds the brief's project from implementation.work_root(root) instead of the caller's root.

No new object: Implementation owns the column data, and Db owns the schema.

tests/db_worktree_root_column_test.rb

# frozen_string_literal: true

require_relative "test_helper"

module OsSdlc
  class DbWorktreeRootColumnTest < TmpdirTestCase
    def setup
      super
      @path = File.join(@dir, ".sdlc", "pipeline.db")
      seed_current_version_without_worktree_root
    end

    def test_opening_a_current_version_file_adds_worktree_root_and_keeps_its_rows
      db = Db.open(@path)

      assert_includes db[:implementations].columns, :worktree_root
      assert_equal ["session-1"], db[:implementations].select_map(:session_id)
    end

    private

    def seed_current_version_without_worktree_root
      FileUtils.mkdir_p(File.dirname(@path))
      db = Db.open_sqlite(@path)
      db.create_table(:implementations) do
        primary_key :id
        String :session_id, null: false
        DateTime :completed_at
      end
      db[:implementations].insert(session_id: "session-1", completed_at: Time.now)
      db.run("PRAGMA user_version = #{Db::SCHEMA_VERSION}")
    ensure
      db&.disconnect
    end
  end
end

tests/pipeline/cli_implementation_open_worktree_root_test.rb

# frozen_string_literal: true

require_relative "../test_helper"

module OsSdlc
  class CliImplementationOpenWorktreeRootTest < TmpdirTestCase
    def setup
      super
      ProjectScenario.new(@dir, name: "fixture").git_init
      scaffold_nested_project(@dir, name: "fixture")
      FileUtils.mkdir_p(File.join(@dir, "tests"))
      TicketFixture.seed(@dir, tickets: ["442"])
    end

    def test_open_from_a_subdirectory_stores_the_worktree_root_not_the_cwd
      _out, err, status = InProcessDispatchRunner.call(File.join(@dir, "lib"), "implementation-open",
                                                       "session-1", "442", fixture_map("toy"))

      assert_predicate status, :success?, "expected success, stderr: #{err}"
      assert_equal @dir, Db.db[:implementations].where(session_id: "session-1").first[:worktree_root]
    end
  end
end

tests/pipeline/cli_brief_worktree_root_test.rb

# frozen_string_literal: true

require_relative "../test_helper"

module OsSdlc
  class CliBriefWorktreeRootTest < TmpdirTestCase
    MAP = <<~YAML
      entry: verify
      nodes:
        verify:
          agent: programmer
          instructions: check the change
          enrich: [test_changes]
          verdict:
            source: gate
            gates: [test]
          edges:
            pass: done
            fail: { to: error }
            error: error
            escalate: escalate
        error:
          agent: null
          edges:
            default: failed
        escalate:
          agent: null
          edges:
            default: needs-orchestrator
        needs-orchestrator:
          agent: null
          waiting: true
          edges: {}
      terminals:
        done: implementation_complete
        failed: implementation_failed
    YAML

    def setup
      super
      @main = File.join(@dir, "main")
      @worktree = File.join(@dir, "worktree")
      ChangedProjectFixture.new(@main)
      @changed = ChangedProjectFixture.new(@worktree)
      [@main, @worktree].product(%w[lib test]).each { |root, sub| FileUtils.mkdir_p(File.join(root, sub)) }
      TicketFixture.seed(@worktree, tickets: ["442"])
      install_map
    end

    def test_brief_run_from_the_main_checkout_reads_test_changes_from_the_stored_worktree
      dispatch_id = open_and_dispatch_in(@worktree)
      @changed.write_test_file

      out, err, status = InProcessDispatchRunner.call(@main, "brief", "--dispatch-id", dispatch_id.to_s)

      assert_predicate status, :success?, "expected success, stderr: #{err}"
      assert_match(/foo_test\.rb/, JSON.parse(out)["test_changes"].to_s)
    end

    private

    def install_map
      maps_dir = File.join(@dir, "maps")
      FileUtils.mkdir_p(maps_dir)
      File.write(File.join(maps_dir, "verify.yaml"), MAP)
      MapCatalog.default = MapCatalog.new(maps_dir, drafts_dir: tmp_drafts_dir)
    end

    def open_and_dispatch_in(root)
      InProcessDispatchRunner.call(root, "implementation-open", "session-1", "442", "verify")
      out, = InProcessDispatchRunner.call(root, "subagent-start", "session-1", "programmer")
      JSON.parse(out).fetch("dispatch_id")
    end
  end
end

Red at HEAD (run on 2026-10-27, main bb8ff3a3), each for the intended reason:

  • test_opening_a_current_version_file_adds_worktree_root_and_keeps_its_rows: Expected [:id, :session_id, :completed_at] to include :worktree_root.
  • test_open_from_a_subdirectory_stores_the_worktree_root_not_the_cwd: expected the tmpdir path, actual nil.
  • test_brief_run_from_the_main_checkout_reads_test_changes_from_the_stored_worktree: Expected /foo_test\.rb/ to match "".

All three pass against a spike of Required work 1, 2, 4 and 6 (no code committed).

Out of scope

  • Hook admission's other root uses (admission.rb:7, boot.rb:57), the submit path, gate runs (X3), and Dispatch::Settlement.build_repo (X4): #1436.
  • Superseding or editing ADR-0055.
  • Deriving the root with git worktree list.
  • Changing how worktree creates worktrees.
  • Backfilling worktree_root for closed implementations.
## Problem `os-sdlc-brief` derives the project root from the caller's cwd (`boot/inputs.rb:10` `Input.new(dir: io.cwd)` -> `boot/project_bind.rb:11` `RootSearch.outermost(dir)`). A subagent orchestrator runs in the main checkout, so `dispatch/brief/facts.rb:118` and `:122` read `test_changes` and `code_changes` from the main checkout, not the bound worktree. Hook admission (`admission.rb:7`) and the submit path have the same cwd dependence. Example: implementation 78 (ticket #1159, dispatch 734) bound worktree `/home/jared/dev/cc-os/.claude/worktrees/crc-token-levers`. The test-reviewer got `MISSING: test_changes.` and `verdict:error`. ## Ruling Option C, decided AFK by the orchestrator on 2026-10-27 (reversible). Store the worktree root on the `implementations` row at implementation open. ADR-0055 scopes "stores NO paths" to `.sdlc/project.yaml`: "Project.load(dir) walks up ... to find .sdlc/project.yaml, and root is the directory containing that .sdlc/". It does not govern `pipeline.db`. No superseding ADR. Option A (match branch `ticket-<n>`) fails because the #1159 branch is `crc-token-levers`. Option B is not needed. ## Approach Slice 1 of 2. Store the bound root on the `implementations` row and make the brief read it. Every other cwd-dependent path moves in #1436. Add a nullable `worktree_root` column to `implementations`. `Implementation::Open` writes the root it bound: `Open#root` (`implementation/open.rb:25`), which `Boot::ProjectBind#call` found with `Project::RootSearch.outermost(cwd)`. That search stops at the git top level, so an open from `W/lib` stores `W`, not `W/lib`. `Implementation#work_root(fallback)` returns `worktree_root`, or `fallback` when the column is null. `Dispatch::Brief.for` builds the project from `implementation.work_root(root)`, so `Brief::Facts` (`facts.rb:118`, `:122`) reads `test_changes` and `code_changes` from the stored worktree. `Brief.for` is the one composition point for both `os-sdlc-brief` (`Dispatch::BriefLookup`) and the SubagentStart brief payload (`Dispatch::SubagentStart#hook_payload`), so both briefs get the stored root. Admission's other root uses stay in #1436. Migration mechanism: today `Db.file_db` archives and recreates a file whose `PRAGMA user_version` differs from `Db::SCHEMA_VERSION` (30), and refuses while an implementation is live. A version bump is therefore destructive, and a worktree binary that bumps it wipes the shared `.sdlc/pipeline.db` through the symlink (ADR-0253). Do not bump `SCHEMA_VERSION`. Instead: - `Db.add_implementation_binding_columns` adds `t.String :worktree_root` (nullable, no default), so new files and `:memory:` databases have the column. - `Db.file_db`, after the stale-version check, runs `db.alter_table(:implementations) { add_column :worktree_root, String }` only when `db[:implementations].columns` lacks `:worktree_root`. SQLite `ADD COLUMN` is additive: it keeps every row and sets the new column to NULL. - The step is idempotent and needs no version change, so no binary (main or worktree) takes the archive path because of it. A binary at HEAD that opens a file with the extra column ignores the column. ADR check done: `/os-adr:create` judged the column a habit (only os-sdlc reads it); ADR-0109 (SQLite via Sequel) allows it. ADR-0055 is not affected (see Ruling). Required work: 1. `Db.add_implementation_binding_columns` adds a nullable `worktree_root` String column (proven by AC 1). 2. `Db.file_db` adds `worktree_root` to an existing current-version file that lacks it, with `alter_table ... add_column`, and keeps its rows (proven by AC 1). 3. Keep the schema version unchanged — check: present `SCHEMA_VERSION = 30` in `lib/os_sdlc/db.rb` 4. `Implementation::Open#open_implementation` passes `worktree_root: root` to `Implementation.open` (proven by AC 2). 5. `Implementation::Targeting` adds `work_root(fallback)`: the stored `worktree_root`, else `fallback` (proven by AC 3). 6. `Dispatch::Brief.for` builds the project from `implementation.work_root(root)` (proven by AC 3). 7. Add the three test files in the Owner sketch. ## Acceptance criteria 1. Given a `.sdlc/pipeline.db` file stamped with the current `user_version` whose `implementations` table has no `worktree_root` column and holds one row, when `Db.open` opens it, then the table has a `worktree_root` column, the row is still there, and no `pipeline.db.stale` file is made. 2. Given a git repository W with `.sdlc/project.yaml` at its top level, when `implementation-open` runs with cwd `W/lib`, then the new `implementations` row has `worktree_root` equal to W. 3. Given an implementation opened in worktree W, a pending dispatch whose node enriches `test_changes`, and an uncommitted test file `test/foo_test.rb` in W, when `os-sdlc-brief --dispatch-id <id>` runs with cwd set to a different checkout M that has no changes, then the brief's `test_changes` names `foo_test.rb`. ## Owner sketch | Object | The one job it owns in this slice | |---|---| | `Db` | Owns the pipeline schema: creates `implementations.worktree_root` in new databases, and adds it in place to an existing current-version file that lacks it, without a version bump. | | `Implementation::Open` | Records the root it bound (`Open#root`) as `worktree_root` when it creates the row. | | `Implementation` (`Implementation::Targeting`) | Owns the row's root: `work_root(fallback)` answers the stored `worktree_root`, else the fallback. | | `Dispatch::Brief.for` | Builds the brief's project from `implementation.work_root(root)` instead of the caller's root. | No new object: `Implementation` owns the column data, and `Db` owns the schema. `tests/db_worktree_root_column_test.rb` ```ruby # frozen_string_literal: true require_relative "test_helper" module OsSdlc class DbWorktreeRootColumnTest < TmpdirTestCase def setup super @path = File.join(@dir, ".sdlc", "pipeline.db") seed_current_version_without_worktree_root end def test_opening_a_current_version_file_adds_worktree_root_and_keeps_its_rows db = Db.open(@path) assert_includes db[:implementations].columns, :worktree_root assert_equal ["session-1"], db[:implementations].select_map(:session_id) end private def seed_current_version_without_worktree_root FileUtils.mkdir_p(File.dirname(@path)) db = Db.open_sqlite(@path) db.create_table(:implementations) do primary_key :id String :session_id, null: false DateTime :completed_at end db[:implementations].insert(session_id: "session-1", completed_at: Time.now) db.run("PRAGMA user_version = #{Db::SCHEMA_VERSION}") ensure db&.disconnect end end end ``` `tests/pipeline/cli_implementation_open_worktree_root_test.rb` ```ruby # frozen_string_literal: true require_relative "../test_helper" module OsSdlc class CliImplementationOpenWorktreeRootTest < TmpdirTestCase def setup super ProjectScenario.new(@dir, name: "fixture").git_init scaffold_nested_project(@dir, name: "fixture") FileUtils.mkdir_p(File.join(@dir, "tests")) TicketFixture.seed(@dir, tickets: ["442"]) end def test_open_from_a_subdirectory_stores_the_worktree_root_not_the_cwd _out, err, status = InProcessDispatchRunner.call(File.join(@dir, "lib"), "implementation-open", "session-1", "442", fixture_map("toy")) assert_predicate status, :success?, "expected success, stderr: #{err}" assert_equal @dir, Db.db[:implementations].where(session_id: "session-1").first[:worktree_root] end end end ``` `tests/pipeline/cli_brief_worktree_root_test.rb` ```ruby # frozen_string_literal: true require_relative "../test_helper" module OsSdlc class CliBriefWorktreeRootTest < TmpdirTestCase MAP = <<~YAML entry: verify nodes: verify: agent: programmer instructions: check the change enrich: [test_changes] verdict: source: gate gates: [test] edges: pass: done fail: { to: error } error: error escalate: escalate error: agent: null edges: default: failed escalate: agent: null edges: default: needs-orchestrator needs-orchestrator: agent: null waiting: true edges: {} terminals: done: implementation_complete failed: implementation_failed YAML def setup super @main = File.join(@dir, "main") @worktree = File.join(@dir, "worktree") ChangedProjectFixture.new(@main) @changed = ChangedProjectFixture.new(@worktree) [@main, @worktree].product(%w[lib test]).each { |root, sub| FileUtils.mkdir_p(File.join(root, sub)) } TicketFixture.seed(@worktree, tickets: ["442"]) install_map end def test_brief_run_from_the_main_checkout_reads_test_changes_from_the_stored_worktree dispatch_id = open_and_dispatch_in(@worktree) @changed.write_test_file out, err, status = InProcessDispatchRunner.call(@main, "brief", "--dispatch-id", dispatch_id.to_s) assert_predicate status, :success?, "expected success, stderr: #{err}" assert_match(/foo_test\.rb/, JSON.parse(out)["test_changes"].to_s) end private def install_map maps_dir = File.join(@dir, "maps") FileUtils.mkdir_p(maps_dir) File.write(File.join(maps_dir, "verify.yaml"), MAP) MapCatalog.default = MapCatalog.new(maps_dir, drafts_dir: tmp_drafts_dir) end def open_and_dispatch_in(root) InProcessDispatchRunner.call(root, "implementation-open", "session-1", "442", "verify") out, = InProcessDispatchRunner.call(root, "subagent-start", "session-1", "programmer") JSON.parse(out).fetch("dispatch_id") end end end ``` Red at HEAD (run on 2026-10-27, main bb8ff3a3), each for the intended reason: - `test_opening_a_current_version_file_adds_worktree_root_and_keeps_its_rows`: `Expected [:id, :session_id, :completed_at] to include :worktree_root.` - `test_open_from_a_subdirectory_stores_the_worktree_root_not_the_cwd`: expected the tmpdir path, actual `nil`. - `test_brief_run_from_the_main_checkout_reads_test_changes_from_the_stored_worktree`: `Expected /foo_test\.rb/ to match "".` All three pass against a spike of Required work 1, 2, 4 and 6 (no code committed). ## Out of scope - Hook admission's other root uses (`admission.rb:7`, `boot.rb:57`), the submit path, gate runs (X3), and `Dispatch::Settlement.build_repo` (X4): #1436. - Superseding or editing ADR-0055. - Deriving the root with `git worktree list`. - Changing how `worktree` creates worktrees. - Backfilling `worktree_root` for closed implementations.
Author
Owner

Occurrence: Briefs and submits read the implementation's recorded worktree, not the hook cwd
Seen: plugins/os-sdlc/lib/os_sdlc/dispatch/admission.rb:7 (event.cwd) -> subagent_start.rb:30 -> brief.rb:7 -> project/snapshot.rb -> brief/facts.rb:91,97; submit side boot/inputs.rb:33-38 and hook_reading.rb:22
Run: session c843bd27-b3bf-4e36-9243-df1697457146; takedown runs docs/takedown/1415-20261027-123629 and docs/takedown/1415-20261027-125443; implementation 178 of #1111
Adds: Two candidate designs (worktree_root column on implementations, graded cause_removed, with open defects X3 gate runs, X4 Settlement.build_repo, X5 open records any cwd; or a root key in project_config JSON, graded defect_remains), the shared pipeline.db migration risk (additive, apply from main checkout, run /os-adr:find on ADR-0109), and the next step: re-run /os-sdlc:ticket-takedown #1164 after the tickets_bin_test.rb fix lands.

Occurrence: Briefs and submits read the implementation's recorded worktree, not the hook cwd Seen: plugins/os-sdlc/lib/os_sdlc/dispatch/admission.rb:7 (event.cwd) -> subagent_start.rb:30 -> brief.rb:7 -> project/snapshot.rb -> brief/facts.rb:91,97; submit side boot/inputs.rb:33-38 and hook_reading.rb:22 Run: session c843bd27-b3bf-4e36-9243-df1697457146; takedown runs docs/takedown/1415-20261027-123629 and docs/takedown/1415-20261027-125443; implementation 178 of #1111 Adds: Two candidate designs (worktree_root column on implementations, graded cause_removed, with open defects X3 gate runs, X4 Settlement.build_repo, X5 open records any cwd; or a root key in project_config JSON, graded defect_remains), the shared pipeline.db migration risk (additive, apply from main checkout, run /os-adr:find on ADR-0109), and the next step: re-run /os-sdlc:ticket-takedown #1164 after the tickets_bin_test.rb fix lands.
Author
Owner

Takedown relevance: needs a ruling

Harm: A pipeline agent that runs os-sdlc-brief from a directory outside the implementation's worktree gets a dispatch brief computed from the main checkout, so test_changes and code_changes come back empty and the test-reviewer stops with verdict error.
Protects: Briefs must describe the change the implementation actually produced, so each pipeline agent judges the real diff and the pipeline can run from a subagent orchestrator without a human re-running it by hand.
Harm path: The os-sdlc-brief CLI (bin/os-sdlc-brief) resolves its project root from the caller's cwd via ProjectBind and RootSearch, and Facts then reads test_changes and code_changes from that root. implementation-open also derives its root from the caller's cwd and stores no worktree path, so the brief reads whatever checkout the caller is in. (live: true)

Evidence:

  • plugins/os-sdlc/bin/os-sdlc-brief:6: The brief entry point runs Boot.run("brief") with the process cwd, with no root argument.
  • plugins/os-sdlc/lib/os_sdlc/boot/inputs.rb:10-14: Inputs.brief sets dir: io.cwd and no root, so the brief root comes from the caller's working directory.
  • plugins/os-sdlc/lib/os_sdlc/boot/commands.rb:14: The "brief" command is still registered and calls Dispatch::Briefing#brief(root, dispatch_id).
  • plugins/os-sdlc/lib/os_sdlc/boot/project_bind.rb:11: ProjectBind.call resolves the root with Project::RootSearch.outermost(dir), which returns the checkout that contains the cwd.
  • plugins/os-sdlc/lib/os_sdlc/project/root_search.rb:41: boundary? stops the search at the git top level, so a worktree and the main checkout each resolve to their own root.
  • plugins/os-sdlc/lib/os_sdlc/dispatch/brief/facts.rb:118-122: changes and codebase are built from @project.root, so test_changes and code_changes read the root that was passed in.
  • plugins/os-sdlc/lib/os_sdlc/implementation/open.rb:25: The implementation's root is @project.root.to_s, which comes from the caller's cwd. Nothing stores a worktree path.
  • docs/adr/0055-os-sdlc-project-attributes-persist-in-a-tracked-sdlc-project-yaml-root-is-derived-from-its-location-never-stored.md:19: Accepted ADR: the root is derived from the location of .sdlc/project.yaml and is never stored.

Conflicting ADRs:

  • ADR-0055: A fix that records the bound worktree root (as the ticket's 'bound' wording implies) stores a root that ADR-0055 says is always derived from location and never stored.

The ticket is waiting for Jared's ruling on the ADRs above.

Takedown relevance: needs a ruling Harm: A pipeline agent that runs os-sdlc-brief from a directory outside the implementation's worktree gets a dispatch brief computed from the main checkout, so test_changes and code_changes come back empty and the test-reviewer stops with verdict error. Protects: Briefs must describe the change the implementation actually produced, so each pipeline agent judges the real diff and the pipeline can run from a subagent orchestrator without a human re-running it by hand. Harm path: The os-sdlc-brief CLI (bin/os-sdlc-brief) resolves its project root from the caller's cwd via ProjectBind and RootSearch, and Facts then reads test_changes and code_changes from that root. implementation-open also derives its root from the caller's cwd and stores no worktree path, so the brief reads whatever checkout the caller is in. (live: true) Evidence: - plugins/os-sdlc/bin/os-sdlc-brief:6: The brief entry point runs Boot.run("brief") with the process cwd, with no root argument. - plugins/os-sdlc/lib/os_sdlc/boot/inputs.rb:10-14: Inputs.brief sets dir: io.cwd and no root, so the brief root comes from the caller's working directory. - plugins/os-sdlc/lib/os_sdlc/boot/commands.rb:14: The "brief" command is still registered and calls Dispatch::Briefing#brief(root, dispatch_id). - plugins/os-sdlc/lib/os_sdlc/boot/project_bind.rb:11: ProjectBind.call resolves the root with Project::RootSearch.outermost(dir), which returns the checkout that contains the cwd. - plugins/os-sdlc/lib/os_sdlc/project/root_search.rb:41: boundary? stops the search at the git top level, so a worktree and the main checkout each resolve to their own root. - plugins/os-sdlc/lib/os_sdlc/dispatch/brief/facts.rb:118-122: changes and codebase are built from @project.root, so test_changes and code_changes read the root that was passed in. - plugins/os-sdlc/lib/os_sdlc/implementation/open.rb:25: The implementation's root is @project.root.to_s, which comes from the caller's cwd. Nothing stores a worktree path. - docs/adr/0055-os-sdlc-project-attributes-persist-in-a-tracked-sdlc-project-yaml-root-is-derived-from-its-location-never-stored.md:19: Accepted ADR: the root is derived from the location of .sdlc/project.yaml and is never stored. Conflicting ADRs: - ADR-0055: A fix that records the bound worktree root (as the ticket's 'bound' wording implies) stores a root that ADR-0055 says is always derived from location and never stored. The ticket is waiting for Jared's ruling on the ADRs above.
Author
Owner

Ruling: option C (decided AFK 2026-10-27)

  • Chosen: store the worktree root on the implementations row at open (earlier takedown Design 1, graded cause_removed).
  • ADR-0055 scopes "stores NO paths" to .sdlc/project.yaml; it does not govern pipeline.db, so no superseding ADR is needed.
  • Option A is rejected: the #1159 branch is crc-token-levers, not ticket-<n>, and ADR-0157 has no file in docs/adr.
  • Constraints: additive migration from the main checkout only; open defects X3, X4, X5 are in required work.
  • Made by the orchestrator under Jared's standing AFK instruction; reversible.
Ruling: option C (decided AFK 2026-10-27) - Chosen: store the worktree root on the `implementations` row at open (earlier takedown Design 1, graded cause_removed). - ADR-0055 scopes "stores NO paths" to `.sdlc/project.yaml`; it does not govern `pipeline.db`, so no superseding ADR is needed. - Option A is rejected: the #1159 branch is `crc-token-levers`, not `ticket-<n>`, and ADR-0157 has no file in docs/adr. - Constraints: additive migration from the main checkout only; open defects X3, X4, X5 are in required work. - Made by the orchestrator under Jared's standing AFK instruction; reversible.
Author
Owner

Takedown relevance: needs a ruling

Harm: A pipeline agent that runs os-sdlc-brief from the main checkout gets test_changes and code_changes read from that checkout, not the worktree the implementation is bound to, so the test-reviewer reports MISSING: test_changes and verdict error.
Protects: Pipeline briefs, hook admission, and submit must judge the work in the implementation's bound worktree, so that a review verdict reflects the code actually under review.
Harm path: The os-sdlc-brief command: Boot::Inputs.brief takes io.cwd as the directory, ProjectBind resolves the root with RootSearch.outermost, and Dispatch::Brief::Facts builds Changes and Codebase from @project.root for test_changes and code_changes. The same cwd-derived root feeds boot.rb:57 (pipeline_db? for hook launch). (live: true)

Evidence:

  • plugins/os-sdlc/lib/os_sdlc/boot/inputs.rb:14: Inputs.brief passes dir: io.cwd, so the brief root comes from the caller's working directory.
  • plugins/os-sdlc/lib/os_sdlc/boot/project_bind.rb:11: ProjectBind#call resolves the root with Project::RootSearch.outermost(dir) and binds it.
  • plugins/os-sdlc/lib/os_sdlc/dispatch/brief/facts.rb:118: Facts#changes builds Project::Changes from Git::Repo.new(@project.root.to_s), so test_changes and code_changes read the cwd-derived checkout.
  • plugins/os-sdlc/lib/os_sdlc/dispatch/brief/facts.rb:122: Facts#codebase uses bound_target.codebase(@project.root.to_s), the same cwd-derived root.
  • plugins/os-sdlc/lib/os_sdlc/boot.rb:57: pipeline_db? also derives the root with RootSearch.outermost(cwd), so the cwd dependence is present in the hook path too.
  • plugins/os-sdlc/lib/os_sdlc/db.rb:333: implementations binding columns are map_name, map_digest, position, target, project_config; no worktree_root or worktree column exists in the schema.
  • docs/adr/0253-implementation-open-places-the-worktree-start-picks-the-mode-finish-keep-integrates-persistent-worktrees-amends-adr-0151-adr-0157.md:32: Reused worktrees symlink .sdlc/pipeline.db to the primary checkout's copy, so the shared db is reachable from both checkouts; the bug remains because the root is still derived from cwd.
  • .takedown/history.txt: No commit subject names a bound-worktree root fix for the brief path; the only root commits are root_search and lib_root refactors and a test-changes map enrich change (18a4a82c).

Conflicting ADRs:

  • ADR-0055: ADR-0055 is titled 'root is derived from its location, never stored'; the ticket stores a worktree root in pipeline.db. The body scopes 'stores no paths' to .sdlc/project.yaml, so the conflict is arguable and the ticket's ruling says no conflict, but the title's wording is not settled by any later Accepted ADR.

The ticket is waiting for Jared's ruling on the ADRs above.

Takedown relevance: needs a ruling Harm: A pipeline agent that runs os-sdlc-brief from the main checkout gets test_changes and code_changes read from that checkout, not the worktree the implementation is bound to, so the test-reviewer reports MISSING: test_changes and verdict error. Protects: Pipeline briefs, hook admission, and submit must judge the work in the implementation's bound worktree, so that a review verdict reflects the code actually under review. Harm path: The os-sdlc-brief command: Boot::Inputs.brief takes io.cwd as the directory, ProjectBind resolves the root with RootSearch.outermost, and Dispatch::Brief::Facts builds Changes and Codebase from @project.root for test_changes and code_changes. The same cwd-derived root feeds boot.rb:57 (pipeline_db? for hook launch). (live: true) Evidence: - plugins/os-sdlc/lib/os_sdlc/boot/inputs.rb:14: Inputs.brief passes dir: io.cwd, so the brief root comes from the caller's working directory. - plugins/os-sdlc/lib/os_sdlc/boot/project_bind.rb:11: ProjectBind#call resolves the root with Project::RootSearch.outermost(dir) and binds it. - plugins/os-sdlc/lib/os_sdlc/dispatch/brief/facts.rb:118: Facts#changes builds Project::Changes from Git::Repo.new(@project.root.to_s), so test_changes and code_changes read the cwd-derived checkout. - plugins/os-sdlc/lib/os_sdlc/dispatch/brief/facts.rb:122: Facts#codebase uses bound_target.codebase(@project.root.to_s), the same cwd-derived root. - plugins/os-sdlc/lib/os_sdlc/boot.rb:57: pipeline_db? also derives the root with RootSearch.outermost(cwd), so the cwd dependence is present in the hook path too. - plugins/os-sdlc/lib/os_sdlc/db.rb:333: implementations binding columns are map_name, map_digest, position, target, project_config; no worktree_root or worktree column exists in the schema. - docs/adr/0253-implementation-open-places-the-worktree-start-picks-the-mode-finish-keep-integrates-persistent-worktrees-amends-adr-0151-adr-0157.md:32: Reused worktrees symlink .sdlc/pipeline.db to the primary checkout's copy, so the shared db is reachable from both checkouts; the bug remains because the root is still derived from cwd. - .takedown/history.txt: No commit subject names a bound-worktree root fix for the brief path; the only root commits are root_search and lib_root refactors and a test-changes map enrich change (18a4a82c). Conflicting ADRs: - ADR-0055: ADR-0055 is titled 'root is derived from its location, never stored'; the ticket stores a worktree root in pipeline.db. The body scopes 'stores no paths' to .sdlc/project.yaml, so the conflict is arguable and the ticket's ruling says no conflict, but the title's wording is not settled by any later Accepted ADR. The ticket is waiting for Jared's ruling on the ADRs above.
Author
Owner

ADR check (2026-10-27, AFK): /os-adr:create rejected an ADR for the worktree_root column. Judge verdict, verbatim: "The new worktree_root column on implementations is read only by os-sdlc's own code (brief, admission, submit, open). A column that only one plugin reads is a habit, because the reversing commit can drop it and change those readers. ... Record this nowhere; put the reason in the commit message." So ADR-0055's one-way-door content does not cover this column: ADR-0055's decision text governs .sdlc/project.yaml only ("It stores NO paths"), and the column is a habit, not ADR content. Logged in docs/decision-system/afk-adr-log.md.

ADR check (2026-10-27, AFK): /os-adr:create rejected an ADR for the worktree_root column. Judge verdict, verbatim: "The new worktree_root column on implementations is read only by os-sdlc's own code (brief, admission, submit, open). A column that only one plugin reads is a habit, because the reversing commit can drop it and change those readers. ... Record this nowhere; put the reason in the commit message." So ADR-0055's one-way-door content does not cover this column: ADR-0055's decision text governs .sdlc/project.yaml only ("It stores NO paths"), and the column is a habit, not ADR content. Logged in docs/decision-system/afk-adr-log.md.
Author
Owner

Takedown relevance: needs a ruling

Harm: A pipeline brief run from the main checkout reads test_changes and code_changes from the main checkout instead of the implementation's bound worktree, so the test-reviewer receives MISSING: test_changes and settles error.
Protects: Pipeline agents judge the change that was actually made in the bound worktree, so verdicts and gates reflect the real implementation rather than the caller's checkout.
Harm path: os-sdlc-brief --dispatch-id N (bin/os-sdlc-brief:6) enters Boot.brief, which builds Input from io.cwd (boot/inputs.rb:14), then ProjectBind derives the root with RootSearch.outermost(cwd) (boot/project_bind.rb:11), and Dispatch::Brief::Facts reads changes from that root (dispatch/brief/facts.rb:118, :122). (live: true)

Evidence:

  • plugins/os-sdlc/bin/os-sdlc-brief:6: the executable routes straight to Boot.run("brief", ...), so the command is registered and reachable today.
  • plugins/os-sdlc/lib/os_sdlc/boot/inputs.rb:14: Inputs.brief builds Input.new(dir: io.cwd, ...), so the brief root comes from the caller's working directory.
  • plugins/os-sdlc/lib/os_sdlc/boot/project_bind.rb:11: ProjectBind#call derives root = Project::RootSearch.outermost(dir) from that cwd.
  • plugins/os-sdlc/lib/os_sdlc/dispatch/brief/facts.rb:118: changes is Project::Changes.new(codebase, Git::Repo.new(@project.root.to_s)), which reads the root derived from the caller's cwd.
  • plugins/os-sdlc/lib/os_sdlc/dispatch/brief/facts.rb:122: codebase uses bound_target.codebase(@project.root.to_s), so the target codebase is also resolved against that root.
  • plugins/os-sdlc/skills/responsibility-refactor/SKILL.md:86: The skill tells operators to run agents with cwd at the worktree root, which confirms the cwd dependence still exists in current use.
  • docs/decision-system/afk-adr-log.md:13: The AFK log row for #1164 is still Open, so no ruling has landed that removes the cwd path.
  • .takedown/history.txt:1: The newest commit is 76880dcc (2026-10-27, the AFK ADR log row for #1164), with no later commit that removes the cwd root derivation.

Conflicting ADRs:

  • ADR-0055: ADR-0055 is Accepted and titled 'root is derived from its location, never stored'; storing a worktree_root column on implementations reverses that stored-root stance, though the ADR's decision text names only project.yaml.

The ticket is waiting for Jared's ruling on the ADRs above.

Takedown relevance: needs a ruling Harm: A pipeline brief run from the main checkout reads test_changes and code_changes from the main checkout instead of the implementation's bound worktree, so the test-reviewer receives MISSING: test_changes and settles error. Protects: Pipeline agents judge the change that was actually made in the bound worktree, so verdicts and gates reflect the real implementation rather than the caller's checkout. Harm path: `os-sdlc-brief --dispatch-id N` (bin/os-sdlc-brief:6) enters Boot.brief, which builds Input from io.cwd (boot/inputs.rb:14), then ProjectBind derives the root with RootSearch.outermost(cwd) (boot/project_bind.rb:11), and Dispatch::Brief::Facts reads changes from that root (dispatch/brief/facts.rb:118, :122). (live: true) Evidence: - plugins/os-sdlc/bin/os-sdlc-brief:6: the executable routes straight to Boot.run("brief", ...), so the command is registered and reachable today. - plugins/os-sdlc/lib/os_sdlc/boot/inputs.rb:14: Inputs.brief builds Input.new(dir: io.cwd, ...), so the brief root comes from the caller's working directory. - plugins/os-sdlc/lib/os_sdlc/boot/project_bind.rb:11: ProjectBind#call derives root = Project::RootSearch.outermost(dir) from that cwd. - plugins/os-sdlc/lib/os_sdlc/dispatch/brief/facts.rb:118: changes is Project::Changes.new(codebase, Git::Repo.new(@project.root.to_s)), which reads the root derived from the caller's cwd. - plugins/os-sdlc/lib/os_sdlc/dispatch/brief/facts.rb:122: codebase uses bound_target.codebase(@project.root.to_s), so the target codebase is also resolved against that root. - plugins/os-sdlc/skills/responsibility-refactor/SKILL.md:86: The skill tells operators to run agents with cwd at the worktree root, which confirms the cwd dependence still exists in current use. - docs/decision-system/afk-adr-log.md:13: The AFK log row for #1164 is still Open, so no ruling has landed that removes the cwd path. - .takedown/history.txt:1: The newest commit is 76880dcc (2026-10-27, the AFK ADR log row for #1164), with no later commit that removes the cwd root derivation. Conflicting ADRs: - ADR-0055: ADR-0055 is Accepted and titled 'root is derived from its location, never stored'; storing a worktree_root column on implementations reverses that stored-root stance, though the ADR's decision text names only project.yaml. The ticket is waiting for Jared's ruling on the ADRs above.
Author
Owner

Takedown relevance blocked twice on ADR-0055 (runs docs/takedown/1164-20261027-134934 and 1164-20261027-135509), even after /os-adr:create judged the column a habit. Proceeding with /os-sdlc:implement 1164 on the current body. Relevance defect filed separately.

Takedown relevance blocked twice on ADR-0055 (runs docs/takedown/1164-20261027-134934 and 1164-20261027-135509), even after /os-adr:create judged the column a habit. Proceeding with /os-sdlc:implement 1164 on the current body. Relevance defect filed separately.
Author
Owner

Work started on branch ticket-1164 via /os-sdlc:implement (poodr-ticket-implementation), slice 1 per the current body. Slice 2 is #1436.

Work started on branch ticket-1164 via /os-sdlc:implement (poodr-ticket-implementation), slice 1 per the current body. Slice 2 is #1436.
Author
Owner

Resolution

Done: implementation-open stores the bound git top level in implementations.worktree_root (added in place, schema version unchanged, double-checked under an immediate transaction). Brief.for builds the brief project from the stored root and falls back to the caller root. A brief run from the main checkout now reads test_changes and code_changes from the bound worktree.

Evidence: main d2f8a213 (merged via worktree finish, fast-forward to 71e1818e). Tests: db_worktree_root_column_test, cli_implementation_open_worktree_root_test, cli_brief_worktree_root_test. os-sdlc suite 2790 runs, 0 failures. Rubocop clean. Implementation 193 escalated (test defect: two Bound stubs lacked work_root). It was hand-finished per the escalation procedure, with an opus design audit (no must-fix; the should-fix race is fixed). No ADR: /os-adr:create judged the column a habit. The AFK ruling is logged in docs/decision-system/afk-adr-log.md.

Follow-ups: #1436 (slice 2: admission, submit, gate runs, Settlement) already filed with skeptic verdict CREATE. #1435 (relevance slug) fixed and merged this session.

## Resolution **Done:** implementation-open stores the bound git top level in implementations.worktree_root (added in place, schema version unchanged, double-checked under an immediate transaction). Brief.for builds the brief project from the stored root and falls back to the caller root. A brief run from the main checkout now reads test_changes and code_changes from the bound worktree. **Evidence:** main d2f8a213 (merged via worktree finish, fast-forward to 71e1818e). Tests: db_worktree_root_column_test, cli_implementation_open_worktree_root_test, cli_brief_worktree_root_test. os-sdlc suite 2790 runs, 0 failures. Rubocop clean. Implementation 193 escalated (test defect: two Bound stubs lacked work_root). It was hand-finished per the escalation procedure, with an opus design audit (no must-fix; the should-fix race is fixed). No ADR: /os-adr:create judged the column a habit. The AFK ruling is logged in docs/decision-system/afk-adr-log.md. **Follow-ups:** #1436 (slice 2: admission, submit, gate runs, Settlement) already filed with skeptic verdict CREATE. #1435 (relevance slug) fixed and merged this session.
jared closed this issue 2026-10-27 18:30:29 +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#1164
No description provided.