@event4u/agent-config 14.14.1 → 14.16.0

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.
Files changed (40) hide show
  1. package/.augment-plugin/marketplace.json +3 -3
  2. package/.augment-plugin/plugin.json +1 -1
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CHANGELOG.md +88 -0
  5. package/dist/agent-src/rules/security-sensitive-stop.md +7 -5
  6. package/dist/agent-src/skills/accessibility-auditor/SKILL.md +20 -2
  7. package/dist/agent-src/skills/design-intelligence/data/ux-guidelines.csv +99 -99
  8. package/dist/agent-src/skills/fe-design/SKILL.md +6 -3
  9. package/dist/agent-src/skills/fe-design/references/design-patterns.md +28 -0
  10. package/dist/agent-src/skills/iconography/SKILL.md +37 -6
  11. package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +1 -1
  12. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  13. package/dist/cli-delegate/cmd_conformance.js +655 -34
  14. package/dist/discovery/deprecation-report.md +1 -1
  15. package/dist/discovery/discovery-manifest.json +7 -7
  16. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  17. package/dist/discovery/discovery-manifest.summary.md +1 -1
  18. package/dist/discovery/dormancy-report.md +1 -1
  19. package/dist/discovery/orphan-report.md +1 -1
  20. package/dist/discovery/packs.json +2 -2
  21. package/dist/discovery/trust-report.md +1 -1
  22. package/dist/discovery/workspaces.json +2 -2
  23. package/dist/hooks/dispatch.js +669 -45
  24. package/dist/mcp/registry-manifest.json +2 -2
  25. package/dist/mcp/server.json +2 -2
  26. package/dist/mcp/server.mjs +661 -35
  27. package/dist/router.json +1 -1
  28. package/docs/guidelines/design-antipatterns.md +10 -2
  29. package/package.json +1 -1
  30. package/src/config/gate-violation-baselines.json +2 -2
  31. package/src/config/rule-enforcement-baseline.json +0 -1
  32. package/src/scripts/_lib/release_highlights.ts +63 -7
  33. package/src/scripts/design_slop_rules.ts +19 -9
  34. package/src/scripts/git_authorization_hook.ts +719 -27
  35. package/src/scripts/hooks/block_unauthorized_git.ts +250 -9
  36. package/src/scripts/hooks/merge_impact.ts +275 -0
  37. package/src/scripts/release.ts +9 -34
  38. package/src/scripts/release_drill.ts +180 -8
  39. package/src/scripts/release_env.ts +31 -0
  40. package/src/scripts/release_publication.ts +158 -1
@@ -37256,13 +37256,16 @@ function isInterrogative(prose) {
37256
37256
  if (!t) {
37257
37257
  return false;
37258
37258
  }
37259
- const hasImperative = /\b(mach|mache|bitte|leg|lege|erstell|erstelle|f(ü|ue)hr|f(ü|ue)hre|setz|setze|starte|los|jetzt|go ahead|do it|ja[,.]?\s|ok[,.]?\s)\b/i.test(
37259
+ if (/\?\s*$/.test(t)) {
37260
+ return true;
37261
+ }
37262
+ const hasImperative = /\b(mach|mache|bitte|leg|lege|erstell|erstelle|f(ü|ue)hr|f(ü|ue)hre|setz|setze|starte|los|jetzt|now|asap|go ahead|do it|ja[,.]?\s|ok[,.]?\s)\b/i.test(
37260
37263
  t
37261
37264
  );
37262
37265
  if (hasImperative) {
37263
37266
  return false;
37264
37267
  }
37265
- return /\?\s*$/.test(t) || /^(was|wie|warum|wieso|weshalb|wann|wer|welche[rs]?|wo|ist|sind|kann|kannst|k(ö|oe)nnen|soll|sollen|darf|d(ü|ue)rfen|why|what|how|when|which|who|is|are|can|could|should|does|do)\b/i.test(
37268
+ return /^(was|wie|warum|wieso|weshalb|wann|wer|welche[rs]?|wo|ist|sind|kann|kannst|k(ö|oe)nnen|soll|sollen|darf|d(ü|ue)rfen|why|what|how|when|which|who|is|are|can|could|should|does|do)\b/i.test(
37266
37269
  t
37267
37270
  );
37268
37271
  }
@@ -37289,16 +37292,159 @@ function splitFences(prompt) {
37289
37292
  function _commandLines(fence) {
37290
37293
  return fence.split("\n").map((l) => l.trim()).filter((l) => l.length > 0).filter((l) => !/^(remote:|error:|hint:|fatal:|warning:|To |!\s|\s*\^)/i.test(l)).map((l) => l.replace(/^[$>#]\s*/, "")).filter((l) => /^(git|gh|npm|pnpm|yarn|task)\b/.test(l));
37291
37294
  }
37295
+ function _errText(err) {
37296
+ const t = err instanceof Error ? err.message : String(err);
37297
+ return t.replace(/\s+/g, " ").trim().slice(0, 200);
37298
+ }
37299
+ function extractMergeTargets(prompt) {
37300
+ const out = /* @__PURE__ */ new Set();
37301
+ const re = /(?:#|\bpr[- ]?|\bpull[- ]request\s+#?)(\d{1,10})\b/giu;
37302
+ let m;
37303
+ while ((m = re.exec(prompt)) !== null) {
37304
+ const n = Number.parseInt(m[1], 10);
37305
+ if (Number.isSafeInteger(n) && n > 0 && n <= PR_NUMBER_MAX) {
37306
+ out.add(n);
37307
+ }
37308
+ }
37309
+ return [...out].sort((a, b) => a - b);
37310
+ }
37311
+ function isRevocation(prompt) {
37312
+ return REVOKE_RE.test(prompt);
37313
+ }
37314
+ function _grantId(session_id, at, targets) {
37315
+ return `${sessionSlug(session_id) || "nosession"}:${at}:${targets.join(",")}`;
37316
+ }
37317
+ function foldGrants(prior, prompt, authorized, session_id, now) {
37318
+ if (isRevocation(prompt)) {
37319
+ return [];
37320
+ }
37321
+ const carried = prior.filter((g) => g.consumed.length < g.targets.length);
37322
+ if (!authorized.includes("pr-merge")) {
37323
+ return carried;
37324
+ }
37325
+ const targets = extractMergeTargets(prompt);
37326
+ if (targets.length === 0) {
37327
+ return carried;
37328
+ }
37329
+ const at = now.toISOString();
37330
+ const already = new Set(carried.flatMap((g) => g.targets));
37331
+ const fresh = targets.filter((t) => !already.has(t));
37332
+ if (fresh.length === 0) {
37333
+ return carried;
37334
+ }
37335
+ return [
37336
+ ...carried,
37337
+ {
37338
+ id: _grantId(session_id, at, fresh),
37339
+ op: "pr-merge",
37340
+ targets: fresh,
37341
+ consumed: [],
37342
+ granted_at: at,
37343
+ evidence: prompt.trim().slice(0, 120)
37344
+ }
37345
+ ];
37346
+ }
37347
+ function isSentenceBoundary(text, at) {
37348
+ const mark = text[at];
37349
+ if (mark !== ".") {
37350
+ return true;
37351
+ }
37352
+ if (/\d/.test(text[at - 1] ?? "") && /\d/.test(text[at + 1] ?? "")) {
37353
+ return false;
37354
+ }
37355
+ return !ABBREVIATION_BEFORE_DOT.test(text.slice(0, at + 1));
37356
+ }
37357
+ function sentenceBounds(text, index) {
37358
+ let start = 0;
37359
+ for (let i = index - 1; i >= 0; i -= 1) {
37360
+ const c = text[i];
37361
+ if ((c === "." || c === "!" || c === "?" || c === "\n") && isSentenceBoundary(text, i)) {
37362
+ start = i + 1;
37363
+ break;
37364
+ }
37365
+ }
37366
+ let end = text.length;
37367
+ for (let i = index; i < text.length; i += 1) {
37368
+ const c = text[i];
37369
+ if ((c === "." || c === "!" || c === "?" || c === "\n") && isSentenceBoundary(text, i)) {
37370
+ end = i;
37371
+ break;
37372
+ }
37373
+ }
37374
+ return { start, end };
37375
+ }
37376
+ function _sentences(text) {
37377
+ const out = [];
37378
+ let from = 0;
37379
+ for (let i = 0; i < text.length; i += 1) {
37380
+ const c = text[i];
37381
+ if ((c === "." || c === "!" || c === "?" || c === "\n") && isSentenceBoundary(text, i)) {
37382
+ out.push(text.slice(from, i + 1));
37383
+ from = i + 1;
37384
+ }
37385
+ }
37386
+ if (from < text.length) {
37387
+ out.push(text.slice(from));
37388
+ }
37389
+ return out.filter((t) => t.trim() !== "");
37390
+ }
37391
+ function clauseStart(text, from, to) {
37392
+ let start = from;
37393
+ const seg = text.slice(from, to);
37394
+ for (let i = seg.length - 1; i >= 0; i -= 1) {
37395
+ const c = seg[i];
37396
+ if (c === "," || c === ";" || c === ":") {
37397
+ start = from + i + 1;
37398
+ break;
37399
+ }
37400
+ }
37401
+ CONTRAST_CUE.lastIndex = 0;
37402
+ let m;
37403
+ while ((m = CONTRAST_CUE.exec(seg)) !== null) {
37404
+ const after = from + m.index + m[0].length;
37405
+ if (after > start) {
37406
+ start = after;
37407
+ }
37408
+ }
37409
+ return start;
37410
+ }
37411
+ function clauseEnd(text, from, to) {
37412
+ const seg = text.slice(from, to);
37413
+ let end = to;
37414
+ const punct = seg.search(/[,;:]/);
37415
+ if (punct !== -1) {
37416
+ end = from + punct;
37417
+ }
37418
+ CONTRAST_CUE.lastIndex = 0;
37419
+ const m = CONTRAST_CUE.exec(seg);
37420
+ if (m !== null && from + m.index < end) {
37421
+ end = from + m.index;
37422
+ }
37423
+ return end;
37424
+ }
37425
+ function negatedBefore(text, index, matched = "") {
37426
+ const { start, end } = sentenceBounds(text, index);
37427
+ const before = text.slice(clauseStart(text, start, index), index);
37428
+ const afterFrom = index + matched.length;
37429
+ const after = afterFrom < end ? text.slice(afterFrom, clauseEnd(text, afterFrom, end)) : "";
37430
+ return NEGATION_WORD.test(before) || NEGATION_WORD.test(matched) || NEGATION_WORD.test(after);
37431
+ }
37292
37432
  function classifyAuthorization(prompt) {
37293
37433
  const { prose, fences } = splitFences(prompt);
37294
37434
  const authorized = /* @__PURE__ */ new Set();
37295
37435
  const evidence = {};
37296
37436
  const instruction = prose.split("\n").filter((l) => !OUTPUT_LINE.test(l)).join("\n");
37297
- for (const { op, re } of isInterrogative(instruction) ? [] : PHRASES) {
37298
- const m = re.exec(instruction);
37299
- if (m) {
37300
- authorized.add(op);
37301
- evidence[op] = `prose: "${m[0]}"`;
37437
+ for (const { op, re } of PHRASES) {
37438
+ for (const sentence of _sentences(instruction)) {
37439
+ if (isInterrogative(sentence)) {
37440
+ continue;
37441
+ }
37442
+ const m = re.exec(sentence);
37443
+ if (m && !negatedBefore(sentence, m.index, m[0])) {
37444
+ authorized.add(op);
37445
+ evidence[op] = `prose: "${m[0]}"`;
37446
+ break;
37447
+ }
37302
37448
  }
37303
37449
  }
37304
37450
  for (const fence of fences) {
@@ -37318,6 +37464,72 @@ function classifyAuthorization(prompt) {
37318
37464
  }
37319
37465
  return { authorized: [...authorized], evidence };
37320
37466
  }
37467
+ function readGrants(consumer_root, session_id) {
37468
+ try {
37469
+ const raw = fs65.readFileSync(path66.join(consumer_root, ledgerFileFor(session_id)), "utf8");
37470
+ const decoded = JSON.parse(raw);
37471
+ if (!_isObject4(decoded) || !Array.isArray(decoded["grants"])) {
37472
+ return [];
37473
+ }
37474
+ const owner = typeof decoded["session_id"] === "string" ? decoded["session_id"] : "";
37475
+ if (session_id && owner && owner !== session_id) {
37476
+ return [];
37477
+ }
37478
+ return decoded["grants"].flatMap((g) => {
37479
+ if (!_isObject4(g)) return [];
37480
+ const op = g["op"];
37481
+ if (typeof op !== "string" || !ALL_OPS.includes(op)) return [];
37482
+ const targets = Array.isArray(g["targets"]) ? g["targets"] : [];
37483
+ const consumed = Array.isArray(g["consumed"]) ? g["consumed"] : [];
37484
+ return [
37485
+ {
37486
+ id: String(g["id"] ?? ""),
37487
+ op,
37488
+ targets: targets.filter((t) => typeof t === "number"),
37489
+ consumed: consumed.filter((t) => typeof t === "number"),
37490
+ granted_at: String(g["granted_at"] ?? ""),
37491
+ evidence: String(g["evidence"] ?? "")
37492
+ }
37493
+ ];
37494
+ });
37495
+ } catch {
37496
+ return [];
37497
+ }
37498
+ }
37499
+ function consumeGrantTarget(consumer_root, session_id, target) {
37500
+ const file = path66.join(consumer_root, ledgerFileFor(session_id));
37501
+ let decoded;
37502
+ try {
37503
+ decoded = JSON.parse(fs65.readFileSync(file, "utf8"));
37504
+ } catch {
37505
+ return;
37506
+ }
37507
+ if (!_isObject4(decoded) || !Array.isArray(decoded["grants"])) {
37508
+ return;
37509
+ }
37510
+ let touched = false;
37511
+ for (const g of decoded["grants"]) {
37512
+ if (!_isObject4(g) || !Array.isArray(g["targets"]) || !Array.isArray(g["consumed"])) {
37513
+ continue;
37514
+ }
37515
+ const targets = g["targets"];
37516
+ const consumed = g["consumed"];
37517
+ if (targets.includes(target) && !consumed.includes(target)) {
37518
+ consumed.push(target);
37519
+ touched = true;
37520
+ }
37521
+ }
37522
+ if (touched) {
37523
+ try {
37524
+ atomic_write_json(file, decoded);
37525
+ } catch (err) {
37526
+ process.stderr.write(
37527
+ `git-authorization: grant-consumption write failed (${_errText(err)}) \u2014 the target stays UNSPENT, so one further merge of a PR the user named will still be authorized. Authorization outcome unchanged.
37528
+ `
37529
+ );
37530
+ }
37531
+ }
37532
+ }
37321
37533
  function takePending(consumer_root, session_id, now) {
37322
37534
  const file = path66.join(consumer_root, pendingFileFor(session_id));
37323
37535
  let raw;
@@ -37386,16 +37598,32 @@ function run2(stdin_text, options) {
37386
37598
  authorized.push(pending.op);
37387
37599
  evidence[pending.op] = `confirmation of the refused \`${pending.op}\` (refused ${pending.refused_at}): "${prompt.trim().slice(0, 40)}"`;
37388
37600
  }
37601
+ const now = /* @__PURE__ */ new Date();
37389
37602
  const ledger = {
37390
37603
  session_id: typeof envelope["session_id"] === "string" ? envelope["session_id"] : "",
37391
- detected_at: (/* @__PURE__ */ new Date()).toISOString(),
37604
+ detected_at: now.toISOString(),
37392
37605
  authorized,
37393
37606
  evidence,
37394
- prompt_chars: prompt.length
37607
+ prompt_chars: prompt.length,
37608
+ // Grants survive this write; `authorized` does not. That asymmetry IS the
37609
+ // decision. A bare operation name stays one-shot exactly as `commit-policy`
37610
+ // requires, and only an authorization that also froze its objects earns a
37611
+ // life longer than the turn. See ADR-252.
37612
+ grants: foldGrants(
37613
+ readGrants(options.consumer_root, session),
37614
+ prompt,
37615
+ authorized,
37616
+ session,
37617
+ now
37618
+ )
37395
37619
  };
37396
37620
  try {
37397
37621
  atomic_write_json(path66.join(options.consumer_root, ledgerFileFor(session)), ledger);
37398
- } catch {
37622
+ } catch (err) {
37623
+ process.stderr.write(
37624
+ `git-authorization: session-ledger write failed (${_errText(err)}) \u2014 the gate degrades to "no ledger" for this session, which denies the irreversible subset and warns on the rest. Authorization outcome unchanged.
37625
+ `
37626
+ );
37399
37627
  }
37400
37628
  return EXIT_ALLOW2;
37401
37629
  }
@@ -37432,7 +37660,7 @@ function _isCliEntry19() {
37432
37660
  return false;
37433
37661
  }
37434
37662
  }
37435
- var EXIT_ALLOW2, STATE_FILE2, STATE_DIR2, PENDING_DIR, PENDING_FILE, PENDING_MAX_AGE_MS, ALL_OPS, PHRASES, OUTPUT_LINE, PASTED_COMMANDS;
37663
+ var EXIT_ALLOW2, STATE_FILE2, STATE_DIR2, PENDING_DIR, PENDING_FILE, PENDING_MAX_AGE_MS, ALL_OPS, PHRASES, OUTPUT_LINE, PASTED_COMMANDS, PR_NUMBER_MAX, REVOKE_RE, NEGATION_WORD, CONTRAST_CUE, ABBREVIATION_BEFORE_DOT;
37436
37664
  var init_git_authorization_hook = __esm({
37437
37665
  "src/scripts/git_authorization_hook.ts"() {
37438
37666
  "use strict";
@@ -37455,12 +37683,39 @@ var init_git_authorization_hook = __esm({
37455
37683
  "pr-merge-auto",
37456
37684
  "tag",
37457
37685
  "release",
37458
- "publish"
37686
+ "publish",
37687
+ "unpublish",
37688
+ "deprecate",
37689
+ "release-asset",
37690
+ "protection",
37691
+ "workflow-toggle",
37692
+ "repo-lifecycle",
37693
+ "review-approve",
37694
+ "force-push",
37695
+ "worktree-remove",
37696
+ "clean-ignored",
37697
+ "tag-force",
37698
+ "rebase",
37699
+ "reset-hard",
37700
+ "clean",
37701
+ "stash-drop",
37702
+ "branch-delete",
37703
+ "close"
37459
37704
  ];
37460
37705
  PHRASES = [
37461
37706
  { op: "commit", re: /\b(commit(e|et|te|ten)?|committe|einchecken)\b/i },
37462
37707
  { op: "push", re: /\b(push(e|en|t)?|hochladen|hochschieben|raufschieben)\b/i },
37463
- { op: "branch", re: /\b(branch|feature-branch|zweig)\b/i },
37708
+ // A CREATION VERB IS REQUIRED, as for `pr-create` above and for the same
37709
+ // reason. The bare noun could not tell an authorization from a mention, and
37710
+ // the mention is the dangerous half: "den branch nicht löschen" names the
37711
+ // object precisely in order to forbid acting on it, yet authorized branch
37712
+ // creation — the negation trails a bare-noun match, so neither the
37713
+ // look-behind nor the span check in `negatedBefore` can reach it. Widened
37714
+ // 2026-09-03 alongside the destructive-coverage work that measured it.
37715
+ {
37716
+ op: "branch",
37717
+ re: /\b(erstell(e|en)?|mach(e|en)?|leg(e|en)?\s+an|anleg(e|en)?|(er|be)?(ö|oe)ffne|open|create|new|neuer?|starte?|checkout|wechsl?e|switch)\b[^.\n]{0,30}\b(branch|feature-branch|zweig)\b|\b(branch|feature-branch|zweig)\b[^.\n]{0,25}\b(erstellen|anlegen|aufmachen|(ö|oe)ffnen|starten)\b/i
37718
+ },
37464
37719
  // A creation verb is required — "schau dir den PR an" is not "open a PR".
37465
37720
  {
37466
37721
  op: "pr-create",
@@ -37474,20 +37729,22 @@ var init_git_authorization_hook = __esm({
37474
37729
  // "never auto-merge" each returned ["pr-merge"] — a prompt saying DO NOT
37475
37730
  // MERGE authorized a merge. The noun-sense lookahead was never a negator.
37476
37731
  //
37477
- // The lookBEHIND is line-scoped by `[^.!?\n]` on purpose, the same scoping
37478
- // `turn_end_gate_hook.ts` uses for its own negation exclusion (`:330`) and
37479
- // its negated-claim window (`:490-493`). A sentence boundary ends a
37480
- // negation's reach: "Do not push. Merge PR #12." is two instructions, and a
37481
- // guard that let the first suppress the second would silently stop
37482
- // authorizing merges the user DID order — the failure mode that is worse
37483
- // than the defect, because nothing happens and nothing says why.
37484
- //
37485
- // Reusing that vocabulary rather than inventing a third is deliberate: two
37732
+ // The inline negative lookBEHIND that used to live in this pattern was
37733
+ // REMOVED on 2026-09-03, and its own comment is why. It warned that "two
37486
37734
  // negation vocabularies in one tree drift, and the drift is invisible until
37487
- // a prompt lands in the gap between them.
37735
+ // a prompt lands in the gap between them" — and by then they had drifted:
37736
+ // `negatedBefore` became clause-scanned in both directions while this
37737
+ // lookbehind stayed a blind 30-character window with no forward half. The
37738
+ // prompt that landed in the gap is `"nicht pushen, aber mergen"`, which the
37739
+ // lookbehind refused (13 characters, inside its window, across a contrast
37740
+ // cue it could not see) while the clause scan correctly authorizes it.
37741
+ //
37742
+ // Every phrase match already passes through `negatedBefore` at the call
37743
+ // site, so the guard is not weakened by the removal — it is applied once,
37744
+ // by the one vocabulary this file's comment asked for.
37488
37745
  {
37489
37746
  op: "pr-merge",
37490
- re: /(?<!\b(nicht|nichts|kein(e|en|em|er|es)?|niemals|nie|no|not|dont|don't|never|without|ohne)\b[^.!?\n]{0,30})\b(merge|merg(e|en|st|t)|zusammenf(ü|ue)hren|reinmergen)\b(?!\s*[- ]?(conflict|konflikt|commit|base|queue|state|status))/i
37747
+ re: /\b(merge|merg(e|en|st|t)|zusammenf(ü|ue)hren|reinmergen)\b(?!\s*[- ]?(conflict|konflikt|commit|base|queue|state|status))/i
37491
37748
  },
37492
37749
  // A tag is an ACTION here — bare "Tag" is the German word for day, and
37493
37750
  // "Version" is an ordinary noun. Both authorized a BLOCK op before this.
@@ -37497,7 +37754,111 @@ var init_git_authorization_hook = __esm({
37497
37754
  op: "release",
37498
37755
  re: /\b(releasen?|ver(ö|oe)ffentlich(e|en))\b(?!\s*[- ]?(notes?|branch|candidate|pr\b|datum|date|zweig))/i
37499
37756
  },
37500
- { op: "publish", re: /\b(publish(e|en)?|publiziere[n]?)\b/i }
37757
+ { op: "publish", re: /\b(publish(e|en)?|publiziere[n]?)\b/i },
37758
+ // ── Authorization vocabulary for the ops added 2026-09-03.
37759
+ //
37760
+ // NON-NEGOTIABLE, and the reason it ships in the same change as the new BLOCK
37761
+ // ops: a blocked operation with no phrase that authorizes it is a dead end.
37762
+ // The guard refuses, the user says "doch, mach das", the classifier records
37763
+ // nothing, and the refusal repeats forever. Every op the guard can block must
37764
+ // have a sentence that unblocks it, or the user's override is a claim rather
37765
+ // than a mechanism.
37766
+ //
37767
+ // Tight on purpose. Under-matching costs one confirmation; over-matching
37768
+ // authorizes something the user did not ask for, so each pattern demands the
37769
+ // object noun rather than a bare verb wherever the verb is ordinary German.
37770
+ //
37771
+ // `unpublish` before nothing in particular — `\bpublish\b` does not match
37772
+ // inside "unpublish" (no word boundary between `n` and `p`), so the two never
37773
+ // collide.
37774
+ // TWO SHAPES EVERY GERMAN PATTERN BELOW MUST CARRY, both learned from tests
37775
+ // that went red rather than from inspection:
37776
+ //
37777
+ // 1. THE IMPERATIVE DROPS ITS `-e`. "entfern die worktree", "archivier das
37778
+ // repo", "deaktivier den workflow" are what people type; `entfern(e|en)`
37779
+ // matches none of them. Every verb ending is therefore `(e|en)?`.
37780
+ // 2. WORD ORDER GOES BOTH WAYS. "archivier das repo" and "das repo
37781
+ // archivieren" are one instruction, and a pattern anchored on the noun
37782
+ // first sees only the second.
37783
+ //
37784
+ // The object noun stays REQUIRED in every pattern that has one. That is what
37785
+ // keeps the loosened verb endings from over-authorizing: "lösch" alone
37786
+ // authorizes nothing, "lösch den branch" authorizes a branch deletion.
37787
+ { op: "unpublish", re: /\bunpublish(e|en)?\b/i },
37788
+ {
37789
+ op: "deprecate",
37790
+ re: /\bdeprecate(d|n)?\b|\bals\s+veraltet\s+markier(e|en)?\b/i
37791
+ },
37792
+ {
37793
+ op: "release-asset",
37794
+ re: /\brelease[-\s]?asset\b|\b(clobber|(ü|ue)berschreib(e|en)?|ersetz(e|en)?|replace|austausch(e|en)?)\b[^.\n]{0,25}\b(asset|artefakt|artifact|tarball|binary)\b|\b(asset|artefakt|artifact)\b[^.\n]{0,25}\b((ü|ue)berschreiben|ersetzen|austauschen|replace)\b/i
37795
+ },
37796
+ // A VERB IS REQUIRED, like `tag` and `release` above and for the same reason.
37797
+ // A bare noun cannot tell an authorization from a mention, and here the
37798
+ // mention is the dangerous half: "die branch-protection nicht anfassen" names
37799
+ // the object precisely in order to forbid touching it. Measured — this was the
37800
+ // one row of the negation corpus that neither the look-behind nor the
37801
+ // span check could reach, because the negation trails a bare-noun match.
37802
+ {
37803
+ op: "protection",
37804
+ re: /\b(entfern(e|en)?|l(ö|oe)sch(e|en)?|deaktivier(e|en)?|nimm|weg|remove|delete|disable|drop|lift)\b[^.\n]{0,30}\b(branch[-\s]?protection|protection[-\s]?rule|schutzregel|ruleset)\b|\b(branch[-\s]?protection|protection[-\s]?rule|schutzregel|ruleset)\b[^.\n]{0,30}\b(entfernen|l(ö|oe)schen|deaktivieren|abschalten|remove|delete|disable|drop|lift|weg)\b/i
37805
+ },
37806
+ {
37807
+ op: "workflow-toggle",
37808
+ re: /\bworkflow\b[^.\n]{0,25}\b(deaktivier(e|en)?|aktivier(e|en)?|disable|enable|abschalt(e|en)?|ausschalt(e|en)?)\b|\b(deaktivier(e|en)?|aktivier(e|en)?|disable|enable|abschalt(e|en)?|ausschalt(e|en)?)\b[^.\n]{0,25}\bworkflow\b/i
37809
+ },
37810
+ {
37811
+ op: "repo-lifecycle",
37812
+ re: /\b(repo|repository)\b[^.\n]{0,30}\b(archivier(e|en)?|archive|l(ö|oe)sch(e|en)?|delete)\b|\b(archivier(e|en)?|archive|l(ö|oe)sch(e|en)?|delete)\b[^.\n]{0,25}\b(repo|repository)\b|\b(repo|repository)\b[^.\n]{0,30}\bauf\s+(privat|public|öffentlich|private)\b/i
37813
+ },
37814
+ {
37815
+ op: "review-approve",
37816
+ re: /\b(approve|genehmig(e|en)?|freigeb(e|en)?|abnehm(e|en)?)\b[^.\n]{0,30}\b(review|pr|pull[-\s]request)\b|\b(review|pr|pull[-\s]request)\b[^.\n]{0,20}\b(approve|approven|genehmigen|freigeben)\b/i
37817
+ },
37818
+ {
37819
+ op: "force-push",
37820
+ re: /\bforce[-\s]?push(e|en|st)?\b|--force(-with-lease)?\b|\bmit\s+gewalt\s+push(e|en)?\b/i
37821
+ },
37822
+ {
37823
+ op: "worktree-remove",
37824
+ re: /\bworktree\b[^.\n]{0,25}\b(entfern(e|en)?|l(ö|oe)sch(e|en)?|remove|delete|weg(werfen|r(ä|ae)umen))\b|\b(entfern(e|en)?|l(ö|oe)sch(e|en)?|remove|delete)\b[^.\n]{0,25}\bworktree\b/i
37825
+ },
37826
+ // The `-x` flag is the whole distinction: without it `git clean` spares
37827
+ // ignored files, with it `.env` and local certificates go too.
37828
+ {
37829
+ op: "clean-ignored",
37830
+ re: /\bgit\s+clean\b[^.\n]{0,20}-[A-Za-z]*x|\b(ignorierte|ignored)\b[^.\n]{0,25}\b(dateien|files)\b[^.\n]{0,25}\b(l(ö|oe)sch(e|en)?|entfern(e|en)?|remove|delete)\b/i
37831
+ },
37832
+ {
37833
+ op: "tag-force",
37834
+ re: /\btag\b[^.\n]{0,25}\b(verschieb(e|en)?|(ü|ue)berschreib(e|en)?|umh(ä|ae)ng(e|en)?|force|move)\b|\b(verschieb(e|en)?|(ü|ue)berschreib(e|en)?)\b[^.\n]{0,20}\btag\b/i
37835
+ },
37836
+ { op: "rebase", re: /\brebase(n|st|d)?\b/i },
37837
+ {
37838
+ op: "reset-hard",
37839
+ re: /\breset\s+--hard\b|\bhard\s+reset\b|\bhart\s+zur(ü|ue)cksetz(e|en)?\b/i
37840
+ },
37841
+ { op: "clean", re: /\bgit\s+clean\b/i },
37842
+ {
37843
+ op: "stash-drop",
37844
+ re: /\bstash\b[^.\n]{0,25}\b(drop|clear|verwerf(e|en)?|l(ö|oe)sch(e|en)?)\b|\b(verwerf(e|en)?|l(ö|oe)sch(e|en)?|drop)\b[^.\n]{0,20}\bstash\b/i
37845
+ },
37846
+ {
37847
+ op: "branch-delete",
37848
+ re: /\b(l(ö|oe)sch(e|en)?|entfern(e|en)?|delete|remove)\b[^.\n]{0,25}\b(branch|branches|zweig)\b|\b(branch|branches|zweig)\b[^.\n]{0,25}\b(l(ö|oe)schen|entfernen|delete|remove)\b/i
37849
+ },
37850
+ {
37851
+ op: "close",
37852
+ re: /\b(schlie(ß|ss)(e|en)?|close)\b[^.\n]{0,30}\b(pr|pull[-\s]request|issue|ticket)\b|\b(pr|pull[-\s]request|issue|ticket)\b[^.\n]{0,20}\b(schlie(ß|ss)en|close)\b/i
37853
+ },
37854
+ // Enabling auto-merge is its own op on the guard side, so it needs its own
37855
+ // sentence. Without one it is the dead end this block exists to prevent: the
37856
+ // plain merge phrase authorizes `pr-merge` and never `pr-merge-auto`, so the
37857
+ // refusal would have repeated forever.
37858
+ {
37859
+ op: "pr-merge-auto",
37860
+ re: /\bauto[-\s]?merge\b|\bmerge\b[^.\n]{0,20}\bautomatisch\b|\bautomatisch\b[^.\n]{0,20}\bmerg(e|en)\b/i
37861
+ }
37501
37862
  ];
37502
37863
  OUTPUT_LINE = /^\s*(To\s+\S+|remote:|error:|fatal:|hint:|warning:|!\s|\?\?\s|\s*\^|[-+]{3}\s|@@\s|\$\s|>\s|\d+\s+(pass|fail)|npm ERR!|Error:|Traceback|at\s+\S+:\d+)/i;
37503
37864
  PASTED_COMMANDS = [
@@ -37520,12 +37881,131 @@ var init_git_authorization_hook = __esm({
37520
37881
  { op: "commit", re: /\bgit\s+commit\b/i },
37521
37882
  { op: "branch", re: /\bgit\s+(checkout\s+-b|switch\s+-c|branch)\b/i }
37522
37883
  ];
37884
+ PR_NUMBER_MAX = 2147483647;
37885
+ REVOKE_RE = /\b(stop|stopp|halt|abbrechen|abbruch|cancel|revoke|widerruf(en)?|zur(ü|ue)ck(nehmen|ziehen)|hold\s+(everything|on|the\s+merge)|warte|wait)\b|(?:\b(nicht|kein(e|en)?|no|not|dont|don't|never|niemals|nie)\b[^.!?\n]{0,30}\b(merg(e|en|ing)|zusammenf(ü|ue)hren)\b)/iu;
37886
+ NEGATION_WORD = /\b(nicht|nichts|kein(e|en|em|er|es)?|niemals|nie|no|not|dont|don't|never|without|ohne)\b/i;
37887
+ CONTRAST_CUE = /\b(aber|sondern|jedoch|but|however|instead)\b/gi;
37888
+ ABBREVIATION_BEFORE_DOT = /(?:^|[\s("'\u2018\u201c])(?:[a-z]|z\.b|bzw|ca|usw|d\.h|u\.a|evtl|ggf|inkl|max|min|nr|abb|e\.g|i\.e|etc|vs|approx|fig|no|st|dr|prof)\.$/i;
37523
37889
  if (_isCliEntry19()) {
37524
37890
  process.exit(main23());
37525
37891
  }
37526
37892
  }
37527
37893
  });
37528
37894
 
37895
+ // src/scripts/hooks/merge_impact.ts
37896
+ import { execFileSync as execFileSync2 } from "node:child_process";
37897
+ function fileBlocks(patch) {
37898
+ return patch.split(/^(?=diff --git )/m).filter((b) => b.startsWith("diff --git "));
37899
+ }
37900
+ function renamedSchemaRemoval(patch) {
37901
+ for (const block of fileBlocks(patch)) {
37902
+ if (!RENAME_METADATA.test(block)) {
37903
+ continue;
37904
+ }
37905
+ for (const line of block.split("\n")) {
37906
+ if (line.startsWith("-") && !line.startsWith("---") && SCHEMA_REMOVAL_BODY.test(line)) {
37907
+ return true;
37908
+ }
37909
+ }
37910
+ }
37911
+ return false;
37912
+ }
37913
+ function classifyDiff(patch) {
37914
+ if (patch.trim() === "") {
37915
+ return { verdict: "undecidable", markers: [], reason: "empty diff" };
37916
+ }
37917
+ const markers = [];
37918
+ for (const { label, re } of DESTRUCTIVE_MARKERS) {
37919
+ if (re.test(patch)) {
37920
+ markers.push(label);
37921
+ }
37922
+ }
37923
+ if (renamedSchemaRemoval(patch)) {
37924
+ markers.push("a renamed file drops a column or table");
37925
+ }
37926
+ const bump = MAJOR_BUMP.exec(patch);
37927
+ if (bump && bump[1] !== bump[2]) {
37928
+ markers.push(`major version bump ${bump[1]}.x to ${bump[2]}.x`);
37929
+ }
37930
+ const filesChanged = (patch.match(/^diff --git /gm) ?? []).length;
37931
+ if (markers.length > 0) {
37932
+ return { verdict: "destructive", markers, filesChanged };
37933
+ }
37934
+ return { verdict: "additive", markers: [], filesChanged };
37935
+ }
37936
+ function fetchPatch(pr, opts = {}) {
37937
+ const run6 = opts.exec ?? execFileSync2;
37938
+ const cwd = opts.cwd ?? process.cwd();
37939
+ const capture = (file, args) => run6(file, args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 15e3 });
37940
+ for (const ref of [`refs/pull/${pr}/head`, `pull/${pr}/head`]) {
37941
+ try {
37942
+ const base = capture("git", ["merge-base", "HEAD", ref]).trim();
37943
+ if (base) {
37944
+ const patch = capture("git", ["diff", `${base}...${ref}`]);
37945
+ if (patch.trim() !== "") {
37946
+ return { patch, source: "local" };
37947
+ }
37948
+ }
37949
+ } catch {
37950
+ }
37951
+ }
37952
+ if (opts.allowNetwork === false) {
37953
+ return { patch: "", source: "none", reason: "no local ref and network reads disabled" };
37954
+ }
37955
+ try {
37956
+ const patch = capture("gh", ["pr", "diff", String(pr), "--patch"]);
37957
+ if (patch.trim() !== "") {
37958
+ return { patch, source: "gh" };
37959
+ }
37960
+ return { patch: "", source: "none", reason: "gh returned an empty diff" };
37961
+ } catch {
37962
+ return { patch: "", source: "none", reason: "no local ref, and gh pr diff failed" };
37963
+ }
37964
+ }
37965
+ function analyseMergeImpact(pr, opts = {}) {
37966
+ const got = fetchPatch(pr, opts);
37967
+ if (got.source === "none") {
37968
+ return { verdict: "undecidable", markers: [], ...got.reason ? { reason: got.reason } : {} };
37969
+ }
37970
+ return classifyDiff(got.patch);
37971
+ }
37972
+ function describeImpact(pr, impact) {
37973
+ const say = `Say "merge #${pr}" to authorize it.`;
37974
+ switch (impact.verdict) {
37975
+ case "additive":
37976
+ return `Impact scan: PR #${pr} touches ${impact.filesChanged ?? 0} file(s) and trips no destructive marker \u2014 no schema drop, no removed export or route, no major bump, no BREAKING CHANGE. Additive as far as the diff can show, which is not the same as safe to merge: the base has moved since review and merging may trigger a deploy. ${say}`;
37977
+ case "destructive":
37978
+ return `Impact scan: PR #${pr} is NOT additive \u2014 ${impact.markers.join("; ")}. Read the diff before authorizing. ${say}`;
37979
+ default:
37980
+ return `Impact scan: could not read the diff for PR #${pr} (${impact.reason ?? "unknown"}), so nothing is known about what it changes. ${say}`;
37981
+ }
37982
+ }
37983
+ var DESTRUCTIVE_MARKERS, SCHEMA_REMOVAL_BODY, RENAME_METADATA, MAJOR_BUMP;
37984
+ var init_merge_impact = __esm({
37985
+ "src/scripts/hooks/merge_impact.ts"() {
37986
+ "use strict";
37987
+ DESTRUCTIVE_MARKERS = [
37988
+ // Schema removal. Matched on ADDED lines only — a migration that *removes* a
37989
+ // dropColumn call is the opposite of destructive, and matching the raw patch
37990
+ // would score it the same way.
37991
+ { label: "migration drops a column or table", re: /^\+.*\b(dropColumn|dropTable|dropIfExists|removeColumn|drop_column|drop_table)\b/im },
37992
+ { label: "raw DROP or TRUNCATE in a migration", re: /^\+.*\b(DROP\s+(TABLE|COLUMN|INDEX|CONSTRAINT)|TRUNCATE\s+TABLE)\b/im },
37993
+ // The author said so themselves. Conventional Commits marks a breaking change
37994
+ // with a `!` before the colon or a body trailer.
37995
+ { label: "author marked a BREAKING CHANGE", re: /^\+.*BREAKING[ -]CHANGE/m },
37996
+ // A removed export is the shape most likely to break an importer.
37997
+ { label: "an exported symbol was removed", re: /^-\s*export\s+(default\s+|const\s+|function\s+|class\s+|interface\s+|type\s+|async\s+)/m },
37998
+ { label: "a public PHP or Python symbol was removed", re: /^-\s*(public\s+function|def\s+[a-z_]+\s*\()/m },
37999
+ // Route removal breaks callers that never imported anything.
38000
+ { label: "a route or endpoint was removed", re: /^-.*\b(Route::(get|post|put|patch|delete)|@(Get|Post|Put|Patch|Delete)Mapping|app\.(get|post|put|patch|delete)\s*\()/m },
38001
+ { label: "a whole file was deleted", re: /^deleted file mode /m }
38002
+ ];
38003
+ SCHEMA_REMOVAL_BODY = /\b(dropColumn|dropTable|dropIfExists|removeColumn|drop_column|drop_table|DROP\s+(TABLE|COLUMN|INDEX|CONSTRAINT)|TRUNCATE\s+TABLE)\b/i;
38004
+ RENAME_METADATA = /^rename (?:from|to) /m;
38005
+ MAJOR_BUMP = /^-\s*"version"\s*:\s*"(\d+)\.[^"]*"[\s\S]{0,200}?^\+\s*"version"\s*:\s*"(\d+)\./m;
38006
+ }
38007
+ });
38008
+
37529
38009
  // src/scripts/hooks/block_unauthorized_git.ts
37530
38010
  import fs66 from "node:fs";
37531
38011
  import path67 from "node:path";
@@ -37716,7 +38196,28 @@ function commandOp(command) {
37716
38196
  }
37717
38197
  return found;
37718
38198
  }
38199
+ function mergeTargetOf(command) {
38200
+ const gh = /\bgh\s+pr\s+merge\s+(?:-{1,2}\S+\s+)*?(\d{1,10})\b/i.exec(command);
38201
+ if (gh) {
38202
+ return _boundedTarget(gh[1]);
38203
+ }
38204
+ const api = /\/pulls\/(\d{1,10})\/merge\b/i.exec(command);
38205
+ return api ? _boundedTarget(api[1]) : null;
38206
+ }
38207
+ function _boundedTarget(digits) {
38208
+ const n = Number.parseInt(digits, 10);
38209
+ return Number.isSafeInteger(n) && n > 0 && n <= PR_NUMBER_MAX ? n : null;
38210
+ }
38211
+ function grantCovers(grants, op, target) {
38212
+ if (op !== "pr-merge" || target === null) {
38213
+ return null;
38214
+ }
38215
+ return grants.find(
38216
+ (g) => g.op === "pr-merge" && g.targets.includes(target) && !g.consumed.includes(target)
38217
+ ) ?? null;
38218
+ }
37719
38219
  function _loadLedger(consumer_root, session_id, now) {
38220
+ const grants = readGrants(consumer_root, session_id);
37720
38221
  const candidates = [ledgerFileFor(session_id)];
37721
38222
  if (!candidates.includes(STATE_FILE2)) {
37722
38223
  candidates.push(STATE_FILE2);
@@ -37724,10 +38225,10 @@ function _loadLedger(consumer_root, session_id, now) {
37724
38225
  for (const rel of candidates) {
37725
38226
  const found = _readLedgerFile(path67.join(consumer_root, rel), session_id, now);
37726
38227
  if (found !== null) {
37727
- return found;
38228
+ return { ...found, grants };
37728
38229
  }
37729
38230
  }
37730
- return { authorized: /* @__PURE__ */ new Set(), present: false };
38231
+ return { authorized: /* @__PURE__ */ new Set(), present: false, grants };
37731
38232
  }
37732
38233
  function _readLedgerFile(file, session_id, now) {
37733
38234
  try {
@@ -37751,7 +38252,7 @@ function _readLedgerFile(file, session_id, now) {
37751
38252
  }
37752
38253
  return null;
37753
38254
  }
37754
- function decide(command, ledger) {
38255
+ function decide(command, ledger, impact) {
37755
38256
  if (command === null) {
37756
38257
  return { exit: EXIT_ALLOW3, stdout: "", stderr: "" };
37757
38258
  }
@@ -37759,9 +38260,14 @@ function decide(command, ledger) {
37759
38260
  if (op === null || ledger.authorized.has(op)) {
37760
38261
  return { exit: EXIT_ALLOW3, stdout: "", stderr: "" };
37761
38262
  }
38263
+ const target = mergeTargetOf(command);
38264
+ if (grantCovers(ledger.grants ?? [], op, target) && target !== null) {
38265
+ return { exit: EXIT_ALLOW3, stdout: "", stderr: "", consumedTarget: target };
38266
+ }
37762
38267
  const shown = command.length > 120 ? `${command.slice(0, 117)}\u2026` : command;
37763
38268
  if (BLOCK_OPS.has(op)) {
37764
- const reason = `Blocked: \`${op}\` with no authorization in this turn's prompt. This operation is irreversible and non-destructive-by-default already declares it never-autonomous. Command: ${shown}. Ask the user, in one numbered-options block, and run it only after they answer this turn.`;
38269
+ const scan2 = op === "pr-merge" && target !== null && impact ? ` ${impact}` : "";
38270
+ const reason = `Blocked: \`${op}\` with no authorization in this turn's prompt. This operation is irreversible and non-destructive-by-default already declares it never-autonomous. Command: ${shown}.${scan2} Ask the user, in one numbered-options block, and run it only after they answer this turn.`;
37765
38271
  return { exit: EXIT_BLOCK, stdout: "", stderr: `${reason}
37766
38272
  `, blockedOp: op };
37767
38273
  }
@@ -37789,10 +38295,24 @@ function run3(stdin_text, options) {
37789
38295
  }
37790
38296
  }
37791
38297
  const session_id = typeof envelope["session_id"] === "string" ? envelope["session_id"] : "";
37792
- const decision = decide(
37793
- extractCommand(envelope),
37794
- _loadLedger(options.consumer_root, session_id, Date.now())
37795
- );
38298
+ const command = extractCommand(envelope);
38299
+ const ledger = _loadLedger(options.consumer_root, session_id, Date.now());
38300
+ let impact;
38301
+ const stage1 = command === null ? null : commandOp(command);
38302
+ const stage2Target = command === null ? null : mergeTargetOf(command);
38303
+ if (stage1 === "pr-merge" && stage2Target !== null && !ledger.authorized.has("pr-merge") && !grantCovers(ledger.grants ?? [], "pr-merge", stage2Target)) {
38304
+ try {
38305
+ impact = describeImpact(
38306
+ stage2Target,
38307
+ analyseMergeImpact(stage2Target, { cwd: options.consumer_root })
38308
+ );
38309
+ } catch {
38310
+ }
38311
+ }
38312
+ const decision = decide(command, ledger, impact);
38313
+ if (decision.consumedTarget !== void 0) {
38314
+ consumeGrantTarget(options.consumer_root, session_id, decision.consumedTarget);
38315
+ }
37796
38316
  if (decision.blockedOp) {
37797
38317
  try {
37798
38318
  atomic_write_json(path67.join(options.consumer_root, pendingFileFor(session_id)), {
@@ -37849,6 +38369,7 @@ var init_block_unauthorized_git = __esm({
37849
38369
  "src/scripts/hooks/block_unauthorized_git.ts"() {
37850
38370
  "use strict";
37851
38371
  init_hook_stdin();
38372
+ init_merge_impact();
37852
38373
  init_state_io();
37853
38374
  init_git_authorization_hook();
37854
38375
  EXIT_ALLOW3 = 0;
@@ -37860,13 +38381,52 @@ var init_block_unauthorized_git = __esm({
37860
38381
  "pr-merge",
37861
38382
  // Enabling auto-merge commits the outcome to a condition the agent does not
37862
38383
  // control, so it is blocked on the same terms as the merge it schedules.
37863
- "pr-merge-auto"
38384
+ "pr-merge-auto",
38385
+ // ── Added 2026-09-03. Every one is already never-autonomous in the
38386
+ // `non-destructive-by-default` prose; the measurement that drove it is in the
38387
+ // `GitOp` union's own note — 17 of 25 borderline operations classified as
38388
+ // NOTHING, including three worse than anything the guard already caught.
38389
+ /** The undo that is more destructive than the act: lockfiles break. */
38390
+ "unpublish",
38391
+ /** Metadata only, and every install on earth starts warning. */
38392
+ "deprecate",
38393
+ /** Replaces bytes consumers already hold a checksum for. */
38394
+ "release-asset",
38395
+ /** Deletes the guard rather than the data — everything after is unguarded. */
38396
+ "protection",
38397
+ /** No data lost; the gate that protects everything downstream is off. */
38398
+ "workflow-toggle",
38399
+ /** Reach outside the repository, and mostly one-way. */
38400
+ "repo-lifecycle",
38401
+ /** No code moves, and an armed auto-merge is released. */
38402
+ "review-approve",
38403
+ /** Discards commits that landed after the last fetch, on a shared ref. */
38404
+ "force-push",
38405
+ /** Destroys uncommitted work belonging to a session this one cannot see. */
38406
+ "worktree-remove",
38407
+ /** `-x` is the flag that takes `.env` and local certificates. */
38408
+ "clean-ignored"
37864
38409
  ]);
37865
38410
  WARN_OPS = /* @__PURE__ */ new Set([
37866
38411
  "commit",
37867
38412
  "push",
37868
38413
  "pr-create",
37869
- "branch"
38414
+ "branch",
38415
+ // ── Added 2026-09-03, deliberately at warn rather than block.
38416
+ //
38417
+ // Each is destructive in some reading, and each is also routine enough that
38418
+ // blocking it would be the friction that gets the whole guard routed around —
38419
+ // the failure mode this estate has already recorded twice. `reset-hard` is the
38420
+ // sharpest case and stays here for a decidable reason: the Hard Floor's
38421
+ // qualifier is "past unpushed work", and the command text cannot say whether
38422
+ // the work was pushed.
38423
+ "tag-force",
38424
+ "rebase",
38425
+ "reset-hard",
38426
+ "clean",
38427
+ "stash-drop",
38428
+ "branch-delete",
38429
+ "close"
37870
38430
  ]);
37871
38431
  G = "(?:-[A-Za-z-]+(?:=\\S+)?(?:\\s+\\S+)?\\s+)*";
37872
38432
  P = "^(?:\\S*\\/)?";
@@ -37906,6 +38466,72 @@ var init_block_unauthorized_git = __esm({
37906
38466
  op: "pr-create",
37907
38467
  re: new RegExp(`${P}gh\\s+pr\\s+create\\b|${ghApiWrite("\\/pulls\\b").source}`, "i")
37908
38468
  },
38469
+ // ── Added 2026-09-03. Placement inside this table is as load-bearing as the
38470
+ // patterns: the loop takes the MOST SEVERE match per segment, but within one
38471
+ // op the first pattern wins, so anything more specific than a plain `git push`
38472
+ // has to sit above it.
38473
+ // `npm unpublish` is NOT reached by the publish pattern — `\bpublish\b` needs
38474
+ // a word boundary that `unpublish` does not provide between `n` and `p`. That
38475
+ // is why it classified as nothing for as long as it did.
38476
+ { op: "unpublish", re: new RegExp(`${P}(npm|pnpm|yarn)\\s+${G}unpublish\\b`, "i") },
38477
+ { op: "deprecate", re: new RegExp(`${P}(npm|pnpm|yarn)\\s+${G}deprecate\\b`, "i") },
38478
+ {
38479
+ op: "release-asset",
38480
+ re: new RegExp(`${P}gh\\s+release\\s+(upload|edit|delete)\\b`, "i")
38481
+ },
38482
+ {
38483
+ op: "protection",
38484
+ re: new RegExp(
38485
+ `${P}gh\\s+api\\b(?=[^\\n]*(?:protection|rulesets?)\\b)(?=[^\\n]*(?:-X|--method)\\s+(?:DELETE|PUT|PATCH|POST)\\b)|${P}gh\\s+ruleset\\s+delete\\b`,
38486
+ "i"
38487
+ )
38488
+ },
38489
+ {
38490
+ op: "workflow-toggle",
38491
+ re: new RegExp(`${P}gh\\s+workflow\\s+(disable|enable)\\b`, "i")
38492
+ },
38493
+ {
38494
+ op: "repo-lifecycle",
38495
+ re: new RegExp(
38496
+ `${P}gh\\s+repo\\s+(archive|delete|unarchive)\\b|${P}gh\\s+repo\\s+edit\\b(?=[^\\n]*--visibility\\b)`,
38497
+ "i"
38498
+ )
38499
+ },
38500
+ {
38501
+ op: "review-approve",
38502
+ re: new RegExp(`${P}gh\\s+pr\\s+review\\b(?=[^\\n]*--approve\\b)`, "i")
38503
+ },
38504
+ // Above `push`, and both spellings: `--force` and `--force-with-lease`. The
38505
+ // "safe force" is in the same class here because what it is safe against is a
38506
+ // stale local view, not a collaborator who pushed after your fetch.
38507
+ {
38508
+ op: "force-push",
38509
+ re: new RegExp(`${P}git\\s+${G}push\\b[^\\n;|&]*(--force\\b|--force-with-lease\\b|\\s-f\\b)`, "i")
38510
+ },
38511
+ {
38512
+ op: "worktree-remove",
38513
+ re: new RegExp(`${P}git\\s+worktree\\s+remove\\b[^\\n;|&]*(--force\\b|\\s-f\\b)`, "i")
38514
+ },
38515
+ // `-x` (or `-X`) anywhere in the flag cluster is what reaches ignored files.
38516
+ {
38517
+ op: "clean-ignored",
38518
+ re: new RegExp(`${P}git\\s+clean\\b[^\\n;|&]*-[A-Za-z]*x`, "i")
38519
+ },
38520
+ { op: "tag-force", re: new RegExp(`${P}git\\s+tag\\b[^\\n;|&]*(-f\\b|--force\\b)`, "i") },
38521
+ { op: "rebase", re: new RegExp(`${P}git\\s+${G}rebase\\b`, "i") },
38522
+ { op: "reset-hard", re: new RegExp(`${P}git\\s+${G}reset\\b[^\\n;|&]*--hard\\b`, "i") },
38523
+ { op: "clean", re: new RegExp(`${P}git\\s+clean\\b`, "i") },
38524
+ { op: "stash-drop", re: new RegExp(`${P}git\\s+stash\\s+(drop|clear)\\b`, "i") },
38525
+ // Remote deletion and the local `-D` are one op: both remove a ref, and the
38526
+ // remote form additionally closes any open PR pointing at it.
38527
+ {
38528
+ op: "branch-delete",
38529
+ re: new RegExp(
38530
+ `${P}git\\s+branch\\b[^\\n;|&]*(-D\\b|--delete\\b)|${P}git\\s+${G}push\\b[^\\n;|&]*(--delete\\b|\\s:\\S)`,
38531
+ "i"
38532
+ )
38533
+ },
38534
+ { op: "close", re: new RegExp(`${P}gh\\s+(pr|issue)\\s+close\\b`, "i") },
37909
38535
  { op: "push", re: new RegExp(`${P}git\\s+${G}push\\b`, "i") },
37910
38536
  { op: "commit", re: new RegExp(`${P}git\\s+${G}commit\\b`, "i") },
37911
38537
  { op: "branch", re: new RegExp(`${P}git\\s+${G}(checkout\\s+-b|switch\\s+-c)\\b`, "i") }