Skip to content

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-ready flow. 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-work readiness flow lands a task-body path fix on origin/main before 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 main verb for this same friction but was closed closed/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.md tells the agent to land any task-body edit from Step 2’s relevance check on origin/main before running 5a. An edit left only in the task worktree is discarded by 5b.
  • Step 5a gate. /sdlc:task-ensure-ready shells sdlc 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’s gates and makes no commit. A fail, or an unleased pass, commits through commitToMainViaWorktree (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, throws CommitToMainError, and ensure-ready-mutate.ts returns EXIT_WRITE_REFUSED (19).
  • Step 5b start. sdlc task start (solutions/ontological/lib/model/entities/task/ops/start.ts) runs git reset --hard origin/main in 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.

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.

  1. In ensure-ready-mutate.ts, catch a non-recoverable CommitToMainError on the --commit-on main path and fall back to the --commit behavior: 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. --commit refuses under an active lease today; the fallback needs that check relaxed for the lease holder’s own worktree.
  2. Keep recoverable rejections on the existing retry loop. Only the permission/protected-branch class falls back.
  3. In start.ts, stop unconditionally discarding task-branch commits. Before resetting, diff origin/main..<branch> in the worktree. Empty (today’s normal case — no fallback fix landed) means nothing to preserve: keep the plain reset --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, then git cherry-pick them 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 to git rebase.
  4. Update Step 5 of task-work.md: try origin/main first; if the push is refused, commit the body fix on the task branch and continue.
  5. Add the unwritable-main fallback test in ensure-ready-mutate.test.ts (a bare remote whose main rejects pushes via a pre-receive hook). In start.test.ts, revise — don’t just supplement — the existing 'post-start topology: task branch sits exactly on origin/main' test: its zero-commits assertion (diffLog must 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 new origin/main tip, not discarded), while the no-rebase and no-rebase-merge-directory assertions keep passing unchanged — cherry-pick leaves no rebase state behind.
AreaNote
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.tsUnwritable-main fallback test
solutions/ontological/lib/services/git/commit-to-main.tsSource of CommitToMainError; may need to expose which rejection class fired
solutions/ontological/lib/services/git/tests/commit-to-main.test.tsExisting suite; must still pass
solutions/ontological/lib/model/entities/task/ops/start.tsreset --hard origin/main discards task-branch body fixes; cherry-pick them back on when present
solutions/ontological/lib/model/entities/task/ops/start.test.tsRevise 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.mdStep 5 prose on landing body edits
  • AC-1: When origin/main isn’t writable (the push in commitToMainViaWorktree fails — a CommitToMainError, e.g. from a permission-denied or protected-branch rejection), ensure-ready-mutate.ts’s --commit-on main pass/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 exiting EXIT_WRITE_REFUSED.
  • AC-2: /sdlc:task-work Step 5a’s readiness gate, run against a task branch whose body fix could not be pushed to origin/main, reaches PR-open instead of aborting on the unwritable-main push failure, and the PR still carries the body fix: Step 5b’s sdlc task start cherry-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 for ensure-ready-mutate.ts) exercises the unwritable-origin/main case and asserts the gate falls back to the task branch instead of raising CommitToMainError.
  • 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.ts suite 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 to git rebase.
  • none
  • none

Spawned by /sdlc:spawn-task-pr on 2026-09-27 UTC from T-98C8-polish-lint-config-patch-save in sksizer/dev.

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 main assuming 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.

← Back to Tasks