Hook admission, submit, gates, and settlement read the stored worktree root #1436
Labels
No labels
P0
P1
P2
P3
bug
category/code-change
category/cross-repo
category/eval-run
category/investigation
category/no-test-target
category/owner-ruling
category/prose-artifact
category/recurring-chore
category/structure
category/umbrella
crc-refactor
create
delete
enhancement
filed-by/agent
filed-by/user
frozen
lint-rule
needs-info
needs-triage
next
plugin/cc-architect
plugin/os
plugin/os-adr
plugin/os-aidd-lint
plugin/os-backlog
plugin/os-context
plugin/os-doc-hygiene
plugin/os-sdlc
plugin/os-vault
project/cc-os
ready-for-agent
ready-for-human
recurring
review
rule-proposal
update
waiting
wayfinder:grilling
wayfinder:map
wayfinder:map
wayfinder:research
wayfinder:task
wayfinder:task
wontfix
worklist/deviations
worklist/lint-rule
worklist/new-implement-build
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
jared/cc-os#1436
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
Two paths still take the project root from the caller's cwd after #1164. When the orchestrator runs in the main checkout M and the implementation bound worktree W, they act on M:
os-sdlc-gatebindsio.cwd(boot/inputs.rb:17) andGate::Runuses it for its project (gate/run.rb:38) and as the command directory (gate/run.rb:46Gate.new(root: project_root)). The gate commands run in M.os-sdlc-subagent-stopwith adispatch_idbinds the cwd root (boot/inputs.rb:38, thenBoot#third_argument).CompletionIntakepasses it toCompletionPayload.for(..., root:)(completion_intake.rb:125), thenNextStep(next_step.rb:15), thenTransitionandLanding, thenCommit.new(root:)(landing.rb:33). The landing commit runsgitin M, so the change in W is not committed.CompletionIntake#project(completion_intake.rb:82) also givesObjectionIntakethe M root for its goalpost check.Spike evidence (2026-10-27, scratch worktree at
0faed2ff): the two tests in the Owner sketch fail at HEAD. A gate run from M recordsfailfor a test command that passes only in W. A submit from M leaves W's HEAD at the seed commit.Items from the first draft that are dropped, with evidence:
admission.rb:7).SubagentStartuses@rootfor one purpose only,Brief.for(..., root: @root)(subagent_start.rb:30). Since #1164,Brief.forresolveswork_root(root).BriefPayloadtakes no root. Nothing is left to fix.boot.rb:57pipeline_db?. It checks only that.sdlc/pipeline.dbexists at the cwd root (artifact_dir.rb:21). M holds the real file and W symlinks to it (ADR-0253), so the answer is the same from both. The bind after it opens the same shared database.Settlement.build_repo(settlement.rb:22). It fillsCompletionIntake::Request#repo, and no code reads that field (grep forrepoinimplementation/anddispatch/finds only the Struct member and the writer). The wrong root has no effect.Reproduce
Run the two tests in the Owner sketch at HEAD. Both fail as stated there.
Approach
Resolve the stored root at the two places where the cwd root enters the per-implementation flow. Do not add a second resolver; use
Implementation::Targeting#work_rootfrom #1164.Targeting#project(root):snapshot.project(work_root(root))(slice 1 audit note a). Everyimplementation.project(root)caller then gets W:Brief.for,Gate::Run#project,CompletionIntake#project, andAcceptance#mutation_check.Brief.forgoes back toimplementation.project(root), so it sends one message, not two.Gate::Runusesimplementation.work_root(@project_root)as its root, soGate.new(root:)runs the commands in W.NextStep#initializestoresimplementation.work_root(root).NextStepis the one point that both the submit path (CompletionPayload) and thenextpath (NextInstruction) reach beforeTransition,Landing,Commit, andEscalationPayloaduse the root. So the landing commit runs in W from either path.When
worktree_rootis null,work_rootreturns the cwd root, as today.Required work:
Implementation::Targeting#project(root)builds the project fromwork_root(root)(proven by AC 1 and AC 2).Dispatch::Brief.forcallsimplementation.project(root)withoutwork_root— check: absentwork_rootinplugins/os-sdlc/lib/os_sdlc/dispatch/brief.rbGate::Runruns its gate with the root fromimplementation.work_root(proven by AC 1).Implementation::NextStepresolves its root withimplementation.work_root(root)(proven by AC 2).work_rootfrom theBoundstand-in inbrief_map_name_test.rb, becauseBrief.forno longer sends it — check: absentdef work_rootinplugins/os-sdlc/tests/dispatch/brief_map_name_test.rbwork_rootfrom theBoundstand-in inbrief_drafted_map_test.rb— check: absentdef work_rootinplugins/os-sdlc/tests/dispatch/brief_drafted_map_test.rbAcceptance criteria
os-sdlc-gate <dispatch_id>runs with cwd set to a different checkout M, then the gate reply's verdict ispass.lib/widget.rbin W, and a pending dispatch on an agent-verdict node whose pass edge completes the implementation, whenos-sdlc-subagent-stopreceives thatdispatch_idwith a pass handoff that listslib/widget.rb, with cwd set to M, then W's HEAD commit contains exactlylib/widget.rb.Owner sketch
Implementation::TargetingGate::RunImplementation::NextStepTransition,Landing,Commit, andEscalationPayloadthe stored worktree root.Dispatch::Briefimplementation.project(root).Test stand-ins for objects whose interface changes (slice 1 audit notes b and c):
Boundinplugins/os-sdlc/tests/dispatch/brief_map_name_test.rbandBoundinplugins/os-sdlc/tests/dispatch/brief_drafted_map_test.rbstand in forImplementationasBrief.for's collaborator. They defineproject(_root)andwork_root(root). After the changeBrief.forsends onlyproject, so deletework_rootfrom both.ImplementationthatGate::RunorNextStepreceives.tests/gate/run_test.rb,tests/gate/run_required_work_test.rb,tests/implementation/next_step_*_test.rb,commit_test.rb, andcompletion_payload_test.rbuse realImplementationrows.Spike check (not committed): with the change above, both tests pass, and the full suite passes except
SystemModelCoverageTest, which fails only because the scratch worktree path has noplugins/ossibling.Test 1:
plugins/os-sdlc/tests/pipeline/gate_cli_worktree_root_test.rbRed at HEAD:
Expected: "pass" Actual: "fail", because the test command runs in M, whereworktree-onlyis absent.Test 2:
plugins/os-sdlc/tests/pipeline/cli_subagent_stop_worktree_root_test.rbRed at HEAD:
Expected: ["lib/widget.rb"] Actual: ["README.md"], because the landing commit runsgitin M, so W's HEAD stays at the seed commit.Out of scope
worktree_rootcolumn, its migration,implementation-openstoring it, and brief resolution: #1164.worktree_rootfor closed implementations.CompletionIntake::Request#repofield andSettlement.build_repo. This is dead-code cleanup with no behavior change.required-work-check,retired-tests-check,unchanged-check). They run as gate commands, so they run in W after required work item 3.Depends on #1164 (merged as
d2f8a213).Origin
MISSING: test_changes← brief read the main checkout ← root taken from caller cwd (boot/inputs.rb:10) ← DESIGN: no stored worktree root (#1164 Ruling)Work started on branch ticket-1436 via /os-sdlc:implement (poodr-ticket-implementation).
Resolution
Done: Targeting#project builds the project from the stored worktree root, so Brief.for, Gate::Run, CompletionIntake and Acceptance get the bound worktree. Gate commands run in the worktree. NextStep resolves the stored root once for Transition, Landing, Commit and EscalationPayload, so a submit from the main checkout commits the change in the worktree.
Evidence: main
4c7926be(merged ase652b689). Tests: gate_cli_worktree_root_test, cli_subagent_stop_worktree_root_test. os-sdlc suite 2792 runs, 0 failures. Rubocop clean. Implementation 195 escalated at required-work (target-relative check paths) and was hand-finished per the escalation procedure, with an opus design audit (no must-fix; the should-fix gate-root duplication is fixed).Follow-ups: #1443 (required-work check paths, skeptic CREATE). RerouteIntake passes the cwd root to Transition: dropped, because a reroute does not reach Commit today; recorded in the commit message.