redhat-et / redhat-et/ProtoBot

Define a Source Control Manager for governed Git and Git host operations

Open
#125 2 comments 1 reaction 1 assignee View on GitHub

@lukaskellerstein is already working on this.

Since Sep 16, 2026.

component:drafting-table component:job-site component:specification-toolkit documentation triaged
Dominant language
Go
Stars
5
Forks
6
Avg merge
23h 24m
Merged PRs (30d)
66

Description

Question

Should Git and Git host mutations go through a Source Control Manager
(SCM), one deterministic component that turns an approved decision into
branches, commits, and PRs, instead of through the shell of each agent?

The point

ProtoBot already keeps the agent away from the stores it governs. The
agent decides what requirement to write, and ears-manager makes it
well-formed. The agent decides which work-item change to ask for, and
the WMS Adapter validates it. Git is the one governed system where the
agent still runs the raw tool itself.

An SCM closes that gap with the same split:

  • The agent decides when, on the user's word.
  • The SCM decides how: which branch, which files, which message,
    which PR, and whether the action is allowed at all.

What this brings to the architecture:

  1. A smaller question to guard. The shell guard asks "is this
    command text safe?", which has no end. The SCM asks "may this role do
    this action on this change set?", which is a short, closed list.
  2. Less responsibility per agent. The Drafting Table stops knowing
    Git syntax, the commit format, and the PR body format. It keeps
    specification work. That is the single-responsibility split
    ProtoBot already uses for specifications and work items.
  3. One Git boundary for every caller. The Drafting Table, the Job
    Site, and the Materializer reach Git through one set of rules, one
    host adapter, and one audit record, in every deployment mode.
  4. The Gate gets a home. Hosted modes require a Gate at every
    mutation boundary
    (Authentication and Credential Isolation). The SCM is that
    boundary for Git.

The problem today

The Drafting Table runs Git and gh through the harness shell tool
(OpenCode-plus-skill strawman). #34 lists the permitted Git
operations (Permitted Git operations), and #33 enforces them
with a command allowlist and a guard that parses each shell command.
That works for the MVP.

The #33 PR tightens the shell as an interim step. It trims the
allowlist to the operations a Drafting Table step needs. It removes
merge, branch deletion, and discard from the agent, because the merge
is the approval and a person makes it. It replaces forbidden-option
lists with exact argument forms. The guard binds push and gh pr to
the current change-set branch, --repo to the canonical repository,
and staging to governed paths.

That closes part of the gap. The rest is structural, and a command
allowlist cannot close it:

Gap Example After the #33 tightening
Options nobody listed gh pr edit --base or --add-reviewer on an allowed command Reduced. Exact forms refuse them, but the check is a shell parser, and every new form is another grammar to get right.
Targets not tied to the change set cs/* matches every change set's branch; a PR number can name any PR Reduced. The guard binds them, by reading state and parsing the command. The operation itself still takes a free target.
Fixed-format text written by the model The commit subject, the Change-Set: trailer, and the PR body rendered from compare and impact Stays. A model can drop or change a line.
One copy of the rules per harness Native rules of different strength; Claude Code prefix rules cannot see an option inside a command Stays. The copies are shorter.
Hosted runtimes The Web Drafting Table replaces the harness with a hosted runtime (strawman) Stays. A hosted runtime needs a Git mutation boundary behind the Gate, not an agent shell next to a token.
One host gh ties the contract to GitHub Stays.

Branch protection, ears-manager check in CI, and path ownership keep
the default branch safe in every case. What stays open is damage beside
it: another change set's branch, another PR's metadata, or a wrong PR
body. Visible and fixable, but real. This issue proposes the step after
the #33 tightening.

Who uses it

Caller Kind Needs Refs
Drafting Table Agent Read state, start a branch, commit, refresh, publish Its own cs/ branch and PR
Single-player registration Command Read the merge commit Default branch, read only
Materializer Service Read the manifest at the merge commit; reconcile Git with WMS after a partial failure Default branch, read only
Job Site integration and merge Service Create wi/, apply validated patches, commit, merge the default branch in, merge wi/ to main, or open a PR if Q13 needs a human gate Its own wi/ branch, with a lease and a fencing token
Workers, Test Worker, Inspectors Agents None None

Workers "never receive the canonical remote or repository credentials"
(Job Site internal structure), and "Workers and
implementation-aware test agents receive no Git or WMS mutation role"
(Authentication and Credential Isolation).

So the Drafting Table is the only agent with a Git mutation role.
Every other Git mutation comes from a deterministic service. The
permission table has one agent row. It grows with services, not with
agents, and services are plain code with leases and idempotency keys
(The PR → merge → build model).

Where it sits

The graph marks every piece of the architecture by what it needs from
the SCM. Red mutates through it. Blue reads through it. Gray and
dashed references it by name or binds it. Yellow and dashed is an
open question. White needs nothing from it.

flowchart LR
  subgraph DT["Drafting Table"]
    TUI["TUI Drafting Table<br/>any harness"]
    WEB["Web Drafting Table<br/>hosted runtime"]
    REG["register-approved-change-set<br/>single-player"]
  end

  subgraph ADAPTER["Adapter core and bindings"]
    SKILL["Toolkit skill<br/>drafting-specifications"]
    MANIFEST["Adapter manifest"]
    BIND["Harness bindings"]
    GUARD["Guard"]
  end

  subgraph JS["Job Site"]
    MAT["Materializer / Dispatcher"]
    INT["Integration / Merge service"]
    ESC["Escalation"]
    PROJ["Projector / Test Catalog"]
    PATCH["Patch / Ownership Validator"]
    WORK["Workers A and B<br/>Test Worker, Inspectors<br/>Fullsend pre- and post-phase"]
  end

  SCM["SCM<br/>core + Drafting Table face<br/>+ Job Site face, later"]
  GATE["Gate + credential broker<br/>hosted modes"]
  EM["ears-manager"]
  WMS["WMS Adapter"]
  REPO["Project repository"]
  HOST["Git host<br/>branch protection, CODEOWNERS, CI"]

  TUI -->|"repo_state, branch_start,<br/>commit, publish, refresh"| SCM
  WEB -->|"the same, over the network"| GATE
  GATE --> SCM
  REG -->|"read: merge commit"| SCM
  MAT -->|"read: manifest at merge commit,<br/>reconcile"| SCM
  INT -->|"wi/ branch, patches, merge<br/>(future face)"| SCM
  ESC -.->|"issue on the main repo<br/>(open question 6)"| SCM
  PROJ -.->|"canonical source at a commit<br/>(open question 8)"| SCM
  PATCH -->|"accepted patch bundles"| INT
  WORK -->|"untrusted patch bundles"| PATCH
  MAT --> WMS
  INT --> WMS

  SKILL -.->|"names the operations"| SCM
  MANIFEST -.->|"lists the server"| SCM
  BIND -.->|"register the MCP server"| SCM
  GUARD -.->|"refuses SCM tools<br/>outside the role"| SCM

  SCM -->|"reads: manifest, digests,<br/>compare, impact"| EM
  SCM -->|"git, argument lists"| REPO
  SCM -->|"gh: pr create, edit, view"| HOST

  classDef mutate fill:#fde2e2,stroke:#c0392b,color:#000
  classDef read fill:#e2ecfd,stroke:#2c5aa0,color:#000
  classDef ref fill:#f2f2f2,stroke:#888,stroke-dasharray:4 2,color:#000
  classDef open fill:#fff9db,stroke:#b7791f,stroke-dasharray:4 2,color:#000
  classDef none fill:#ffffff,stroke:#bbb,color:#000
  classDef scm fill:#fff4d6,stroke:#b7791f,stroke-width:2px,color:#000

  class TUI,WEB,INT mutate
  class REG,MAT read
  class SKILL,MANIFEST,BIND,GUARD ref
  class ESC,PROJ open
  class EM,WMS,PATCH,WORK,REPO,HOST,GATE none
  class SCM scm
Piece Needs the SCM For
TUI Drafting Table, any harness Mutate repo_state, branch_start, commit, publish, refresh, on its own cs/ branch and PR
Web Drafting Table, hosted runtime Mutate, through the Gate The same operations over the network; the credential comes from the broker, never from the runtime
register-approved-change-set Read The merge commit of the change set's PR
Materializer / Dispatcher Read The manifest at the merge commit; the Git side of reconciliation after a partial failure
Integration / Merge service Mutate, future face wi/ branch, validated patches, merge of the default branch in, merge to main or a PR (Q13)
Job Site escalation Open An issue on the main repo: SCM action or WMS action (open question 6)
Projector / Test Catalog Open Canonical source at the work item's source commit, to build projections (open question 8)
Toolkit skill, adapter manifest, harness bindings, guard Reference The skill names the operations, the manifest lists the server, a binding registers it, the guard refuses its tools outside the role
ears-manager No. The SCM calls it Manifest, digests, compare, impact. It never calls the SCM
WMS Adapter No Records the commit hashes that callers supply
Workers, Test Worker, Inspectors, Patch/Ownership Validator, Fullsend phases No No Git or WMS mutation role; patches return as bundles (Job Site internal structure)
Git host, branch protection, CI No. Downstream What the SCM talks to; final in every mode

Responsibility

One sentence: the SCM turns an approved decision about a governed
object (a change set or a work item) into Git and Git host state, and
reports that state back.

Owns Does not own
Project and repository identity, read from the working tree, never from the caller (The project root) File content of specifications: ears-manager
Refs, by the ref policy of the caller's role File content of code and tests: Job Site Workers, through validated patches
Commits: the file set derived from the manifest and the registry, the message, the trailer Approval, which is the merge of a specification PR: a person, through the host
PRs: create, update, read state; the body rendered by code Work-item state: the WMS Adapter
The Git host adapter: GitHub first, others later Branch protection and CI gates: the host and CI
Structured failures: the #34 failure table as error codes
Audit: caller, role, object, action, ref, result

How far it goes:

  • It never decides content. It applies content that another
    component already decided and validated.
  • Clean merge or abort. On a conflict it aborts, leaves the tree as
    it was, and names the files. The owner of the content resolves it.
  • No generic Git operation. A tool such as git_push(ref) is a
    shell with a new name. Every operation takes a governed object and
    derives the refs, files, and PR from it.
  • No history rewrite, no push to the default branch, and no merge of
    a specification PR.

First operations for the Drafting Table

Operation Replaces Does
repo_state rev-parse, remote -v, status, fetch, merge-base, gh pr view Read only. The branch, whether the default branch moved since base_commit, uncommitted governed paths, and the PR number, state, and merge commit.
branch_start fetch, switch -c Cuts cs/<nnn>-<slug> from a fresh default head.
commit add, commit Runs the digest check, stages exactly the change set's files, writes the subject and trailer.
publish push, gh pr create, gh pr edit Pushes the change set's branch without force, then creates or updates its PR. Code renders the body from compare and impact.
refresh fetch, merge --no-ff, merge --abort Merges the default branch in, or aborts on a conflict and names the files.

This takes Git and the Git host client out of the Drafting Table
role's shell. That is the riskiest part of the shell, because it
reaches remote, shared state: other people's branches and PRs. Whether
ears-manager stays a shell command is #30's decision. If both move to
tools, the role needs no shell at all.

Control

  1. Tool exposure. A caller gets only the operations of its role. A
    harness binding loads only the Drafting Table operations.
  2. Authorization in the SCM. Role, object, action, and ref pattern,
    checked on every call. Locally the role comes from the launch
    configuration: this narrows what an agent can do, but the process
    still runs as the user. Hosted, the role comes from the OAuth token
    through the Gate, which is a real boundary.
  3. The host. Branch protection, CODEOWNERS, required reviews, and
    token scope stay final in every mode.

Shape

Option Gives Costs
One shared SCM service for every caller One host adapter, one audit, one policy engine A permission matrix inside one service. Job Site rules (leases, fencing, patches) enter a component the Drafting Table also depends on. One release cycle for both.
An SCM core library, plus one narrow face per role The role is fixed by which face a caller gets. Identity, host adapter, ref policy, audit, and argument-list Git are shared code. Two deployables later. The core needs its own versioning.

The proposal leans to the second option: start with the Drafting Table
face, and let the Job Site integration and merge service reuse the core
later.

Packaging: a CLI with an MCP face for agents. Single-player: a
local process over MCP stdio, started in the project root, running
git and the host client with argument lists, never through a shell,
with the user's own credentials. Hosted: a network service behind the
Gate, with a broker-issued token scoped to the project and the action.

Why not?

The case against the SCM, stated as strongly as we can make it:

  1. The #33 tightening can be enough. After it, the agent runs exact
    command forms, bound to the current change-set branch, its PR, and
    the canonical repository, and the host protects the default branch.
    What stays is damage that is visible and can be undone: a wrong
    commit on a change-set branch, or a wrong PR body that a reviewer
    sees. A new component for that remainder is hard to justify before
    the MVP (#77) runs.
  2. Models already know Git and gh. Every model has seen a great
    many Git commands. An SCM API is new vocabulary: more skill text to
    teach it, more wrong calls, and nothing learned elsewhere carries
    over. A reader of the session record also sees an operation name
    instead of the Git command that ran, unless the SCM reports it.
  3. It does not remove the shell. ears-manager still runs through
    the harness shell (#33), so the guard still parses shell commands
    and every binding still carries shell rules. The SCM adds a second
    mechanism next to the first one instead of replacing it.
  4. The local security gain is small. In single-player mode, which
    is the on-ramp, the SCM runs as the user with the user's token. Every
    other session, agent, and terminal on that machine still has git
    and gh. The SCM narrows the Drafting Table role, and it isolates
    nothing. The real boundary exists only in hosted modes, behind the
    Gate, and the Web Drafting Table is a non-goal of the first project
    (Vision — Non-goals).
  5. Flexibility moves to the user. Real repositories reach states
    that no operation set foresees: a conflict, a stale local branch, a
    half-finished merge, a commit to split, a branch to rename. With a
    shell the agent can help inside its limits. With an SCM the user
    leaves the Drafting Table and fixes it by hand, and every such case
    becomes a feature request and a release.
  6. The Job Site reuse is speculative. The Job Site's Git work runs
    in a private integration environment with leases, fencing tokens,
    patch import, and attestation commits, and none of it is designed in
    detail yet (Q8; the bot account model is open). Fullsend, the
    first Job Site backend, already has its own pre-script, sandbox, and
    post-script mutation model (Fullsend). A shared SCM
    designed before either side exists risks a lowest-common-denominator
    API, or one service that both sides depend on and neither owns.
  7. It creates a split transaction. One change set then spans two
    deterministic tools: ears-manager writes the manifest and records
    base_commit, and the SCM cuts the branch and commits. Order and
    partial failure between them (manifest written but no branch, commit
    made on a stale manifest) are the split transactions that the single
    write gate of ears-manager exists to avoid
    (ears-manager design principles).
  8. Smaller changes close the text gap. The model-written commit
    message and PR body do not need a new component. ears-manager can
    render both deterministically, and the agent passes the rendered
    text through unchanged.
  9. Host neutrality is not needed yet. The first project is hosted
    on GitHub, and the Vision already defers multiple WMS backends. A
    host adapter layer designed against one host is likely to be wrong
    for the second.
  10. Every binding carries more. Each harness registers one more MCP
    server and, in multi-player mode, authenticates to it, as H13 does
    for wms. The shell rules shrink, but the MCP and authentication
    surface grows, and a harness with weak MCP OAuth support gets
    harder to bind.
  11. Cost and time. Build, package, version, ship, and test one more
    component, with its own fixture and host fakes, and map every Git
    and host error to a structured result. Toolkit packaging is still
    open (Q5). It is also one more local process: if it does not
    start, nothing can be committed.
When the answer flips

The arguments above lose weight when one of these happens:

  • The Web Drafting Table, or any hosted runtime, starts. It needs a Git
    mutation boundary behind the Gate anyway.
  • The Job Site integration and merge service is designed, and its Git
    rules overlap with the Drafting Table's.
  • A second Git host is required.
  • The guard's shell vectors keep growing, or a guard bypass reaches a
    change-set branch or a PR in practice.
  • #30 moves ears-manager from a shell command to tools. Then taking
    Git out of the shell removes the shell from the role entirely.

Open questions

  1. One shared service, or a core library with one face per role?
  2. Who cuts the change-set branch: ears-manager change-set create, or
    branch_start before it?
  3. Does the SCM read the manifest, digests, compare, and impact
    from ears-manager itself, or does the caller pass them in? Passing
    them in brings back model-written content.
  4. Where does single-player registration live once Git leaves the
    role's shell?
  5. Does commit run the project's Git hooks?
  6. Is the Job Site's escalation issue an SCM action or a WMS action?
  7. Which identity does each caller act as? The Drafting Table commits
    as the user; the Job Site needs a bot or app identity
    (Multi-Player Workflow, bot account model).
  8. Does the Projector read canonical source through the SCM, or with
    its own read-only credential? The operations above read state, not
    tree content at a commit.

Scope

Define the SCM's responsibility, callers, operation set per role,
authorization model, deployment topology in single-player,
multi-player, and Web modes, host adapter boundary, failure mapping,
and audit record. Decide the shape and the packaging. Do not implement
the Job Site face here.

Related contracts

  • #34 defines the Git rules the SCM enforces.
  • #33 defines the harness shell operations the SCM replaces for Git.
  • #30 defines ears-manager, which the SCM reads.
  • #31 and #32 define the WMS Adapter and Validation Rules, the model
    for a governed boundary.

Completion criteria

  • A design document under docs/architecture/ defines the SCM, and
    components.md and architecture.md register it.
  • An operation matrix names caller, role, object, refs, authorization,
    result, and failure for every operation.
  • The #34 repository fixture runs against the SCM, including every
    negative check, with no shell in the caller.
  • The adapter contract, both harness bindings, git-integration.md,
    and the Architecture's strawman and tool inventory are updated to use
    it.
  • Single-player, multi-player, and Web differ only in where the SCM
    runs and where its credential comes from.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.