packages/dsh-git-graph
zhu1090093659/dsh-web输入框上方提供 Git 分支选择器,并把分支泳道与提交历史画成图谱,沿着时间线找到任意变更。
AI 编码智能体分支角色守卫:可配置集成/预览/生产/归档角色,拦截对受保护分支的直接推送、强推与删除,以及智能体对生产/归档分支的合并。
Are you tired of agents skipping your GitFlow?
A configurable branch-role guard for AI coding agents — Claude Code, Codex, OpenCode, Antigravity, CodeBuddy, ZCode, Cursor, DeepSeek Harness (DSH), and Pi. You define your own branches — integration (features merge in via PR/MR), preview (env endpoints), production, archive — each with its own update rules. Agents can't skip the flow, and sensitive merges stay in your hands.
English · 简体中文 · 繁體中文 · 日本語 · 한국어 · Deutsch · Français · Italiano · Português · Español · Русский · License
Step 1 — install. All nine clients consume the same npm package agents-gitflow-guard — choose the installation mode matching your agent:
# Mode A: CLI Hook clients (Claude Code · Codex · OpenCode · Antigravity · CodeBuddy · ZCode · Cursor)
npm i -g agents-gitflow-guard# Mode B: DSH in-process plugin (restart DSH afterwards; plugins load at startup)
dsh plugin --profile web add agents-gitflow-guard# Mode C: Pi in-process extension
npm i -D agents-gitflow-guardNote: A bareaddornpm iinstalls the latest version from npm registry. If your registry mirror has a cache delay or you need to lock to a specific version, append@(e.g.npm i -g agents-gitflow-guard@). The DSH-specific peer dependencies (@deepseek-ai/cordis/@deepseek-ai/dsh-tools) are declared optional — only the DSH in-process plugin needs them, and DSH supplies them through its shared profile module fallback at runtime; CLI / Pi / OpenCode users are not forced to install them.
CLI hook clients perform one wiring command after install (see Step 2); Pi copies an extension file; DSH auto-mounts on plugin installation.
Step 2 — wire your client (no config file needed). The guard ships with built-in defaults that protect `develop` (integration) + `main` (archive) — zero configuration, on by default. The only thing you need is to tell your AI client to invoke the guard, with one command per stdin-hook client (DSH is wired automatically; Pi just copies a file, see below):
# Claude Code → this repo's .claude/settings.json
gitflow-guard wire --client claude --project --yes# Codex / OpenCode / Antigravity / CodeBuddy / ZCode / Cursor (each its own config file; --yes skips the y/N prompt)
gitflow-guard wire --client codex --project --yes
gitflow-guard wire --client opencode --project --yes
gitflow-guard wire --client antigravity --project --yes
gitflow-guard wire --client codebuddy --project --yes
gitflow-guard wire --client zcode --project --yes
gitflow-guard wire --client cursor --project --yes# Preview (no writes) / remove / interactive guide:
gitflow-guard wire --client claude --dry-run
gitflow-guard wire --client claude --unwire
gitflow-guard setupwire merges into your existing config non-destructively (already-present hooks are left untouched; legacy gitflow-guard entries are migrated to the current self-anchored form on re-run), and writes to your project dir by default — --global (all repos on this machine) always asks first or needs --yes. Per-client files and formats are mirrored in Installation in detail.
⚠️ main is protected by default. Trunk / single-branch users (everyone pushes straight to one branch) will get blocked on directmainpushes until they opt out — creategitflow-guard.config.jsonwith{ "enabled": false }, or map your own branches (see Configuration Reference).gitflow-guard statusrepeats this notice whenever the built-in defaults are in effect.
Step 3 — verify. Ask the agent to git push origin develop. Expect the tool call to be denied:
Error: [gitflow-guard] blocked: Protected branch "develop" forbids direct push
Next: Integration branch (develop) is updated via PR/MR from a feature branch: push the feature first, then `gh pr create --base develop` / `glab mr create --target-branch develop`.Messages are English by default; create a config with "locale": "zh" to switch to Chinese — messages then read like: *已拦截: 受保护分支「develop」禁止直推 / 下一步: 集成分支(develop)由 PR/MR 合入 feature……* (see Configuration Reference).
Done. The guard is live for this repo with the built-in defaults. Want more stages (preview / production) or different branch names? Write a gitflow-guard.config.json and only the fields you care about — everything else keeps the built-in defaults. For the full decision table, see the Gate Matrix.
Scenario: your team ships a login page (feature/login-page); develop is the integration branch, main the archive. What you and the agent experience at every step:
# · what the agent runs · plugin decision · what you see 1 · git checkout -b feature/login-page (from develop) · ✅ allow (feature work is free) · branch created 2 · git add . && git commit -m "feat: login" · ✅ allow · committed 3 · git push -u origin feature/login-page · ✅ allow (pushing your feature is fine) · pushed 4 · git checkout develop && git merge feature/login-page · 🚫 deny — integration branch is PR/MR-only · must open a PR/MR into develop 5 · gh pr create --base develop · ✅ allow (feature → integration via PR) · PR created, you review & merge 6 · git push origin main or merge into main · 🚫 deny — archive is user-hand only · you archive develop → main yourself after release
Notice what the agent *cannot* do: merge a feature straight into develop, or touch main at all. Every sensitive merge is a deliberate human action in the PR/MR page or your own terminal.
AI coding agents work in your repository. They are *told* — via system prompts, project instruction files (AGENTS.md, CLAUDE.md, GEMINI.md, .cursorrules, and similar), and project docs — to follow a merge flow: develop on a feature branch, merge into the integration branch (and your preview/production stages when you have them), and leave archive/production merges to you.
That is a soft rule. Agents skip it, reorder it, or simply "forget" it — not out of malice, but because soft instructions are optional to a model.
This plugin turns the soft rule into a hard mechanism. Every git operation an agent attempts is checked against the *actual state of your local repository*. Violations are blocked before the command runs, with an explanation of why and what to do next.
Nobody has to remember the rules — the rules are enforced.
develop-style integration branch up to multi-stage preview/production pipelines.develop without review. From then on, every entry to the protected stages requires a PR/MR — a deliberate, audited act.Not for you (see also What it does NOT do):
integration branch to protect.integration (built-in default: develop) is the core role; preview / production / archive are optional arrays of branch names or regexes, each with its own update rules (pr / flexible, mergeBy).~/.local/state/gitflow-guard/, %LOCALAPPDATA%\gitflow-guard on Windows) — outside the repository, never committed, outside the agent's writable sandbox, and shared across all linked worktrees of one repository.gh (GitHub) or glab (GitLab) for PR/MR target resolution, and works fine without them.integration branch; if everyone pushes straight to one branch, this plugin will block constantly — don't enable it there.Server-side branch protection (GitHub branch rules, GitLab protected branches) and this plugin solve different problems. They are complementary, not alternatives.
dimension · server-side protection · this plugin what it governs · *who* may push / merge to protected branches (permissions) · *how* agents may enter the flow (workflow) — which role a merge lands in keeps agents from merging into production/archive · no — it can't tell "agent did it" · yes — production/archive merges are blocked for agents by default per-role flexibility · one rule per branch on the host · per-role update (pr/flexible) + mergeBy (user/anyone) in one config file scope · every user of the repository, humans included · agents with the hook or plugin configured (humans are not restricted) enforcement point · server-side, at push / merge time · local, before the command runs platform · tied to the hosting service · pure local git, platform-agnostic (gh / glab optional) bypassable by · users with admin rights · any agent not wired to the guard, or a determined malicious agent
Why this matters: branch protection answers *"can this push happen at all?"*; this plugin answers *"may this agent enter this role, given the config?"*. The strongest setup uses both — the plugin keeps agents honest about the workflow, and branch protection guarantees that no one, agent or human, pushes straight to a protected branch.
pwsh / bash) with a git command.gitflow-guard.config.json, and applies the gate matrix.~/.local/state/gitflow-guard/repos/-/audit.jsonl).No chat-confirmation or permit store: sensitive merges (production / archive) are simply user-only — an agent may prepare the PR/MR, but the merge click stays yours.
Nothing about branch names or rules is hard-coded. integration ships as a built-in default (develop); preview / production / archive are optional arrays of exact names or regexes, each with its own update and mergeBy — deep-merged over the defaults. The same binary scales from a solo develop to an enterprise multi-env pipeline.
The guard hooks each platform's pre-tool event — DSH tools/pre-execute, Pi tool_call, and the CLI clients' PreToolUse hook — the decision point that runs *before* the command is dispatched. A deny there means the command never runs; the agent only ever sees the rejection. Post-hoc detection (scanning logs after the fact) can't work as enforcement — the damage would already be done.
No plugin code decides "is this merge OK?" for production or archive. The gate simply refuses to let an *agent* perform those merges, so the only path is a PR/MR page where you click merge — and that click is the confirmation. There is no token, permit, or chat message an agent could forge to get past you.
The guard is on by default — no gitflow-guard.config.json needed. It protects:
default · role · rule develop · integration · no direct push; updates via PR/MR (update: "pr") main · archive · no direct push / no agent merge; the archive merge is yours (mergeBy: "user")
When you do create gitflow-guard.config.json, its fields are deep-merged over the defaults: each field/role you write replaces the default for that field/role, everything you don't write keeps the default. Write only what you want to change:
{
"branches": { "production": ["release-[\\w-]+"] } // defaults keep develop+main; production is added
}Disable entirely (trunk / single-branch flows): { "enabled": false }. Fixing an accidental block is a one-file change, and gitflow-guard status always explains what is in effect (including when it is the built-in defaults).
A role maps branch names (or regexes) to a rule set. integration is provided by the defaults; every other role is optional.
feature branches ──(free)──> integration (integration branch; updates via PR/MR)
│
├──> preview (optional; env endpoints; updates via PR/MR)
│
└──> production (optional; PR/MR + only you click merge)
archive (optional; you archive after release)role · config key · required? · enforced behavior feature · featurePattern · — · free: commit / push / sync / rebase integration · branches.integration · default (develop) · no direct push (default pr); features merge in via PR/MR preview · branches.preview (array) · optional · no direct push; updates via PR/MR only (env endpoints) production · branches.production (array) · optional · PR/MR only; merge by user only (mergeBy: "user") archive · branches.archive (array) · default (main) · archive PR/MR may be created by agents; the merge stays user-hand only
Small team (solo / 2–3 devs) — minimal: integration only:
{
"enabled": true,
"featurePattern": "feature/[\\w-]+",
"branches": { "integration": ["develop"] }
}Larger team (multiple preview envs + production + archive):
{
"enabled": true,
"featurePattern": "(topic|feature)/[\\w-]+",
"branches": {
"integration": ["develop", "topic/[\\w-]+"],
"preview": {
"branches": ["ita1", "itb1", "itb2", "sg", "vb", "r1-conf", "r1-ope", "r2-conf", "r2-ope"],
"update": "pr"
},
"production": {
"branches": ["prd-conf", "prd-ope"],
"update": "pr",
"mergeBy": "user"
},
"archive": ["main"]
}
}{
"enabled": true, // default true — set false to turn the guard off
"featurePattern": "feature/[\\w-]+", // JS regex matching your working/feature branches
"branches": {
"integration": { "branches": ["develop"], "update": "pr" }, // default: ["develop"] — omit to keep
"preview": { "branches": ["ita1"], "update": "pr" }, // optional
"production": { "branches": ["prd"], "update": "pr", "mergeBy": "user" }, // optional
"archive": ["main"] // optional
},
"worktree": { // optional: working tree and upstream baseline guard
"requireCleanOnPr": false, // require clean staged/unstaged changes before creating PR (default false)
"requireCleanOnMerge": false, // require clean working tree before merging (default false)
"allowUntracked": true, // allow untracked files (??); false blocks if untracked exist (default true)
"requireUpstreamSynced": false // require branch to be synced with upstream baseline (default false)
},
"locale": "en", // optional: message language — any registered locale ('en'/'zh' built-in); unknown values warn in status and fall back to English
"strict": false, // optional: fail-closed — invalid config / internal errors block instead of warn-and-allow
"ci": { "enabled": true } // optional: gh pr checks logged as reference
}{ branches, update?, mergeBy? }.update: pr (default) = updates only via PR/MR; flexible = allow direct/local merges (small teams).mergeBy (production): user (default) = only you click merge; anyone = allow PR merge through.requireCleanOnPr: true blocks PR creation if there are uncommitted staged/unstaged changes; requireCleanOnMerge: true blocks local and PR merges on dirty working trees; allowUntracked (default true) allows untracked files (??) without friction, or set false for strict human-agent collaboration; requireUpstreamSynced: true blocks PR creation when the branch is behind the upstream baseline (behind > 0). Multi-segment compound commands (e.g. git add . && git commit && gh pr create) dynamically simulate a clean state for subsequent segments.^(?:)$), so write expressions that cover the entire name — substring-style patterns like feature/ or a bare fix won't match, and those branches fall through to other. Avoid catastrophic-backtracking constructs (e.g. nested quantifiers like (\w+)+) in featurePattern and branch entries."locale": "zh" for Chinese, or pass --locale to any gitflow-guard subcommand (priority: CLI flag > project config > English). All user-facing text follows the locale — including CLI framework messages such as --help, unknown-command notices, and the empty-audit line.import { registerLocale } from 'agents-gitflow-guard', call registerLocale('fr', frDict) with a dictionary covering exactly the same keys as built-in English (validated on registration), then set "locale": "fr" in the project config to activate it.import { registerLocale, MESSAGE_KEYS } from 'agents-gitflow-guard'
// MESSAGE_KEYS lists every key a dictionary must define (same set as built-in English);
// registration throws if a key is missing or extra.
const fr = { /* one entry per MESSAGE_KEYS, e.g. */ 'deny.header': ({ why }) => `[gitflow-guard] bloqué : ${why}` }
registerLocale('fr', fr)"locale" value falls back to English during interception (by design — hooks never stall on wording), so a typo is easy to miss; the one-line warning shows up in gitflow-guard status.main to integration while the default archive is still main) is an overlap error — cover or drop the other role too."strict": true flips config errors and internal errors to block (fail-closed) — for high-risk repos. An explicit enabled: false stays silent; a *missing* file is not an error anymore