implementation-open places the worktree through worktree start; payload tells the skill to enter, exit, and finish #846

Open
opened 2026-09-30 16:49:21 +00:00 by jared · 4 comments
Owner

Problem

The implement skill decides the worktree in prose before it opens the implementation (plugins/os-sdlc/skills/implement/SKILL.md:45). Nothing in code stops an implementation from running in the primary checkout on main. implementation-open binds the DB for its cwd; in a linked worktree without ADR-0143 links it silently creates a second pipeline.db. It also deletes retired test files under its cwd (command/implementation/open.rb:34).

This ticket is the os-sdlc part of ADR-0253: open places the worktree through the os plugin's worktree start, and the payload tells the skill what to do. It depends on #844, which adds the start modes and finish --keep.

Reproduce

n/a: design change; nothing fails today.

Approach

implementation-open binds the primary checkout's DB, then shells out once to worktree start ticket-<n> (found on PATH), then opens with the printed path as root. The payload gains a worktree block. os-sdlc's own adapter/git/worktree.rb is deleted; the os worktree CLI is the only worktree creator (ADR-0157).

Required work:

  1. In plugins/os-sdlc/lib/os_sdlc/controller/boot/inputs.rb, the implementation-open input binds the project in the primary checkout, not in the cwd: take the cwd's path relative to its git top-level, apply it under the primary checkout (parent of git rev-parse --git-common-dir), then resolve with RootSearch.outermost as today. Projects whose .sdlc/project.yaml sits below the git top-level (for example ~/dev/ruby-gems/<gem>, ~/dev/cc-plugins/api-wrapper) must keep working. The input also keeps the session cwd for the placement and for enter — check: grep -n "common-dir\|primary_root" plugins/os-sdlc/lib/os_sdlc/controller/boot/inputs.rb plugins/os-sdlc/lib/os_sdlc/adapter/git/repo.rb
  2. Add plugins/os-sdlc/lib/os_sdlc/adapter/worktree_placement.rb: finds worktree on env["PATH"], runs worktree start ticket-<n> with the session cwd, and parses mode, path, branch, finish. A missing CLI, a non-zero exit, or output without all four keys raises WorktreePlacement::Failed with the CLI's stderr or the missing key.
  3. DispatchCommands.implementation_open calls the placement with the session cwd before Implementation::Open.call, then passes the project root inside the printed worktree path (the printed path plus the same relative project path) as root to Open and Tracker::Tea.for, and turns WorktreePlacement::Failed into exit_loudly("implementation-open worktree: ...").
  4. Implementation::Open.call and OpenPayload.for take a worktree: value. The open payload gains worktree: { mode, path, branch, enter, exit, finish }: enter is true when the session cwd differs from path; exit is false only for mode: persistent; finish is the string start printed, unchanged.
  5. Delete plugins/os-sdlc/lib/os_sdlc/adapter/git/worktree.rb, and adapter/git/branch.rb when it has no other sender — check: ! test -e plugins/os-sdlc/lib/os_sdlc/adapter/git/worktree.rb
  6. Test support: tests/support/tmpdir_test_case.rb scaffold_sdlc_project makes a real repo (git init), not a bare .git directory; tests/support/in_process_dispatch_script.rb passes an env whose PATH holds a stub worktree that prints a ticket-continue report with path: set to the test directory. Existing CLI tests that run implementation-open keep passing.

Documentation, done by the orchestrator, not by a pipeline agent:

  • Amend ADR-0253's list with ADR-0250 (Boot binds from input.dir; open now binds the primary) and ADR-0188 (root search stops at the worktree top).
  • Rewrite plugins/os-sdlc/skills/implement/SKILL.md Setup, Start, and Finish; delete the /os:worktree start step (open now runs it): Start — if worktree.enter, call EnterWorktree(path: worktree.path); comment the ticket with worktree.branch; on "Merge conflicts in:" resolve with /mattpocock-skills:resolving-merge-conflicts, commit, re-run open. Finish — if worktree.exit, call ExitWorktree(action: keep); then run worktree.finish as printed.
  • Add one History row to docs/implementation-status/os-sdlc.md (or the owning leaf).
  • Run bin/refresh-plugins.

Rejected: detect the start state in the skill (agents can pick the wrong mode; ADR-0157); bind the cwd DB with a pre-bind check (two binds); call plugins/os/bin/worktree by repo path (fails outside cc-os).

Acceptance criteria

  1. Given worktree on PATH, when implementation-open runs for ticket 844, then it runs worktree start ticket-844 once, before it loads project.yaml or fetches the ticket.
  2. Given the session cwd is a linked worktree, when implementation-open runs, then the implementation row is written to the primary checkout's .sdlc/pipeline.db and no pipeline.db is created in the worktree.
  3. Given start prints mode, path, branch, and finish, when open succeeds, then the payload worktree block holds them and finish equals the printed string.
  4. Given the session cwd equals the printed path, when open succeeds, then worktree.enter is false; otherwise it is true.
  5. Given mode: persistent, when open succeeds, then worktree.exit is false; for the ticket modes it is true.
  6. Given a ticket that lists a retired test, when open runs, then the file is deleted under the printed path and the primary checkout's copy is unchanged.
  7. Given worktree is not on PATH, when open runs, then it exits non-zero, stderr starts with implementation-open worktree:, and no implementation row is created.
  8. Given start exits 1, when open runs, then it exits non-zero with start's stderr after implementation-open worktree:, and no implementation row is created.
  9. Given start exits 0 but prints no mode: line, when open runs, then it exits non-zero, stderr names mode, and no implementation row is created.
  10. Given the cwd is a linked worktree of a repository whose .sdlc/project.yaml sits in a subdirectory, when open runs from that subdirectory, then it binds the same subdirectory's DB in the primary checkout and uses the same subdirectory inside the worktree as root.

Retired tests

plugins/os-sdlc/tests/adapter/git/worktree_test.rb

Owner sketch

module OsSdlc
  class WorktreePlacement
    Failed = Class.new(HarnessError)
    Result = Data.define(:mode, :path, :branch, :finish)

    def start(cwd, name) = parse(run(cwd, name))
  end

  module DispatchCommands
    def self.implementation_open(root, order, env:, placement: WorktreePlacement.new(env: env))
      worktree = placement.start(cwd, "ticket-#{work_order.ticket}")
      Implementation::Open.call(worktree.path, work_order, tracker: Tracker::Tea.for(root: worktree.path, env: env), worktree: worktree)
    end
  end
end

Origin

  • Trigger: split from #844 during triage, 2026-09-30, after the user ruled that open places the worktree
  • Improvised this session: none
  • Chain: skill prose orders worktree before open ← implementation-open binds cwd and has no branch guard ← DESIGN: ADR-0157, ADR-0213
  • Root candidate: this ticket
  • Where: plugins/os-sdlc/lib/os_sdlc/controller/boot/inputs.rb; command/dispatch_commands.rb; command/implementation/open.rb; presenter/implementation/open_payload.rb; adapter/git/worktree.rb; skills/implement/SKILL.md
  • Session: f531ed01-125b-49b4-b35a-43528beff19e
  • Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/f531ed01-125b-49b4-b35a-43528beff19e.jsonl

Blocked by: #844.

VERDICT: CREATE — user-requested hard floor; split from #844 which carries ADR-0253 (2026-09-30).

## Problem The implement skill decides the worktree in prose before it opens the implementation (`plugins/os-sdlc/skills/implement/SKILL.md:45`). Nothing in code stops an implementation from running in the primary checkout on main. `implementation-open` binds the DB for its cwd; in a linked worktree without ADR-0143 links it silently creates a second `pipeline.db`. It also deletes retired test files under its cwd (`command/implementation/open.rb:34`). This ticket is the os-sdlc part of ADR-0253: open places the worktree through the os plugin's `worktree start`, and the payload tells the skill what to do. It depends on #844, which adds the start modes and `finish --keep`. ## Reproduce n/a: design change; nothing fails today. ## Approach `implementation-open` binds the primary checkout's DB, then shells out once to `worktree start ticket-<n>` (found on PATH), then opens with the printed path as root. The payload gains a `worktree` block. os-sdlc's own `adapter/git/worktree.rb` is deleted; the os worktree CLI is the only worktree creator (ADR-0157). Required work: 1. In `plugins/os-sdlc/lib/os_sdlc/controller/boot/inputs.rb`, the `implementation-open` input binds the project in the primary checkout, not in the cwd: take the cwd's path relative to its git top-level, apply it under the primary checkout (parent of `git rev-parse --git-common-dir`), then resolve with `RootSearch.outermost` as today. Projects whose `.sdlc/project.yaml` sits below the git top-level (for example `~/dev/ruby-gems/<gem>`, `~/dev/cc-plugins/api-wrapper`) must keep working. The input also keeps the session cwd for the placement and for `enter` — check: `grep -n "common-dir\|primary_root" plugins/os-sdlc/lib/os_sdlc/controller/boot/inputs.rb plugins/os-sdlc/lib/os_sdlc/adapter/git/repo.rb` 2. Add `plugins/os-sdlc/lib/os_sdlc/adapter/worktree_placement.rb`: finds `worktree` on `env["PATH"]`, runs `worktree start ticket-<n>` with the session cwd, and parses `mode`, `path`, `branch`, `finish`. A missing CLI, a non-zero exit, or output without all four keys raises `WorktreePlacement::Failed` with the CLI's stderr or the missing key. 3. `DispatchCommands.implementation_open` calls the placement with the session cwd before `Implementation::Open.call`, then passes the project root inside the printed worktree path (the printed path plus the same relative project path) as root to `Open` and `Tracker::Tea.for`, and turns `WorktreePlacement::Failed` into `exit_loudly("implementation-open worktree: ...")`. 4. `Implementation::Open.call` and `OpenPayload.for` take a `worktree:` value. The open payload gains `worktree: { mode, path, branch, enter, exit, finish }`: `enter` is true when the session cwd differs from `path`; `exit` is false only for `mode: persistent`; `finish` is the string `start` printed, unchanged. 5. Delete `plugins/os-sdlc/lib/os_sdlc/adapter/git/worktree.rb`, and `adapter/git/branch.rb` when it has no other sender — check: `! test -e plugins/os-sdlc/lib/os_sdlc/adapter/git/worktree.rb` 6. Test support: `tests/support/tmpdir_test_case.rb` `scaffold_sdlc_project` makes a real repo (`git init`), not a bare `.git` directory; `tests/support/in_process_dispatch_script.rb` passes an env whose `PATH` holds a stub `worktree` that prints a `ticket-continue` report with `path:` set to the test directory. Existing CLI tests that run `implementation-open` keep passing. Documentation, done by the orchestrator, not by a pipeline agent: - Amend ADR-0253's list with ADR-0250 (Boot binds from `input.dir`; open now binds the primary) and ADR-0188 (root search stops at the worktree top). - Rewrite `plugins/os-sdlc/skills/implement/SKILL.md` Setup, Start, and Finish; delete the `/os:worktree start` step (open now runs it): Start — if `worktree.enter`, call `EnterWorktree(path: worktree.path)`; comment the ticket with `worktree.branch`; on "Merge conflicts in:" resolve with `/mattpocock-skills:resolving-merge-conflicts`, commit, re-run open. Finish — if `worktree.exit`, call `ExitWorktree(action: keep)`; then run `worktree.finish` as printed. - Add one History row to `docs/implementation-status/os-sdlc.md` (or the owning leaf). - Run `bin/refresh-plugins`. Rejected: detect the start state in the skill (agents can pick the wrong mode; ADR-0157); bind the cwd DB with a pre-bind check (two binds); call `plugins/os/bin/worktree` by repo path (fails outside cc-os). ## Acceptance criteria 1. Given `worktree` on PATH, when `implementation-open` runs for ticket 844, then it runs `worktree start ticket-844` once, before it loads project.yaml or fetches the ticket. 2. Given the session cwd is a linked worktree, when `implementation-open` runs, then the implementation row is written to the primary checkout's `.sdlc/pipeline.db` and no `pipeline.db` is created in the worktree. 3. Given `start` prints mode, path, branch, and finish, when open succeeds, then the payload `worktree` block holds them and `finish` equals the printed string. 4. Given the session cwd equals the printed path, when open succeeds, then `worktree.enter` is false; otherwise it is true. 5. Given `mode: persistent`, when open succeeds, then `worktree.exit` is false; for the ticket modes it is true. 6. Given a ticket that lists a retired test, when open runs, then the file is deleted under the printed path and the primary checkout's copy is unchanged. 7. Given `worktree` is not on PATH, when open runs, then it exits non-zero, stderr starts with `implementation-open worktree:`, and no implementation row is created. 8. Given `start` exits 1, when open runs, then it exits non-zero with `start`'s stderr after `implementation-open worktree:`, and no implementation row is created. 9. Given `start` exits 0 but prints no `mode:` line, when open runs, then it exits non-zero, stderr names `mode`, and no implementation row is created. 10. Given the cwd is a linked worktree of a repository whose `.sdlc/project.yaml` sits in a subdirectory, when open runs from that subdirectory, then it binds the same subdirectory's DB in the primary checkout and uses the same subdirectory inside the worktree as root. ## Retired tests plugins/os-sdlc/tests/adapter/git/worktree_test.rb ## Owner sketch ```ruby module OsSdlc class WorktreePlacement Failed = Class.new(HarnessError) Result = Data.define(:mode, :path, :branch, :finish) def start(cwd, name) = parse(run(cwd, name)) end module DispatchCommands def self.implementation_open(root, order, env:, placement: WorktreePlacement.new(env: env)) worktree = placement.start(cwd, "ticket-#{work_order.ticket}") Implementation::Open.call(worktree.path, work_order, tracker: Tracker::Tea.for(root: worktree.path, env: env), worktree: worktree) end end end ``` ## Origin - Trigger: split from #844 during triage, 2026-09-30, after the user ruled that open places the worktree - Improvised this session: none - Chain: skill prose orders worktree before open ← `implementation-open` binds cwd and has no branch guard ← DESIGN: ADR-0157, ADR-0213 - Root candidate: this ticket - Where: plugins/os-sdlc/lib/os_sdlc/controller/boot/inputs.rb; command/dispatch_commands.rb; command/implementation/open.rb; presenter/implementation/open_payload.rb; adapter/git/worktree.rb; skills/implement/SKILL.md - Session: f531ed01-125b-49b4-b35a-43528beff19e - Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/f531ed01-125b-49b4-b35a-43528beff19e.jsonl Blocked by: #844. VERDICT: CREATE — user-requested hard floor; split from #844 which carries ADR-0253 (2026-09-30).
Author
Owner

Work started on branch ticket-846 (worktree .claude/worktrees/ticket-846, mode ticket-new) via /os-sdlc:implement. #842 and #844 merged (main 9ac00e4a).

Work started on branch ticket-846 (worktree .claude/worktrees/ticket-846, mode ticket-new) via /os-sdlc:implement. #842 and #844 merged (main 9ac00e4a).
Author
Owner

Implementation 12 failed at change-auditor (dispatch 80), verdict:error. Cause: the worktree-session harness guard refused the agent's os-sdlc-subagent-stop --json call four times (#848); the agent then sent a bare {dispatch_id: 80}, which the Runner recorded as an empty completion. The audit itself found no UNMAPPED/CONFLICT items; it noted tests/adapter/git/branch_test.rb must be retired with branch.rb. Blocked on how to run the pipeline around #848.

Implementation 12 failed at change-auditor (dispatch 80), verdict:error. Cause: the worktree-session harness guard refused the agent's os-sdlc-subagent-stop --json call four times (#848); the agent then sent a bare {dispatch_id: 80}, which the Runner recorded as an empty completion. The audit itself found no UNMAPPED/CONFLICT items; it noted tests/adapter/git/branch_test.rb must be retired with branch.rb. Blocked on how to run the pipeline around #848.
Author
Owner

Body stale (Tracker::Tea.for -> Tracker.for; untyped checks to be rewritten after #847) and item 6 edits existing test support files, which needs the #863 children. Label removed until both are done.

Body stale (Tracker::Tea.for -> Tracker.for; untyped checks to be rewritten after #847) and item 6 edits existing test support files, which needs the #863 children. Label removed until both are done.
Author
Owner

Blocked by #866 (Forgejo dependency recorded).

Blocked by #866 (Forgejo dependency recorded).
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.

Reference
jared/cc-os#846
No description provided.