T-A3HX-readiness-gate-tolerates-unwritable-main
State: open/ready · Impact: medium · Complexity: small
AUTO-DEFINED: this spec was best-effort machine-authored by /sdlc:task-auto-define on 2026-09-27 as part of /sdlc:spawn-task-pr’s
--drive-to-readyflow. Review the Goal and Acceptance-criteria carefully before trusting it.
The /sdlc:task-work readiness gate should accept a task-body fix landing on
the task branch itself when origin/main isn’t writable, instead of
requiring that fix to land on main before the gate runs. This gap surfaced on
T-98C8-polish-lint-config-patch-save in
sksizer/dev, quoted verbatim below.
The
/sdlc:task-workreadiness flow lands a task-body path fix onorigin/mainbefore the readiness gate runs, but this requires a direct push to main; when the session’s permission policy refuses that push, the fix has to ride the task branch as a manual workaround instead. The gate should accept the body fix landing on the task branch itself when main isn’t writable, rather than requiring the main push to succeed first. Related prior art: T-NXAM-task-doc-edit-commit-on-main proposed a sanctioned--commit-on mainverb for this same friction but was closedclosed/superseded, absorbed into engine-v0 phase E6 of D-VSLI-distributed-work-runner-architecture (not yet shipped) whose corpus-writer actor is meant to own every main-branch write; this task is the interim fallback for when even a sanctioned main-push path is unavailable.
Three places assume origin/main is writable during /sdlc:task-work
Step 5:
- Step 5 prose.
task-work.mdtells the agent to land any task-body edit from Step 2’s relevance check onorigin/mainbefore running 5a. An edit left only in the task worktree is discarded by 5b. - Step 5a gate.
/sdlc:task-ensure-readyshellssdlc task ensure-ready-mutate --commit-on main --cleanup-on-fail(solutions/ontological/lib/model/entities/task/ops/ensure-ready-mutate.ts). With an active lease, a pass writes only the lease’sgatesand makes no commit. A fail, or an unleased pass, commits throughcommitToMainViaWorktree(solutions/ontological/lib/services/git/commit-to-main.ts). That function retries only non-fast-forward, stale-info and lock rejections. Any other rejection, such as permission denied or a protected branch, throwsCommitToMainError, andensure-ready-mutate.tsreturnsEXIT_WRITE_REFUSED(19). - Step 5b start.
sdlc task start(solutions/ontological/lib/model/entities/task/ops/start.ts) runsgit reset --hard origin/mainin the worktree. A body fix committed on the task branch is thrown away.
So when the session may not push to origin/main, a body fix has nowhere
to go. On the T-98C8 run it rode the task branch as a manual workaround.
Proposed
Section titled “Proposed”When origin/main refuses the write, the task branch carries the body fix
and the run continues to PR-open. The fix then merges to main with the PR.
The writable-main path does not change. This is an interim fallback until
the corpus-writer actor in D-VSLI-distributed-work-runner-architecture
owns main-branch writes.
Approach
Section titled “Approach”- In
ensure-ready-mutate.ts, catch a non-recoverableCommitToMainErroron the--commit-on mainpath and fall back to the--commitbehavior: apply the same mutation to the task file in the calling worktree and commit it on the task branch. Emit the usual marker plus a line naming the fallback, so task-work can tell the fix did not reach main.--commitrefuses under an active lease today; the fallback needs that check relaxed for the lease holder’s own worktree. - Keep recoverable rejections on the existing retry loop. Only the permission/protected-branch class falls back.
- In
start.ts, stop unconditionally discarding task-branch commits. Before resetting, difforigin/main..<branch>in the worktree. Empty (today’s normal case — no fallback fix landed) means nothing to preserve: keep the plainreset --hard origin/main. Non-empty (the task branch carries a fallback body-fix commit from step 1) means capture those commit(s),reset --hard origin/main, thengit cherry-pickthem back onto the fresh tip. Cherry-pick, not rebase:start.test.ts’s no-rebase invariant test greps the compiled source for the literal"rebase"/'rebase'git argument and fails if it’s present, so this fallback path must not shell out togit rebase. - Update Step 5 of
task-work.md: tryorigin/mainfirst; if the push is refused, commit the body fix on the task branch and continue. - Add the unwritable-main fallback test in
ensure-ready-mutate.test.ts(a bare remote whosemainrejects pushes via apre-receivehook). Instart.test.ts, revise — don’t just supplement — the existing'post-start topology: task branch sits exactly on origin/main'test: its zero-commits assertion (diffLogmust equal[]) only holds for the no-preserved-commit case, since a preserved fallback commit now rides forward through the reset via cherry-pick. Scope that assertion to the plain case, and add a case in the same test (or a sibling right next to it) covering a task branch that already carries a fallback body-fix commit before start runs: assert the fix’s content survives (the branch ends up with a cherry-picked equivalent of the commit on top of the neworigin/maintip, not discarded), while the no-rebase and no-rebase-merge-directory assertions keep passing unchanged — cherry-pick leaves no rebase state behind.
| Area | Note |
|---|---|
solutions/ontological/lib/model/entities/task/ops/ensure-ready-mutate.ts | --commit-on main path; catch CommitToMainError and fall back to a task-branch commit |
solutions/ontological/lib/model/entities/task/ops/ensure-ready-mutate.test.ts | Unwritable-main fallback test |
solutions/ontological/lib/services/git/commit-to-main.ts | Source of CommitToMainError; may need to expose which rejection class fired |
solutions/ontological/lib/services/git/tests/commit-to-main.test.ts | Existing suite; must still pass |
solutions/ontological/lib/model/entities/task/ops/start.ts | reset --hard origin/main discards task-branch body fixes; cherry-pick them back on when present |
solutions/ontological/lib/model/entities/task/ops/start.test.ts | Revise the existing zero-commits topology assertion (scope to the no-preserved-commit case) and add the preserved-commit case; must not regress the no-rebase invariant test |
solutions/ontological/plugin/plugins/sdlc/skills/task-work/task-work.md | Step 5 prose on landing body edits |
Acceptance criteria
Section titled “Acceptance criteria”- AC-1: When
origin/mainisn’t writable (the push incommitToMainViaWorktreefails — aCommitToMainError, e.g. from a permission-denied or protected-branch rejection),ensure-ready-mutate.ts’s--commit-on mainpass/fail path no longer requires that push to succeed before recording its verdict: it falls back to landing the fix on the task branch instead of exitingEXIT_WRITE_REFUSED. - AC-2:
/sdlc:task-workStep 5a’s readiness gate, run against a task branch whose body fix could not be pushed toorigin/main, reaches PR-open instead of aborting on the unwritable-main push failure, and the PR still carries the body fix: Step 5b’ssdlc task startcherry-picks the fallback commit back onto the reset tip rather than discarding it. - AC-3: A test in
solutions/ontological/lib/services/git/tests/commit-to-main.test.ts(or a sibling test forensure-ready-mutate.ts) exercises the unwritable-origin/maincase and asserts the gate falls back to the task branch instead of raisingCommitToMainError. - AC-4: The existing writable-main path (successful push) is unchanged —
this is a fallback, not a replacement — and the existing
commit-to-main.test.tssuite still passes. - AC-5:
start.test.ts’s'post-start topology'test is revised, not just supplemented: its zero-commits-ahead assertion is scoped to the no-preserved-commit case, and a case in that test (or a sibling next to it) covers a task branch that already carries a fallback body-fix commit before start runs, asserting the commit’s content survives via cherry-pick. The file’s no-rebase invariant test still passes — the fallback path never shells out togit rebase.
Out of scope
Section titled “Out of scope”- none
Dependencies
Section titled “Dependencies”- none
Discovery context
Section titled “Discovery context”Spawned by /sdlc:spawn-task-pr on 2026-09-27 UTC from
T-98C8-polish-lint-config-patch-save in
sksizer/dev.
Dedup search (spawn-from-post-mortem)
Section titled “Dedup search (spawn-from-post-mortem)”Bullet: sdlc task resolve found no product-scoped task, and start_task.ts / ensure-ready —commit-on main assume the root task tree and a writable main; the promotion, lease transition and gate were done by hand — the resolver and start path should search every planning root. Keywords searched: product-scoped, ensure-ready, start_task, transition, commit-on, promotion, writable, resolver Excluded: T-50QX-polish-rule-options-with-no-default Top candidates (score / status / headline):
- 65 / closed/superseded / T-RNHP-autonomy-body-fill-lands-on-main — Route ensure-ready autonomy auto-define body edits to origin/main so start_task’s reset preserves them
- 53 / closed/done / T-F61F-pluggable-claim-resolver-interface-for-task-ensure-ready — Pluggable claim-resolver interface for task-ensure-ready
- 45 / closed/done / T-XM0B-ensure-ready-script-emits-markers-and-cleanup — ensure-ready emits terminal markers + worktree teardown deterministically from the script, not LLM prose
- 44 / closed/superseded / T-2QXZ-start-task-handles-frontmatter-rebase-cleanly — start_task.py auto-merges frontmatter on stamp/start rebase conflict
- 39 / closed/done / T-SIHV-task-state-frontmatter-commits-on-main-not-worktree-branch —
All task-state frontmatter commits land on main; worktree branch is implementation diff only
Decision: LINKED-EXISTING T-A3HX-readiness-gate-tolerates-unwritable-main (override, from
T-50QX-polish-rule-options-with-no-default) Rationale: This bullet, from T-50QX’s post-mortem, bundles two distinct gaps. This half — ensure-ready’s--commit-on mainassuming a writable main — matches this task exactly (T-A3HX was spawned the same day from sibling task T-98C8’s post-mortem describing the identical friction: the readiness gate requiring a push to origin/main to succeed before it runs). The other half of the bullet (resolver / start path missing planning roots) is linked separately to T-9WPJ-resolve-searches-every-planning-root. Neither surfaced in the automated top-5 above — generic ensure-ready/start_task keywords score higher against closed/superseded dead ends. Both are still open, unimplemented. Linked here instead of spawning a duplicate.