Skip to content

[06] Commit Workflows — The Pluggable Path From Diff to PR

Every agent eventually has to land code somewhere. SASE's commit XPrompt workflows are the small, runtime-uniform layer that turns an agent's diff into a commit, a proposal, or a pull request — without the agent caring which VCS is underneath.

[05] covered how a plan turns into a fleet of phase agents that produce code. This post covers what happens at the end: how each of those agents lands its diff somewhere durable, and how that path is uniform across runtimes and VCS providers.

One CLI, Three Outcomes

The sase commit command drives three XPrompt workflows. They share the same orchestrator, the same pre-stages, and the same result format; they differ only in what the dispatch step produces.

Workflow XPrompt Dispatch hook What it produces Tracking
Commit #commit create_commit Git commit on current branch COMMITS entry
Propose #propose create_proposal Saved diff file COMMITS entry
PR #pr create_pull_request New branch + PR ChangeSpec

Every flow walks the same pre-dispatch pipeline: bead association → bead lifecycle close (skipped for proposals) → plan handling → precommit command → parent CL detection (PR only) → diff capture → checkpoint. Only then does it call the VCS-specific create_commit / create_proposal / create_pull_request hook. Post-dispatch, the workflow writes a commit_result.json marker for the XPrompt post-steps to read.

The Stop-Hook Contract

Agents do not run git commit themselves. They make changes; when they finish, the sase_commit_stop_hook notices the uncommitted state and blocks them with a structured instruction to invoke the matching commit skill (for example /sase_git_commit for git-based projects, /sase_hg_commit for Mercurial). The skill calls sase commit with flags — usually -M commit_message.md -f <files> -t <method>.

That stop hook is runtime-uniform. Codex receives a structured JSON response with decision=block, Gemini and Qwen receive structured JSON with decision=deny, and Claude-compatible hooks receive stderr plus a blocking exit code. The control flow is the same regardless of which runtime is on the other end: changes exist → block with skill name → skill calls sase commit. No runtime-specific branching in the agent prompt, no "if Codex then X" anywhere in the workflow.

If SASE_BEAD_ID is set, the stop hook first asks the agent to decide whether the uncommitted changes were made in the current session. For changes the agent did make, it instructs the agent to close and verify the bead before invoking the commit skill. That keeps bead lifecycle state ahead of the commit dispatch while avoiding accidental closure of unrelated dirty work.

Runtime-Uniform Commit Skills

Every supported agent runtime ships the same commit skill surface. The skill names, flags, and outputs are identical across Claude, Codex, Gemini, Qwen, and OpenCode. An XPrompt that calls into the commit workflow runs the same way regardless of who is on the other end. This is one of the gotchas baked into SASE's project memory: do not introduce runtime-specific special cases; treat all runtimes uniformly.

The VCS Provider Boundary

The three dispatch methods are pluggy hooks defined in VCSHookSpec:

Plugin create_commit create_proposal create_pull_request
BareGitPlugin Commit + push Save diff + clean Branch + commit + push
GitHubPlugin Inherits from git Inherits from git + creates PR via gh CLI
HgPlugin hg commit + mail sase_hg_clean Not supported natively

All hooks return tuple[bool, str | None] — a success flag and an optional result string (commit hash, diff path, or PR URL). The orchestrator stays VCS-agnostic; new VCS providers slot in by implementing the three hooks. Provider selection is also pluggable: an env var (SASE_VCS_PROVIDER), then sase.yml vcs_provider config, then auto-detection. Documented in vcs.md.

Resume After Conflict

When a VCS dispatch hits a merge conflict mid-flight, the workflow leaves a checkpoint on disk ($SASE_ARTIFACTS_DIR/commit_state.json, or ~/.sase/commit_state/<session>.json if no artifacts dir is set) and exits with RunResult.CONFLICT (exit code 2). The CLI prints:

create_commit hit a merge conflict: … Resolve the conflict, then run sase commit --resume to finish.

sase commit --resume loads the checkpoint, re-checks the working tree for conflict markers, verifies the commit at HEAD matches the subject line from the checkpointed message, calls the provider's vcs_finalize_commit hook to replay idempotent post-commit work (bead amend, push with retry), re-runs the tracking steps (COMMITS entry append, ChangeSpec creation), and deletes the checkpoint on success. Resume is VCS-agnostic: the same --resume flag works for commits, proposals, and PRs.

This is the recovery path that makes multi-agent execution survivable. Without it, a conflict on phase 3 of a seven-phase epic would mean wiping the workspace and restarting; with it, the human resolves the conflict, runs sase commit --resume, and the rest of the epic carries on through AXE's %wait resolution.

What's in commit_result.json

After a successful dispatch, the marker contains the durable hand-off between the agent that wrote the code and whoever (or whatever) consumes the result:

{
  "method": "create_commit",
  "result": "<commit_hash | diff_path | pr_url | null>",
  "message": "The commit message",
  "name": "Branch/CL name",
  "bead_id": "Bead ID if SASE_BEAD_ID was set",
  "changespec_name": "ChangeSpec name (PR only)",
  "entry_id": "COMMITS entry ID (commit/propose only)",
  "diff_path": "Saved pre-dispatch diff path, when available"
}

XPrompt post-steps read it, emit metadata outputs (meta_new_commit, meta_commit_message, meta_changespec, …), and downstream workflows consume those.

The Public Plugin API

Two entry-point groups are the public extension surface:

  • sase_vcs — provider classes that implement create_commit, create_proposal, create_pull_request, plus resume and classification hooks. sase-github is the canonical out-of-tree implementation.
  • sase_xprompts — packages whose xprompts/ directories contribute reusable XPrompts and workflows, including overrides for the built-in commit XPrompts.

Plugin resource loading can be disabled via environment variables for debugging:

Variable Effect
SASE_DISABLE_PLUGINS Disable resource plugin loading for config and xprompts
SASE_DISABLE_PLUGIN_XPROMPTS Disable xprompt/workflow resource plugins only
SASE_DISABLE_PLUGIN_CONFIG Disable plugin default_config.yml resource loading only

The VCS, workspace, and LLM provider registries load entry points directly and do not consult these disable switches; those are about which configuration and prompt files contribute to the resolver, not which providers exist.

Series Navigation

This is [06] in the SASE Blog Series.