agents-gitflow-guard 0.0.13 → 0.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -42,7 +42,7 @@ You define your own branches —
42
42
  # installs the latest release
43
43
  dsh plugin --profile web add agents-gitflow-guard
44
44
  # ...or pin an exact known-good version (recommended; also bypasses stale registry caches)
45
- dsh plugin --profile web add agents-gitflow-guard@0.0.13
45
+ dsh plugin --profile web add agents-gitflow-guard@0.0.15
46
46
  ```
47
47
 
48
48
  > **Version gotcha**: a bare `add` resolves whatever `latest` is at install time — on machines behind a stale npm/pnpm registry cache or mirror it may install an old version. If the installed version looks wrong, pin it explicitly. The peer-dependency *warning* pnpm may print is expected: DSH supplies `@deepseek-ai/cordis` / `@deepseek-ai/dsh-tools` through its shared profile module fallback at startup (the plugin works normally).
@@ -132,7 +132,7 @@ Nobody has to remember the rules — the rules are enforced.
132
132
  - **Role-driven, fully configurable**: `integration` is the only required role; `preview` / `production` / `archive` are optional arrays of branch names or regexes, each with its own update rules (`pr` / `flexible`, `mergeBy`).
133
133
  - **Merge-by-user where it matters**: production & archive merges stay in your hands — the plugin blocks the agent from clicking merge, so your action *is* the confirmation.
134
134
  - **Works with any naming**: branch names are mapped by your config, never hard-coded (see [Configuration](#configuration-reference)).
135
- - **Fully audited**: every deny is written to `.git/gitflow-guard/audit.jsonl` — inside `.git`, never committed.
135
+ - **Fully audited**: every deny is appended to an audit log under your user state directory (`~/.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.
136
136
  - **Platform-agnostic core**: pure local git; optionally consults `gh` (GitHub) or `glab` (GitLab) for PR/MR target resolution, and works fine without them.
137
137
 
138
138
  ---
@@ -168,7 +168,7 @@ Why this matters: branch protection answers *"can this push happen at all?"*; th
168
168
 
169
169
  1. An agent calls a shell tool (`pwsh` / `bash`) with a git command.
170
170
  2. The plugin classifies the command, resolves the branch roles from `gitflow-guard.config.json`, and applies the gate matrix.
171
- 3. Violation → the tool call is **denied before it runs**, with a reason and the next step. Allowed → the command proceeds; every deny is audited to `.git/gitflow-guard/audit.jsonl`.
171
+ 3. Violation → the tool call is **denied before it runs**, with a reason and the next step. Allowed → the command proceeds; every deny is audited to the user-level log (`~/.local/state/gitflow-guard/repos/<repo>-<hash>/audit.jsonl`).
172
172
 
173
173
  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.
174
174
 
@@ -302,7 +302,7 @@ The PR/MR target is resolved via `gh pr view` (GitHub) or `glab mr view` (GitLab
302
302
  ## Where the human stays in control
303
303
 
304
304
  - **Production merge** and **archive** are user-only by default: an agent may help prepare the PR/MR, but **you click the merge button** — that click *is* the confirmation. There is no separate permit store to outsource that decision.
305
- - Every deny is written to `.git/gitflow-guard/audit.jsonl` for review (`gitflow-guard audit`).
305
+ - Every deny is appended to the user-level audit log for review (`gitflow-guard audit`).
306
306
 
307
307
  ---
308
308
  ## Installation in detail
@@ -312,7 +312,7 @@ The PR/MR target is resolved via `gh pr view` (GitHub) or `glab mr view` (GitLab
312
312
  **From the npm registry** — the standard path, already covered in [Quick Start](#quick-start--30-seconds-to-a-guarded-repo):
313
313
 
314
314
  ```bash
315
- dsh plugin --profile web add agents-gitflow-guard@0.0.13 # pin recommended, see note above
315
+ dsh plugin --profile web add agents-gitflow-guard@0.0.15 # pin recommended, see note above
316
316
  ```
317
317
 
318
318
  Then restart DSH. Upgrades are the same command, followed by another restart.
@@ -471,7 +471,7 @@ If it saves your team from a shortcut gone wrong, the coffee button at the top o
471
471
  ## Roadmap
472
472
 
473
473
  - **i18n — localized block messages** ✅ (0.0.3): English by default, `"locale": "zh"` for Chinese.
474
- - **v2 — audit sync**: sync `.git/gitflow-guard/audit.jsonl` across machines (audit is local-only today).
474
+ - **v2 — audit sync**: sync the user-level audit log across machines (audit is local-only today).
475
475
  - **v2 — more pre-built templates**: ready-made config templates for common flows (solo `develop`, multi-env enterprise) as community-contributed presets.
476
476
  - **v2 — CI hard-gating research**: whether `pr checks` could become a real gate without hurting the platform-agnostic core.
477
477
 
package/README.zh.md CHANGED
@@ -41,7 +41,7 @@
41
41
  # 安装最新版
42
42
  dsh plugin --profile web add agents-gitflow-guard
43
43
  # ...或锁定已知良好版本(推荐; 同时绕开 registry 陈旧缓存)
44
- dsh plugin --profile web add agents-gitflow-guard@0.0.13
44
+ dsh plugin --profile web add agents-gitflow-guard@0.0.15
45
45
  ```
46
46
 
47
47
  > **版本坑**: 裸 `add` 装的是安装时刻的 `latest`——在 npm/pnpm 注册表缓存或镜像陈旧的机器上可能拿到旧版本。看到版本不对就锁版本。pnpm 打印的 peer 依赖 *警告* 属预期: DSH 启动时经共享模块回退提供 `@deepseek-ai/cordis` / `@deepseek-ai/dsh-tools`(插件正常工作)。
@@ -129,7 +129,7 @@ AI 编码 agent 在你的仓库里工作。它通过系统提示词、项目智
129
129
  - **角色驱动、完全可配**:`integration` 是唯一必填;`preview` / `production` / `archive` 是可选数组(精确名或正则),每个角色独立 `update`(`pr` / `flexible`)与 `mergeBy`。
130
130
  - **在关键处保留人的操作权**:生产与归档合并始终在你手上——插件阻止 agent 点击合并,于是你的动作*就是*确认。
131
131
  - **任何命名都行**:分支名全由配置映射,绝无硬编码(见[配置参考](#配置参考))。
132
- - **全程审计**:每次拦截都写入 `.git/gitflow-guard/audit.jsonl`——在 `.git` 内,绝不进版本库。
132
+ - **全程审计**:每次拦截都追加到用户级状态目录(macOS/Linux `~/.local/state/gitflow-guard/`,Windows `%LOCALAPPDATA%\gitflow-guard`)下的审计日志——在仓库外、绝不进版本库、位于 agent 可写沙箱之外,且同一仓库的所有 worktree 共享一份日志。
133
133
  - **平台无关核心**:纯本地 git;可选调用 `gh`(GitHub)或 `glab`(GitLab)做 PR/MR 目标解析,没有它们照样工作。
134
134
 
135
135
  ---
@@ -165,7 +165,7 @@ AI 编码 agent 在你的仓库里工作。它通过系统提示词、项目智
165
165
 
166
166
  1. agent 调用 shell 工具(`pwsh`/`bash`)执行一条 git 命令。
167
167
  2. 插件分类该命令,从 `gitflow-guard.config.json` 解析分支角色,套用门禁矩阵。
168
- 3. 违规 → 工具调用在**运行前被拒绝**,附原因和下一步;放行 → 命令照常执行,每次拦截都写入 `.git/gitflow-guard/audit.jsonl`。
168
+ 3. 违规 → 工具调用在**运行前被拒绝**,附原因和下一步;放行 → 命令照常执行,每次拦截都写入用户级日志(`~/.local/state/gitflow-guard/repos/<repo>-<hash>/audit.jsonl`)。
169
169
 
170
170
  没有聊天确认、也没有特许库:敏感合并(生产/归档)就是**仅用户**——agent 可以帮你准备 PR/MR,但点合并的始终是你。
171
171
 
@@ -298,7 +298,7 @@ PR/MR 目标通过 `gh pr view`(GitHub)或 `glab mr view`(GitLab)解析;没有
298
298
  ## 人保持控制权的地方
299
299
 
300
300
  - **生产合并与归档**默认仅用户:agent 可以帮你准备 PR/MR,但**合并按钮由你点**——那个点击*就是*确认。没有独立特许库能把这决定外包出去。
301
- - 每次拦截都写入 `.git/gitflow-guard/audit.jsonl` 供查阅(`gitflow-guard audit`)。
301
+ - 每次拦截都追加到用户级审计日志供查阅(`gitflow-guard audit`)。
302
302
 
303
303
  ---
304
304
  ## 安装详解
@@ -308,7 +308,7 @@ PR/MR 目标通过 `gh pr view`(GitHub)或 `glab mr view`(GitLab)解析;没有
308
308
  **从 npm registry**——标准路径,已在[快速开始](#快速开始30-秒用上)覆盖:
309
309
 
310
310
  ```bash
311
- dsh plugin --profile web add agents-gitflow-guard@0.0.13 # 建议锁版本, 见上文提示
311
+ dsh plugin --profile web add agents-gitflow-guard@0.0.15 # 建议锁版本, 见上文提示
312
312
  ```
313
313
 
314
314
  然后重启 DSH。升级用同一命令,再重启一次。
@@ -467,7 +467,7 @@ MIT,免费,无条件。随便用、随便改、随便发,唯一义务是保留
467
467
  ## 路线图
468
468
 
469
469
  - **i18n——拦截文案本地化** ✅(0.0.3):默认英文,`"locale": "zh"` 切中文。
470
- - **v2——审计同步**:跨机器同步 `.git/gitflow-guard/audit.jsonl`(现仅本地)。
470
+ - **v2——审计同步**:跨机器同步用户级审计日志(现仅本地)。
471
471
  - **v2——更多预制模板**:常用流程(solo `develop`、多环境企业)的现成配置模板,由社区贡献。
472
472
  - **v2——CI 硬门槛研究**:`pr checks` 能否在不伤平台无关核心的前提下变成真实门槛。
473
473
 
package/lib/cli.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { c as findRepoRoot, d as roleMatches, g as classify, h as resolveLocale, i as formatDeny, l as gitRunner, o as stateDir, p as makeT, r as evaluateCommand, s as currentBranch, u as loadConfig } from "./src-BHyQJ6hX.mjs";
1
+ import { _ as classify, c as currentBranch, d as loadConfig, f as roleMatches, g as resolveLocale, i as formatDeny, l as findRepoRoot, m as makeT, o as stateDir, r as evaluateCommand, u as gitRunner } from "./src-p6q9dCIr.mjs";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
4
  //#region src/platform.ts
@@ -129,7 +129,7 @@ async function main(argv, opts = {}) {
129
129
  }
130
130
  try {
131
131
  if (cmd === "status") return await status(flags, runner);
132
- if (cmd === "audit") return await audit(flags);
132
+ if (cmd === "audit") return await audit(flags, runner);
133
133
  if (cmd === "check") return await check(flags);
134
134
  const t = makeT(await resolveFrameworkLocale(flags, runner));
135
135
  console.error(`${t("cli.unknownCommand", { cmd: cmd ?? "" })}\n\n${t("usage.text")}`);
@@ -192,7 +192,7 @@ async function status(flags, runner) {
192
192
  for (const b of localBranches) console.log(` ${b} → ${classifyBranch(b)}`);
193
193
  return 0;
194
194
  }
195
- async function audit(flags) {
195
+ async function audit(flags, runner) {
196
196
  const repoRoot = await resolveRepo(flags, gitRunner);
197
197
  if (!repoRoot) {
198
198
  console.error(makeT(resolveLocale(flags.locale))("cli.cannotLocate"));
@@ -202,7 +202,7 @@ async function audit(flags) {
202
202
  const t = makeT(cliLocale(flags, config?.locale));
203
203
  const lines = flags.lines != null && Number.isFinite(flags.lines) && flags.lines > 0 ? Math.floor(flags.lines) : 20;
204
204
  try {
205
- const all = (await readFile(join(stateDir(repoRoot), "audit.jsonl"), "utf8")).split("\n").filter(Boolean);
205
+ const all = (await readFile(join(await stateDir(repoRoot, gitRunner), "audit.jsonl"), "utf8")).split("\n").filter(Boolean);
206
206
  for (const line of all.slice(-lines)) try {
207
207
  const e = JSON.parse(line);
208
208
  console.log(` ${new Date(e.time).toISOString()} ${e.event} ${e.role ?? ""}${e.command ? ` | ${e.command.slice(0, 80)}` : ""}${e.reason ? ` | ${e.reason.slice(0, 60)}` : ""}`);
package/lib/index.d.mts CHANGED
@@ -53,12 +53,24 @@ interface AuditEntry {
53
53
  role?: string;
54
54
  reason?: string;
55
55
  }
56
- declare function stateDir(repoRoot: string): string;
56
+ /**
57
+ * 用户级运行时状态根目录(仓库外): macOS/Linux 走 XDG state, Windows 走 %LOCALAPPDATA%。
58
+ * GITFLOW_GUARD_STATE_ROOT 显式覆盖所有平台默认值(测试/特殊部署用)。
59
+ */
60
+ declare function userStateRoot(): string;
61
+ /**
62
+ * 仓库运行时状态目录(审计流水等), 键为「仓库名-真实路径哈希」。
63
+ * 键由 git 权威解析(rev-parse --git-common-dir): linked worktree 与主仓库共用
64
+ * 同一状态目录(≤0.0.13 共享语义), 且天然规避 Windows 8.3 短名/大小写差异;
65
+ * git 查询不可用时回退 fs realpath。刻意放在仓库外、且在 agent 文件沙箱
66
+ * (workspace-write)可写区之外 —— 凡 agent 可写之处的状态都可能被伪造而自我授权。
67
+ */
68
+ declare function stateDir(repoRoot: string, runner?: Runner): Promise<string>;
57
69
  /** 审计留痕; 失败不阻断门禁 */
58
- declare function appendAudit(repoRoot: string, entry: AuditEntry): Promise<void>;
70
+ declare function appendAudit(repoRoot: string, entry: AuditEntry, runner?: Runner): Promise<void>;
59
71
  /** 解析一条命令: 分类 → git 事实 → 门禁 → allow/deny */
60
72
  declare function evaluateCommand(command: string, opts: EvaluateOptions): Promise<EvaluateResult>;
61
73
  declare function formatDeny(locale: Locale, why: string, next: string): string;
62
74
  declare function apply(ctx: Context, pluginConfig?: PluginConfig): void;
63
75
  //#endregion
64
- export { AuditEntry, type Dict, EvaluateOptions, EvaluateResult, MESSAGE_KEYS, PluginConfig, appendAudit, apply, evaluateCommand, formatDeny, name, registerLocale, stateDir };
76
+ export { AuditEntry, type Dict, EvaluateOptions, EvaluateResult, MESSAGE_KEYS, PluginConfig, appendAudit, apply, evaluateCommand, formatDeny, name, registerLocale, stateDir, userStateRoot };
package/lib/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { a as name, f as MESSAGE_KEYS, i as formatDeny, m as registerLocale, n as apply, o as stateDir, r as evaluateCommand, t as appendAudit } from "./src-BHyQJ6hX.mjs";
2
- export { MESSAGE_KEYS, appendAudit, apply, evaluateCommand, formatDeny, name, registerLocale, stateDir };
1
+ import { a as name, h as registerLocale, i as formatDeny, n as apply, o as stateDir, p as MESSAGE_KEYS, r as evaluateCommand, s as userStateRoot, t as appendAudit } from "./src-p6q9dCIr.mjs";
2
+ export { MESSAGE_KEYS, appendAudit, apply, evaluateCommand, formatDeny, name, registerLocale, stateDir, userStateRoot };
@@ -1,5 +1,8 @@
1
1
  import { appendFile, mkdir, readFile } from "node:fs/promises";
2
- import { join } from "node:path";
2
+ import { realpathSync } from "node:fs";
3
+ import { createHash } from "node:crypto";
4
+ import { homedir } from "node:os";
5
+ import { basename, join } from "node:path";
3
6
  import { execFile } from "node:child_process";
4
7
  //#region src/classify.ts
5
8
  /** 拆分命令为多段(&& / || / | / 分号 / 换行), 每段独立分类; 引号内的分隔符不算 */
@@ -1018,6 +1021,15 @@ async function findRepoRoot(runner, cwd) {
1018
1021
  const r = await runner.run(["rev-parse", "--show-toplevel"], cwd);
1019
1022
  return r.code === 0 ? r.stdout.trim() || null : null;
1020
1023
  }
1024
+ /** 共享 .git 目录(git 权威绝对路径): linked worktree 返回主仓库 .git, 主仓库返回自身 .git; 查询失败返回 null */
1025
+ async function commonGitDir(runner, cwd) {
1026
+ const r = await runner.run([
1027
+ "rev-parse",
1028
+ "--path-format=absolute",
1029
+ "--git-common-dir"
1030
+ ], cwd);
1031
+ return r.code === 0 ? r.stdout.trim() || null : null;
1032
+ }
1021
1033
  async function currentBranch(runner, cwd) {
1022
1034
  const r = await runner.run(["branch", "--show-current"], cwd);
1023
1035
  return r.code === 0 ? r.stdout.trim() || null : null;
@@ -1112,14 +1124,51 @@ function resolvePrTarget(info, config) {
1112
1124
  //#endregion
1113
1125
  //#region src/index.ts
1114
1126
  const name = "gitflow-guard";
1115
- function stateDir(repoRoot) {
1116
- return join(repoRoot, ".git", "gitflow-guard");
1127
+ /**
1128
+ * 用户级运行时状态根目录(仓库外): macOS/Linux 走 XDG state, Windows 走 %LOCALAPPDATA%。
1129
+ * GITFLOW_GUARD_STATE_ROOT 显式覆盖所有平台默认值(测试/特殊部署用)。
1130
+ */
1131
+ function userStateRoot() {
1132
+ const override = process.env.GITFLOW_GUARD_STATE_ROOT?.trim();
1133
+ if (override) return override;
1134
+ if (process.platform === "win32") {
1135
+ const local = process.env.LOCALAPPDATA?.trim() || join(homedir(), "AppData", "Local");
1136
+ return join(local, "gitflow-guard");
1137
+ }
1138
+ const xdg = process.env.XDG_STATE_HOME?.trim();
1139
+ return join(xdg || join(homedir(), ".local", "state"), "gitflow-guard");
1140
+ }
1141
+ /** fs 兜底规范化(仅当 git 查询不可用时使用; Windows 8.3 短名等以 git 输出为准) */
1142
+ function safeRealpath(p) {
1143
+ try {
1144
+ return realpathSync(p);
1145
+ } catch {
1146
+ return p;
1147
+ }
1148
+ }
1149
+ /**
1150
+ * 仓库运行时状态目录(审计流水等), 键为「仓库名-真实路径哈希」。
1151
+ * 键由 git 权威解析(rev-parse --git-common-dir): linked worktree 与主仓库共用
1152
+ * 同一状态目录(≤0.0.13 共享语义), 且天然规避 Windows 8.3 短名/大小写差异;
1153
+ * git 查询不可用时回退 fs realpath。刻意放在仓库外、且在 agent 文件沙箱
1154
+ * (workspace-write)可写区之外 —— 凡 agent 可写之处的状态都可能被伪造而自我授权。
1155
+ */
1156
+ async function stateDir(repoRoot, runner = gitRunner) {
1157
+ let key = safeRealpath(repoRoot);
1158
+ try {
1159
+ const common = await commonGitDir(runner, repoRoot);
1160
+ if (common) key = common.replace(/[/\\]\.git[/\\]?$/, "") || common;
1161
+ } catch {}
1162
+ const hash = createHash("sha256").update(key).digest("hex").slice(0, 12);
1163
+ const name = basename(key).replace(/[^\w.-]+/g, "-") || "repo";
1164
+ return join(userStateRoot(), "repos", `${name}-${hash}`);
1117
1165
  }
1118
1166
  /** 审计留痕; 失败不阻断门禁 */
1119
- async function appendAudit(repoRoot, entry) {
1167
+ async function appendAudit(repoRoot, entry, runner = gitRunner) {
1120
1168
  try {
1121
- await mkdir(stateDir(repoRoot), { recursive: true });
1122
- await appendFile(join(stateDir(repoRoot), "audit.jsonl"), `${JSON.stringify(entry)}\n`, "utf8");
1169
+ const dir = await stateDir(repoRoot, runner);
1170
+ await mkdir(dir, { recursive: true });
1171
+ await appendFile(join(dir, "audit.jsonl"), `${JSON.stringify(entry)}\n`, "utf8");
1123
1172
  } catch {}
1124
1173
  }
1125
1174
  /** 解析一条命令: 分类 → git 事实 → 门禁 → allow/deny */
@@ -1158,7 +1207,7 @@ async function evaluateCommand(command, opts) {
1158
1207
  event: "deny",
1159
1208
  command,
1160
1209
  reason: decision.reason
1161
- });
1210
+ }, env.runner);
1162
1211
  return {
1163
1212
  outcome: "deny",
1164
1213
  reason: {
@@ -1189,7 +1238,7 @@ async function logCiReference(seg, env) {
1189
1238
  event: "ci",
1190
1239
  command: seg.pr ?? void 0,
1191
1240
  role: state
1192
- });
1241
+ }, env.runner);
1193
1242
  }
1194
1243
  /** 按段预取 git 事实(异步 I/O 全部前置, 门禁保持纯函数) */
1195
1244
  async function factsFor(seg, env) {
@@ -1242,4 +1291,4 @@ function apply(ctx, pluginConfig = {}) {
1242
1291
  });
1243
1292
  }
1244
1293
  //#endregion
1245
- export { name as a, findRepoRoot as c, roleMatches as d, MESSAGE_KEYS as f, classify as g, resolveLocale as h, formatDeny as i, gitRunner as l, registerLocale as m, apply as n, stateDir as o, makeT as p, evaluateCommand as r, currentBranch as s, appendAudit as t, loadConfig as u };
1294
+ export { classify as _, name as a, currentBranch as c, loadConfig as d, roleMatches as f, resolveLocale as g, registerLocale as h, formatDeny as i, findRepoRoot as l, makeT as m, apply as n, stateDir as o, MESSAGE_KEYS as p, evaluateCommand as r, userStateRoot as s, appendAudit as t, gitRunner as u };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-gitflow-guard",
3
- "version": "0.0.13",
3
+ "version": "0.0.15",
4
4
  "description": "A configurable branch-role guard for AI coding agents (DSH / Claude Code / Codex / OpenCode / Antigravity) — integration/preview/production/archive, each with its own update rules",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -60,7 +60,9 @@
60
60
  "prepack": "npm run build",
61
61
  "test": "vitest run",
62
62
  "typecheck": "tsc --noEmit",
63
- "verify:matrix": "npm run build && node scripts/verify-matrix.mjs"
63
+ "verify:matrix": "npm run build && node scripts/verify-matrix.mjs",
64
+ "check:pins": "node scripts/check-version-pins.mjs",
65
+ "prepublishOnly": "npm run check:pins"
64
66
  },
65
67
  "peerDependencies": {
66
68
  "@deepseek-ai/cordis": "^4.0.1",