docs(rfc): rewrite the Layer 1 RFC after review [DMD-1900] - #709
Draft
martinsifra wants to merge 1 commit into
Draft
docs(rfc): rewrite the Layer 1 RFC after review [DMD-1900]#709martinsifra wants to merge 1 commit into
martinsifra wants to merge 1 commit into
Conversation
Two review passes (Opus + Sonnet) and a design discussion reshaped the
document. The command surface is unchanged; what changed is what may
happen without a human saying so, plus a batch of factual corrections.
Design decisions:
- Auto-merge is a destructive act, and classifying only `merge` as such
was theatre. A background scheduler runs every approved MR armed with
autoMergeStrategy through the SAME MergeProcessor under a system token
(AutoMergeCandidateRepository.php:44-47, AutoMergeTickHandler.php:86),
polling rather than hooking approve, retrying indefinitely. So
create/update escalate to destructive when they arm it, and
request-review/approve/resolve escalate on an already-armed MR.
Confirmation sits where a human CHOOSES an irreversible outcome
(merge, arming); escalation sits wherever one is CAUSED. `none` never
escalates -- it is the disarm, and escalating it would let
--deny-destructive lock the hazard in place.
- One rule replaces a per-command table: when an invocation resolves to
`destructive`, --json requires an explicit target. Derived from the
house rule that implicit branch resolution selects the SCOPE while the
target is always named (storage delete-table takes the active branch
but requires --table-id; branch delete requires --branch because there
the branch IS the target). A bare `--json merge` would satisfy neither
of the repo's two destructive shapes -- it neither prompts nor names.
Inverting --yes under --json was considered and rejected: zero
precedent across 48 commands carrying the flag.
- Flag is `--merge-request-id` (short alias `--id`), not `--mr-id`: a
bare noun is the context you work IN (--project 206x, --branch 94x),
`--<noun>-id` is the object you act ON (20 flags, ~170 uses).
- Every serve route declares require_permission, or the classification
above is decorative over HTTP (only auth.py does today).
Corrections:
- The module split is justified qualitatively; output.py is at 679 CODE
lines of 1000, not the 1013 raw lines previously cited (CONTRIBUTING
measures code lines -- the exact mistake it warns about).
- The wire field list is deleted, not fixed: the notes doc is the
authority. The previous list was wrong three ways (createdAt exists,
mergerName is nested in merge{}, autoMerge* are response fields too).
- diff/allowed_actions claims are now true rather than aspirational --
Layer 2 applied the findings (get_config_diff derives the branch from
the MR; _enrich_row carries allowed_actions everywhere).
- Rich markup escaping, --state/--take pre-validation to exit 2, plain
ConfigError -> exit 5, the mandatory CLI-layer test file, the serve
wiring steps and route-order hazard, `resolve --take delete
--change-description` being silently dropped, `update` with no fields
being a no-op, and the branch-merge deprecation also resetting
active_branch_id.
E2E remains explicitly unsettled.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
RFC for the
kbagent merge-requestcommand group (Layer 1, DMD-1900). Docs only — one file,one commit, no code.
The intended order is L2 →
mainfirst, then L1 →main. This PR is based onms/dmd-1899/cli-layer-2only so the diff shows L1's changes alone — merging it as-is wouldfold Layer 1 into the Layer 2 branch, which is not the plan.
mainmainWhy a second RFC PR
#708 landed the first draft. Two independent review
passes (Opus + Sonnet) and a design session then changed several decisions materially — enough
that a patch-on-top would have been unreadable, so the document is rewritten. The base already
contains v1, so this diff is the delta.
The command surface did not change. What changed is what may happen without a human saying
so, plus a batch of factual corrections.
The decisions that need sign-off
1. Auto-merge is a destructive act, and classifying only
mergeas destructive was theatre.autoMergeStrategyis not metadata. A background scheduler(
AutoMergeScheduleProvider.php:24-26) selects everyapprovedMR armed with it and runs itthrough the same
MergeProcessorthe merge endpoint uses, under a system token(
AutoMergeCandidateRepository.php:44-47,AutoMergeTickHandler.php:86). It polls — the approvepath has no hook — and a blocked tick retries indefinitely. Order does not matter: arming
before approval fires when the state arrives. So
create --auto-merge-strategy immediatelyfollowed by
request-reviewends in a production merge withmergenever called, and nothingkbagent returns reports it.
Consequently
create/updateescalate todestructivewhen they arm it, andrequest-review/approve/resolveescalate on an already-armed MR. The dividing line:--auto-merge-strategy nonenever escalates — it is the disarm, and escalating it would let--deny-destructivelock the hazard in place.2. One rule replaces a per-command safety table.
Derived from a house rule that holds without exception across the repo: implicit branch
resolution selects the scope; the target is always named on the command line.
storage delete-tabletakes the active branch but requires--table-id;branch deleterequires
--branchbecause there the branch is the target. A barekbagent --json merge-request merge --project acmewould satisfy neither of the repo's twodestructive shapes — it neither prompts nor names anything, and would be the first kbagent
command where nothing on the command line identifies what gets destroyed.
Humans keep the full active-branch fallback and get a prompt; scripts, which received the id in
the previous call's JSON, name it. Inverting
--yesunder--jsonwas considered andrejected — zero precedent across the 48 commands carrying that flag, and a familiar flag
with reversed semantics is worse than a rule that names the target.
3.
--merge-request-id(short alias--id), not--mr-id.Measured convention: a bare noun is the context you work in (
--project206×,--branch94×,--model23×);--<noun>-idis the object you act on (20 flags, ~170 uses). An MR is anobject, not a context.
--mr-idabbreviated the noun, which nothing else in the CLI does.4. Every
serveroute declaresrequire_permission.Today only
auth.pydoes; the other ~30 routers ignore the engine. Without it the entireclassification above is decorative for
servecallers.What the reviews caught
output.pyis at 679 code linesof a 1000 ceiling, not the 1013 raw lines cited. CONTRIBUTING measures code lines and warns
about exactly this mistake. The split still stands, on qualitative grounds.
--reviewer-idnames a real foot-gun but pointed at the wrong mechanism: on this stacktyper.Option(None, …)yieldsNonecorrectly; the danger is thetyper.Option([], …)stylelive at
commands/agent.py:862, which yields[]and silently clears the reviewer set.Fix [bold] parsingmangles thetable, an unbalanced
[/]raisesMarkupError. Ten command modules already importescape.the only automated coverage this work will have.
--state/--takeshould pre-validate to exit 2 (every peer flag does) rather thanreaching the service at exit 5; plain
ConfigErrorhad no exit-code story;resolve --take delete --change-descriptionsilently drops the text(
rebase_config_deletehas no such parameter);updatewith no fields is a server-sideno-op; serve wiring steps and the
by-branchroute-order hazard; andbranch mergealsounconditionally resets
active_branch_id, so calling it a harmless URL builder misleads.Not found by either review: the wire field list was wrong three ways —
createdAtexists,mergerNameis nested inmerge{}, andautoMergeStrategy/autoMergeAtare response fieldstoo. That surfaced only by reading
MergeRequestResponseProvider.php:86-117directly. The RFCnow deletes its field list rather than fixing it:
merge-requests-notes.mdis the authorityand this document must not carry a second copy.
Cross-layer
Seven findings surfaced while writing this went to Layer 2 and were applied in
07daa50, whichis in this PR's base. Two claims in the RFC were aspirational when written and are now simply
true:
get_config_diffderives the branch from the merge request, and_enrich_rowcarriesallowed_actionson every return.Open
E2E is deliberately unsettled and the RFC says so in place. No project carries
branches-merge-requests, kbagent cannot provision one (ManageClienthas no project create;Connection's suite makes its own), and the happy path necessarily merges into production. The
proposed path — enable the feature on the existing E2E project via
kbagent feature project-add, gate with aFEATURE_NOT_ENABLEDskip meanwhile — is recorded, not decided.Verification
Every
file:linecitation in the document was re-checked against the working tree and againstthe local
keboola/connectioncheckout. No code changed, so there is nothing to run — but notethat an RFC is a plan: nothing here is enforced by CI until the commands exist.
Worth pushing back on
The three judgement calls, in order of how much they would cost to reverse later:
--jsonexplicit-target rule for destructive invocations (an ergonomic tax on scripts)request-review/approve/resolveon an armed MR — deliberatelyconservative, because the required-approvals count is unreadable with a Storage token
(DMD-1969), so it escalates even where the
operation would not yet merge
serverouter in the same PR as the commands rather than as a follow-up🤖 Generated with Claude Code