redhat-et / redhat-et/ProtoBot
Define a Source Control Manager for governed Git and Git host operations
@lukaskellerstein is already working on this.
Since Sep 16, 2026.
- 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:
- 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. - 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. - 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. - 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
- Tool exposure. A caller gets only the operations of its role. A
harness binding loads only the Drafting Table operations. - 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. - 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:
- 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. - 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. - It does not remove the shell.
ears-managerstill 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. - 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 hasgit
andgh. 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). - 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. - 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. - It creates a split transaction. One change set then spans two
deterministic tools:ears-managerwrites 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 ofears-managerexists to avoid
(ears-managerdesign principles). - Smaller changes close the text gap. The model-written commit
message and PR body do not need a new component.ears-managercan
render both deterministically, and the agent passes the rendered
text through unchanged. - 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. - Every binding carries more. Each harness registers one more MCP
server and, in multi-player mode, authenticates to it, as H13 does
forwms. The shell rules shrink, but the MCP and authentication
surface grows, and a harness with weak MCP OAuth support gets
harder to bind. - 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-managerfrom a shell command to tools. Then taking
Git out of the shell removes the shell from the role entirely.
Open questions
- One shared service, or a core library with one face per role?
- Who cuts the change-set branch:
ears-manager change-set create, or
branch_startbefore it? - Does the SCM read the manifest, digests,
compare, andimpact
fromears-manageritself, or does the caller pass them in? Passing
them in brings back model-written content. - Where does single-player registration live once Git leaves the
role's shell? - Does
commitrun the project's Git hooks? - Is the Job Site's escalation issue an SCM action or a WMS action?
- 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). - 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.mdandarchitecture.mdregister 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
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.