@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
@@ -25850,12 +25850,39 @@ var ALL_OPS = [
25850
25850
  "pr-merge-auto",
25851
25851
  "tag",
25852
25852
  "release",
25853
- "publish"
25853
+ "publish",
25854
+ "unpublish",
25855
+ "deprecate",
25856
+ "release-asset",
25857
+ "protection",
25858
+ "workflow-toggle",
25859
+ "repo-lifecycle",
25860
+ "review-approve",
25861
+ "force-push",
25862
+ "worktree-remove",
25863
+ "clean-ignored",
25864
+ "tag-force",
25865
+ "rebase",
25866
+ "reset-hard",
25867
+ "clean",
25868
+ "stash-drop",
25869
+ "branch-delete",
25870
+ "close"
25854
25871
  ];
25855
25872
  var PHRASES = [
25856
25873
  { op: "commit", re: /\b(commit(e|et|te|ten)?|committe|einchecken)\b/i },
25857
25874
  { op: "push", re: /\b(push(e|en|t)?|hochladen|hochschieben|raufschieben)\b/i },
25858
- { op: "branch", re: /\b(branch|feature-branch|zweig)\b/i },
25875
+ // A CREATION VERB IS REQUIRED, as for `pr-create` above and for the same
25876
+ // reason. The bare noun could not tell an authorization from a mention, and
25877
+ // the mention is the dangerous half: "den branch nicht löschen" names the
25878
+ // object precisely in order to forbid acting on it, yet authorized branch
25879
+ // creation — the negation trails a bare-noun match, so neither the
25880
+ // look-behind nor the span check in `negatedBefore` can reach it. Widened
25881
+ // 2026-09-03 alongside the destructive-coverage work that measured it.
25882
+ {
25883
+ op: "branch",
25884
+ 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
25885
+ },
25859
25886
  // A creation verb is required — "schau dir den PR an" is not "open a PR".
25860
25887
  {
25861
25888
  op: "pr-create",
@@ -25869,20 +25896,22 @@ var PHRASES = [
25869
25896
  // "never auto-merge" each returned ["pr-merge"] — a prompt saying DO NOT
25870
25897
  // MERGE authorized a merge. The noun-sense lookahead was never a negator.
25871
25898
  //
25872
- // The lookBEHIND is line-scoped by `[^.!?\n]` on purpose, the same scoping
25873
- // `turn_end_gate_hook.ts` uses for its own negation exclusion (`:330`) and
25874
- // its negated-claim window (`:490-493`). A sentence boundary ends a
25875
- // negation's reach: "Do not push. Merge PR #12." is two instructions, and a
25876
- // guard that let the first suppress the second would silently stop
25877
- // authorizing merges the user DID order — the failure mode that is worse
25878
- // than the defect, because nothing happens and nothing says why.
25879
- //
25880
- // Reusing that vocabulary rather than inventing a third is deliberate: two
25899
+ // The inline negative lookBEHIND that used to live in this pattern was
25900
+ // REMOVED on 2026-09-03, and its own comment is why. It warned that "two
25881
25901
  // negation vocabularies in one tree drift, and the drift is invisible until
25882
- // a prompt lands in the gap between them.
25902
+ // a prompt lands in the gap between them" — and by then they had drifted:
25903
+ // `negatedBefore` became clause-scanned in both directions while this
25904
+ // lookbehind stayed a blind 30-character window with no forward half. The
25905
+ // prompt that landed in the gap is `"nicht pushen, aber mergen"`, which the
25906
+ // lookbehind refused (13 characters, inside its window, across a contrast
25907
+ // cue it could not see) while the clause scan correctly authorizes it.
25908
+ //
25909
+ // Every phrase match already passes through `negatedBefore` at the call
25910
+ // site, so the guard is not weakened by the removal — it is applied once,
25911
+ // by the one vocabulary this file's comment asked for.
25883
25912
  {
25884
25913
  op: "pr-merge",
25885
- 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
25914
+ re: /\b(merge|merg(e|en|st|t)|zusammenf(ü|ue)hren|reinmergen)\b(?!\s*[- ]?(conflict|konflikt|commit|base|queue|state|status))/i
25886
25915
  },
25887
25916
  // A tag is an ACTION here — bare "Tag" is the German word for day, and
25888
25917
  // "Version" is an ordinary noun. Both authorized a BLOCK op before this.
@@ -25892,7 +25921,111 @@ var PHRASES = [
25892
25921
  op: "release",
25893
25922
  re: /\b(releasen?|ver(ö|oe)ffentlich(e|en))\b(?!\s*[- ]?(notes?|branch|candidate|pr\b|datum|date|zweig))/i
25894
25923
  },
25895
- { op: "publish", re: /\b(publish(e|en)?|publiziere[n]?)\b/i }
25924
+ { op: "publish", re: /\b(publish(e|en)?|publiziere[n]?)\b/i },
25925
+ // ── Authorization vocabulary for the ops added 2026-09-03.
25926
+ //
25927
+ // NON-NEGOTIABLE, and the reason it ships in the same change as the new BLOCK
25928
+ // ops: a blocked operation with no phrase that authorizes it is a dead end.
25929
+ // The guard refuses, the user says "doch, mach das", the classifier records
25930
+ // nothing, and the refusal repeats forever. Every op the guard can block must
25931
+ // have a sentence that unblocks it, or the user's override is a claim rather
25932
+ // than a mechanism.
25933
+ //
25934
+ // Tight on purpose. Under-matching costs one confirmation; over-matching
25935
+ // authorizes something the user did not ask for, so each pattern demands the
25936
+ // object noun rather than a bare verb wherever the verb is ordinary German.
25937
+ //
25938
+ // `unpublish` before nothing in particular — `\bpublish\b` does not match
25939
+ // inside "unpublish" (no word boundary between `n` and `p`), so the two never
25940
+ // collide.
25941
+ // TWO SHAPES EVERY GERMAN PATTERN BELOW MUST CARRY, both learned from tests
25942
+ // that went red rather than from inspection:
25943
+ //
25944
+ // 1. THE IMPERATIVE DROPS ITS `-e`. "entfern die worktree", "archivier das
25945
+ // repo", "deaktivier den workflow" are what people type; `entfern(e|en)`
25946
+ // matches none of them. Every verb ending is therefore `(e|en)?`.
25947
+ // 2. WORD ORDER GOES BOTH WAYS. "archivier das repo" and "das repo
25948
+ // archivieren" are one instruction, and a pattern anchored on the noun
25949
+ // first sees only the second.
25950
+ //
25951
+ // The object noun stays REQUIRED in every pattern that has one. That is what
25952
+ // keeps the loosened verb endings from over-authorizing: "lösch" alone
25953
+ // authorizes nothing, "lösch den branch" authorizes a branch deletion.
25954
+ { op: "unpublish", re: /\bunpublish(e|en)?\b/i },
25955
+ {
25956
+ op: "deprecate",
25957
+ re: /\bdeprecate(d|n)?\b|\bals\s+veraltet\s+markier(e|en)?\b/i
25958
+ },
25959
+ {
25960
+ op: "release-asset",
25961
+ 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
25962
+ },
25963
+ // A VERB IS REQUIRED, like `tag` and `release` above and for the same reason.
25964
+ // A bare noun cannot tell an authorization from a mention, and here the
25965
+ // mention is the dangerous half: "die branch-protection nicht anfassen" names
25966
+ // the object precisely in order to forbid touching it. Measured — this was the
25967
+ // one row of the negation corpus that neither the look-behind nor the
25968
+ // span check could reach, because the negation trails a bare-noun match.
25969
+ {
25970
+ op: "protection",
25971
+ 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
25972
+ },
25973
+ {
25974
+ op: "workflow-toggle",
25975
+ 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
25976
+ },
25977
+ {
25978
+ op: "repo-lifecycle",
25979
+ 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
25980
+ },
25981
+ {
25982
+ op: "review-approve",
25983
+ 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
25984
+ },
25985
+ {
25986
+ op: "force-push",
25987
+ re: /\bforce[-\s]?push(e|en|st)?\b|--force(-with-lease)?\b|\bmit\s+gewalt\s+push(e|en)?\b/i
25988
+ },
25989
+ {
25990
+ op: "worktree-remove",
25991
+ 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
25992
+ },
25993
+ // The `-x` flag is the whole distinction: without it `git clean` spares
25994
+ // ignored files, with it `.env` and local certificates go too.
25995
+ {
25996
+ op: "clean-ignored",
25997
+ 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
25998
+ },
25999
+ {
26000
+ op: "tag-force",
26001
+ 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
26002
+ },
26003
+ { op: "rebase", re: /\brebase(n|st|d)?\b/i },
26004
+ {
26005
+ op: "reset-hard",
26006
+ re: /\breset\s+--hard\b|\bhard\s+reset\b|\bhart\s+zur(ü|ue)cksetz(e|en)?\b/i
26007
+ },
26008
+ { op: "clean", re: /\bgit\s+clean\b/i },
26009
+ {
26010
+ op: "stash-drop",
26011
+ 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
26012
+ },
26013
+ {
26014
+ op: "branch-delete",
26015
+ 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
26016
+ },
26017
+ {
26018
+ op: "close",
26019
+ 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
26020
+ },
26021
+ // Enabling auto-merge is its own op on the guard side, so it needs its own
26022
+ // sentence. Without one it is the dead end this block exists to prevent: the
26023
+ // plain merge phrase authorizes `pr-merge` and never `pr-merge-auto`, so the
26024
+ // refusal would have repeated forever.
26025
+ {
26026
+ op: "pr-merge-auto",
26027
+ re: /\bauto[-\s]?merge\b|\bmerge\b[^.\n]{0,20}\bautomatisch\b|\bautomatisch\b[^.\n]{0,20}\bmerg(e|en)\b/i
26028
+ }
25896
26029
  ];
25897
26030
  var 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;
25898
26031
  function isInterrogative(prose) {
@@ -25900,13 +26033,16 @@ function isInterrogative(prose) {
25900
26033
  if (!t) {
25901
26034
  return false;
25902
26035
  }
25903
- 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(
26036
+ if (/\?\s*$/.test(t)) {
26037
+ return true;
26038
+ }
26039
+ 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(
25904
26040
  t
25905
26041
  );
25906
26042
  if (hasImperative) {
25907
26043
  return false;
25908
26044
  }
25909
- 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(
26045
+ 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(
25910
26046
  t
25911
26047
  );
25912
26048
  }
@@ -25953,16 +26089,164 @@ function splitFences(prompt) {
25953
26089
  function _commandLines(fence) {
25954
26090
  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));
25955
26091
  }
26092
+ function _errText(err) {
26093
+ const t = err instanceof Error ? err.message : String(err);
26094
+ return t.replace(/\s+/g, " ").trim().slice(0, 200);
26095
+ }
26096
+ var PR_NUMBER_MAX = 2147483647;
26097
+ function extractMergeTargets(prompt) {
26098
+ const out = /* @__PURE__ */ new Set();
26099
+ const re = /(?:#|\bpr[- ]?|\bpull[- ]request\s+#?)(\d{1,10})\b/giu;
26100
+ let m;
26101
+ while ((m = re.exec(prompt)) !== null) {
26102
+ const n = Number.parseInt(m[1], 10);
26103
+ if (Number.isSafeInteger(n) && n > 0 && n <= PR_NUMBER_MAX) {
26104
+ out.add(n);
26105
+ }
26106
+ }
26107
+ return [...out].sort((a, b) => a - b);
26108
+ }
26109
+ var 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;
26110
+ function isRevocation(prompt) {
26111
+ return REVOKE_RE.test(prompt);
26112
+ }
26113
+ function _grantId(session_id, at, targets) {
26114
+ return `${sessionSlug(session_id) || "nosession"}:${at}:${targets.join(",")}`;
26115
+ }
26116
+ function foldGrants(prior, prompt, authorized, session_id, now) {
26117
+ if (isRevocation(prompt)) {
26118
+ return [];
26119
+ }
26120
+ const carried = prior.filter((g) => g.consumed.length < g.targets.length);
26121
+ if (!authorized.includes("pr-merge")) {
26122
+ return carried;
26123
+ }
26124
+ const targets = extractMergeTargets(prompt);
26125
+ if (targets.length === 0) {
26126
+ return carried;
26127
+ }
26128
+ const at = now.toISOString();
26129
+ const already = new Set(carried.flatMap((g) => g.targets));
26130
+ const fresh = targets.filter((t) => !already.has(t));
26131
+ if (fresh.length === 0) {
26132
+ return carried;
26133
+ }
26134
+ return [
26135
+ ...carried,
26136
+ {
26137
+ id: _grantId(session_id, at, fresh),
26138
+ op: "pr-merge",
26139
+ targets: fresh,
26140
+ consumed: [],
26141
+ granted_at: at,
26142
+ evidence: prompt.trim().slice(0, 120)
26143
+ }
26144
+ ];
26145
+ }
26146
+ var NEGATION_WORD = /\b(nicht|nichts|kein(e|en|em|er|es)?|niemals|nie|no|not|dont|don't|never|without|ohne)\b/i;
26147
+ var CONTRAST_CUE = /\b(aber|sondern|jedoch|but|however|instead)\b/gi;
26148
+ var 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;
26149
+ function isSentenceBoundary(text, at) {
26150
+ const mark = text[at];
26151
+ if (mark !== ".") {
26152
+ return true;
26153
+ }
26154
+ if (/\d/.test(text[at - 1] ?? "") && /\d/.test(text[at + 1] ?? "")) {
26155
+ return false;
26156
+ }
26157
+ return !ABBREVIATION_BEFORE_DOT.test(text.slice(0, at + 1));
26158
+ }
26159
+ function sentenceBounds(text, index) {
26160
+ let start = 0;
26161
+ for (let i = index - 1; i >= 0; i -= 1) {
26162
+ const c = text[i];
26163
+ if ((c === "." || c === "!" || c === "?" || c === "\n") && isSentenceBoundary(text, i)) {
26164
+ start = i + 1;
26165
+ break;
26166
+ }
26167
+ }
26168
+ let end = text.length;
26169
+ for (let i = index; i < text.length; i += 1) {
26170
+ const c = text[i];
26171
+ if ((c === "." || c === "!" || c === "?" || c === "\n") && isSentenceBoundary(text, i)) {
26172
+ end = i;
26173
+ break;
26174
+ }
26175
+ }
26176
+ return { start, end };
26177
+ }
26178
+ function _sentences(text) {
26179
+ const out = [];
26180
+ let from = 0;
26181
+ for (let i = 0; i < text.length; i += 1) {
26182
+ const c = text[i];
26183
+ if ((c === "." || c === "!" || c === "?" || c === "\n") && isSentenceBoundary(text, i)) {
26184
+ out.push(text.slice(from, i + 1));
26185
+ from = i + 1;
26186
+ }
26187
+ }
26188
+ if (from < text.length) {
26189
+ out.push(text.slice(from));
26190
+ }
26191
+ return out.filter((t) => t.trim() !== "");
26192
+ }
26193
+ function clauseStart(text, from, to) {
26194
+ let start = from;
26195
+ const seg = text.slice(from, to);
26196
+ for (let i = seg.length - 1; i >= 0; i -= 1) {
26197
+ const c = seg[i];
26198
+ if (c === "," || c === ";" || c === ":") {
26199
+ start = from + i + 1;
26200
+ break;
26201
+ }
26202
+ }
26203
+ CONTRAST_CUE.lastIndex = 0;
26204
+ let m;
26205
+ while ((m = CONTRAST_CUE.exec(seg)) !== null) {
26206
+ const after = from + m.index + m[0].length;
26207
+ if (after > start) {
26208
+ start = after;
26209
+ }
26210
+ }
26211
+ return start;
26212
+ }
26213
+ function clauseEnd(text, from, to) {
26214
+ const seg = text.slice(from, to);
26215
+ let end = to;
26216
+ const punct = seg.search(/[,;:]/);
26217
+ if (punct !== -1) {
26218
+ end = from + punct;
26219
+ }
26220
+ CONTRAST_CUE.lastIndex = 0;
26221
+ const m = CONTRAST_CUE.exec(seg);
26222
+ if (m !== null && from + m.index < end) {
26223
+ end = from + m.index;
26224
+ }
26225
+ return end;
26226
+ }
26227
+ function negatedBefore(text, index, matched = "") {
26228
+ const { start, end } = sentenceBounds(text, index);
26229
+ const before = text.slice(clauseStart(text, start, index), index);
26230
+ const afterFrom = index + matched.length;
26231
+ const after = afterFrom < end ? text.slice(afterFrom, clauseEnd(text, afterFrom, end)) : "";
26232
+ return NEGATION_WORD.test(before) || NEGATION_WORD.test(matched) || NEGATION_WORD.test(after);
26233
+ }
25956
26234
  function classifyAuthorization(prompt) {
25957
26235
  const { prose, fences } = splitFences(prompt);
25958
26236
  const authorized = /* @__PURE__ */ new Set();
25959
26237
  const evidence = {};
25960
26238
  const instruction = prose.split("\n").filter((l) => !OUTPUT_LINE.test(l)).join("\n");
25961
- for (const { op, re } of isInterrogative(instruction) ? [] : PHRASES) {
25962
- const m = re.exec(instruction);
25963
- if (m) {
25964
- authorized.add(op);
25965
- evidence[op] = `prose: "${m[0]}"`;
26239
+ for (const { op, re } of PHRASES) {
26240
+ for (const sentence of _sentences(instruction)) {
26241
+ if (isInterrogative(sentence)) {
26242
+ continue;
26243
+ }
26244
+ const m = re.exec(sentence);
26245
+ if (m && !negatedBefore(sentence, m.index, m[0])) {
26246
+ authorized.add(op);
26247
+ evidence[op] = `prose: "${m[0]}"`;
26248
+ break;
26249
+ }
25966
26250
  }
25967
26251
  }
25968
26252
  for (const fence of fences) {
@@ -25982,6 +26266,72 @@ function classifyAuthorization(prompt) {
25982
26266
  }
25983
26267
  return { authorized: [...authorized], evidence };
25984
26268
  }
26269
+ function readGrants(consumer_root, session_id) {
26270
+ try {
26271
+ const raw = fs55.readFileSync(path55.join(consumer_root, ledgerFileFor(session_id)), "utf8");
26272
+ const decoded = JSON.parse(raw);
26273
+ if (!_isObject8(decoded) || !Array.isArray(decoded["grants"])) {
26274
+ return [];
26275
+ }
26276
+ const owner = typeof decoded["session_id"] === "string" ? decoded["session_id"] : "";
26277
+ if (session_id && owner && owner !== session_id) {
26278
+ return [];
26279
+ }
26280
+ return decoded["grants"].flatMap((g) => {
26281
+ if (!_isObject8(g)) return [];
26282
+ const op = g["op"];
26283
+ if (typeof op !== "string" || !ALL_OPS.includes(op)) return [];
26284
+ const targets = Array.isArray(g["targets"]) ? g["targets"] : [];
26285
+ const consumed = Array.isArray(g["consumed"]) ? g["consumed"] : [];
26286
+ return [
26287
+ {
26288
+ id: String(g["id"] ?? ""),
26289
+ op,
26290
+ targets: targets.filter((t) => typeof t === "number"),
26291
+ consumed: consumed.filter((t) => typeof t === "number"),
26292
+ granted_at: String(g["granted_at"] ?? ""),
26293
+ evidence: String(g["evidence"] ?? "")
26294
+ }
26295
+ ];
26296
+ });
26297
+ } catch {
26298
+ return [];
26299
+ }
26300
+ }
26301
+ function consumeGrantTarget(consumer_root, session_id, target) {
26302
+ const file = path55.join(consumer_root, ledgerFileFor(session_id));
26303
+ let decoded;
26304
+ try {
26305
+ decoded = JSON.parse(fs55.readFileSync(file, "utf8"));
26306
+ } catch {
26307
+ return;
26308
+ }
26309
+ if (!_isObject8(decoded) || !Array.isArray(decoded["grants"])) {
26310
+ return;
26311
+ }
26312
+ let touched = false;
26313
+ for (const g of decoded["grants"]) {
26314
+ if (!_isObject8(g) || !Array.isArray(g["targets"]) || !Array.isArray(g["consumed"])) {
26315
+ continue;
26316
+ }
26317
+ const targets = g["targets"];
26318
+ const consumed = g["consumed"];
26319
+ if (targets.includes(target) && !consumed.includes(target)) {
26320
+ consumed.push(target);
26321
+ touched = true;
26322
+ }
26323
+ }
26324
+ if (touched) {
26325
+ try {
26326
+ atomic_write_json(file, decoded);
26327
+ } catch (err) {
26328
+ process.stderr.write(
26329
+ `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.
26330
+ `
26331
+ );
26332
+ }
26333
+ }
26334
+ }
25985
26335
  function takePending(consumer_root, session_id, now) {
25986
26336
  const file = path55.join(consumer_root, pendingFileFor(session_id));
25987
26337
  let raw;
@@ -26050,16 +26400,32 @@ function run10(stdin_text, options) {
26050
26400
  authorized.push(pending.op);
26051
26401
  evidence[pending.op] = `confirmation of the refused \`${pending.op}\` (refused ${pending.refused_at}): "${prompt.trim().slice(0, 40)}"`;
26052
26402
  }
26403
+ const now = /* @__PURE__ */ new Date();
26053
26404
  const ledger = {
26054
26405
  session_id: typeof envelope["session_id"] === "string" ? envelope["session_id"] : "",
26055
- detected_at: (/* @__PURE__ */ new Date()).toISOString(),
26406
+ detected_at: now.toISOString(),
26056
26407
  authorized,
26057
26408
  evidence,
26058
- prompt_chars: prompt.length
26409
+ prompt_chars: prompt.length,
26410
+ // Grants survive this write; `authorized` does not. That asymmetry IS the
26411
+ // decision. A bare operation name stays one-shot exactly as `commit-policy`
26412
+ // requires, and only an authorization that also froze its objects earns a
26413
+ // life longer than the turn. See ADR-252.
26414
+ grants: foldGrants(
26415
+ readGrants(options.consumer_root, session),
26416
+ prompt,
26417
+ authorized,
26418
+ session,
26419
+ now
26420
+ )
26059
26421
  };
26060
26422
  try {
26061
26423
  atomic_write_json(path55.join(options.consumer_root, ledgerFileFor(session)), ledger);
26062
- } catch {
26424
+ } catch (err) {
26425
+ process.stderr.write(
26426
+ `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.
26427
+ `
26428
+ );
26063
26429
  }
26064
26430
  return EXIT_ALLOW10;
26065
26431
  }
@@ -26104,6 +26470,116 @@ if (_isCliEntry24()) {
26104
26470
  import fs56 from "node:fs";
26105
26471
  import path56 from "node:path";
26106
26472
  import { fileURLToPath as fileURLToPath34, pathToFileURL as pathToFileURL27 } from "node:url";
26473
+
26474
+ // src/scripts/hooks/merge_impact.ts
26475
+ import { execFileSync as execFileSync3 } from "node:child_process";
26476
+ var DESTRUCTIVE_MARKERS = [
26477
+ // Schema removal. Matched on ADDED lines only — a migration that *removes* a
26478
+ // dropColumn call is the opposite of destructive, and matching the raw patch
26479
+ // would score it the same way.
26480
+ { label: "migration drops a column or table", re: /^\+.*\b(dropColumn|dropTable|dropIfExists|removeColumn|drop_column|drop_table)\b/im },
26481
+ { label: "raw DROP or TRUNCATE in a migration", re: /^\+.*\b(DROP\s+(TABLE|COLUMN|INDEX|CONSTRAINT)|TRUNCATE\s+TABLE)\b/im },
26482
+ // The author said so themselves. Conventional Commits marks a breaking change
26483
+ // with a `!` before the colon or a body trailer.
26484
+ { label: "author marked a BREAKING CHANGE", re: /^\+.*BREAKING[ -]CHANGE/m },
26485
+ // A removed export is the shape most likely to break an importer.
26486
+ { 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 },
26487
+ { label: "a public PHP or Python symbol was removed", re: /^-\s*(public\s+function|def\s+[a-z_]+\s*\()/m },
26488
+ // Route removal breaks callers that never imported anything.
26489
+ { 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 },
26490
+ { label: "a whole file was deleted", re: /^deleted file mode /m }
26491
+ ];
26492
+ var SCHEMA_REMOVAL_BODY = /\b(dropColumn|dropTable|dropIfExists|removeColumn|drop_column|drop_table|DROP\s+(TABLE|COLUMN|INDEX|CONSTRAINT)|TRUNCATE\s+TABLE)\b/i;
26493
+ var RENAME_METADATA = /^rename (?:from|to) /m;
26494
+ function fileBlocks(patch) {
26495
+ return patch.split(/^(?=diff --git )/m).filter((b) => b.startsWith("diff --git "));
26496
+ }
26497
+ function renamedSchemaRemoval(patch) {
26498
+ for (const block of fileBlocks(patch)) {
26499
+ if (!RENAME_METADATA.test(block)) {
26500
+ continue;
26501
+ }
26502
+ for (const line of block.split("\n")) {
26503
+ if (line.startsWith("-") && !line.startsWith("---") && SCHEMA_REMOVAL_BODY.test(line)) {
26504
+ return true;
26505
+ }
26506
+ }
26507
+ }
26508
+ return false;
26509
+ }
26510
+ var MAJOR_BUMP = /^-\s*"version"\s*:\s*"(\d+)\.[^"]*"[\s\S]{0,200}?^\+\s*"version"\s*:\s*"(\d+)\./m;
26511
+ function classifyDiff(patch) {
26512
+ if (patch.trim() === "") {
26513
+ return { verdict: "undecidable", markers: [], reason: "empty diff" };
26514
+ }
26515
+ const markers = [];
26516
+ for (const { label, re } of DESTRUCTIVE_MARKERS) {
26517
+ if (re.test(patch)) {
26518
+ markers.push(label);
26519
+ }
26520
+ }
26521
+ if (renamedSchemaRemoval(patch)) {
26522
+ markers.push("a renamed file drops a column or table");
26523
+ }
26524
+ const bump = MAJOR_BUMP.exec(patch);
26525
+ if (bump && bump[1] !== bump[2]) {
26526
+ markers.push(`major version bump ${bump[1]}.x to ${bump[2]}.x`);
26527
+ }
26528
+ const filesChanged = (patch.match(/^diff --git /gm) ?? []).length;
26529
+ if (markers.length > 0) {
26530
+ return { verdict: "destructive", markers, filesChanged };
26531
+ }
26532
+ return { verdict: "additive", markers: [], filesChanged };
26533
+ }
26534
+ function fetchPatch(pr, opts = {}) {
26535
+ const run13 = opts.exec ?? execFileSync3;
26536
+ const cwd = opts.cwd ?? process.cwd();
26537
+ const capture = (file, args) => run13(file, args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 15e3 });
26538
+ for (const ref of [`refs/pull/${pr}/head`, `pull/${pr}/head`]) {
26539
+ try {
26540
+ const base = capture("git", ["merge-base", "HEAD", ref]).trim();
26541
+ if (base) {
26542
+ const patch = capture("git", ["diff", `${base}...${ref}`]);
26543
+ if (patch.trim() !== "") {
26544
+ return { patch, source: "local" };
26545
+ }
26546
+ }
26547
+ } catch {
26548
+ }
26549
+ }
26550
+ if (opts.allowNetwork === false) {
26551
+ return { patch: "", source: "none", reason: "no local ref and network reads disabled" };
26552
+ }
26553
+ try {
26554
+ const patch = capture("gh", ["pr", "diff", String(pr), "--patch"]);
26555
+ if (patch.trim() !== "") {
26556
+ return { patch, source: "gh" };
26557
+ }
26558
+ return { patch: "", source: "none", reason: "gh returned an empty diff" };
26559
+ } catch {
26560
+ return { patch: "", source: "none", reason: "no local ref, and gh pr diff failed" };
26561
+ }
26562
+ }
26563
+ function analyseMergeImpact(pr, opts = {}) {
26564
+ const got = fetchPatch(pr, opts);
26565
+ if (got.source === "none") {
26566
+ return { verdict: "undecidable", markers: [], ...got.reason ? { reason: got.reason } : {} };
26567
+ }
26568
+ return classifyDiff(got.patch);
26569
+ }
26570
+ function describeImpact(pr, impact) {
26571
+ const say = `Say "merge #${pr}" to authorize it.`;
26572
+ switch (impact.verdict) {
26573
+ case "additive":
26574
+ 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}`;
26575
+ case "destructive":
26576
+ return `Impact scan: PR #${pr} is NOT additive \u2014 ${impact.markers.join("; ")}. Read the diff before authorizing. ${say}`;
26577
+ default:
26578
+ return `Impact scan: could not read the diff for PR #${pr} (${impact.reason ?? "unknown"}), so nothing is known about what it changes. ${say}`;
26579
+ }
26580
+ }
26581
+
26582
+ // src/scripts/hooks/block_unauthorized_git.ts
26107
26583
  var EXIT_ALLOW11 = 0;
26108
26584
  var EXIT_BLOCK = 1;
26109
26585
  function _isObject9(v) {
@@ -26116,13 +26592,52 @@ var BLOCK_OPS = /* @__PURE__ */ new Set([
26116
26592
  "pr-merge",
26117
26593
  // Enabling auto-merge commits the outcome to a condition the agent does not
26118
26594
  // control, so it is blocked on the same terms as the merge it schedules.
26119
- "pr-merge-auto"
26595
+ "pr-merge-auto",
26596
+ // ── Added 2026-09-03. Every one is already never-autonomous in the
26597
+ // `non-destructive-by-default` prose; the measurement that drove it is in the
26598
+ // `GitOp` union's own note — 17 of 25 borderline operations classified as
26599
+ // NOTHING, including three worse than anything the guard already caught.
26600
+ /** The undo that is more destructive than the act: lockfiles break. */
26601
+ "unpublish",
26602
+ /** Metadata only, and every install on earth starts warning. */
26603
+ "deprecate",
26604
+ /** Replaces bytes consumers already hold a checksum for. */
26605
+ "release-asset",
26606
+ /** Deletes the guard rather than the data — everything after is unguarded. */
26607
+ "protection",
26608
+ /** No data lost; the gate that protects everything downstream is off. */
26609
+ "workflow-toggle",
26610
+ /** Reach outside the repository, and mostly one-way. */
26611
+ "repo-lifecycle",
26612
+ /** No code moves, and an armed auto-merge is released. */
26613
+ "review-approve",
26614
+ /** Discards commits that landed after the last fetch, on a shared ref. */
26615
+ "force-push",
26616
+ /** Destroys uncommitted work belonging to a session this one cannot see. */
26617
+ "worktree-remove",
26618
+ /** `-x` is the flag that takes `.env` and local certificates. */
26619
+ "clean-ignored"
26120
26620
  ]);
26121
26621
  var WARN_OPS = /* @__PURE__ */ new Set([
26122
26622
  "commit",
26123
26623
  "push",
26124
26624
  "pr-create",
26125
- "branch"
26625
+ "branch",
26626
+ // ── Added 2026-09-03, deliberately at warn rather than block.
26627
+ //
26628
+ // Each is destructive in some reading, and each is also routine enough that
26629
+ // blocking it would be the friction that gets the whole guard routed around —
26630
+ // the failure mode this estate has already recorded twice. `reset-hard` is the
26631
+ // sharpest case and stays here for a decidable reason: the Hard Floor's
26632
+ // qualifier is "past unpushed work", and the command text cannot say whether
26633
+ // the work was pushed.
26634
+ "tag-force",
26635
+ "rebase",
26636
+ "reset-hard",
26637
+ "clean",
26638
+ "stash-drop",
26639
+ "branch-delete",
26640
+ "close"
26126
26641
  ]);
26127
26642
  var G = "(?:-[A-Za-z-]+(?:=\\S+)?(?:\\s+\\S+)?\\s+)*";
26128
26643
  var P = "^(?:\\S*\\/)?";
@@ -26168,6 +26683,72 @@ var COMMAND_OPS = [
26168
26683
  op: "pr-create",
26169
26684
  re: new RegExp(`${P}gh\\s+pr\\s+create\\b|${ghApiWrite("\\/pulls\\b").source}`, "i")
26170
26685
  },
26686
+ // ── Added 2026-09-03. Placement inside this table is as load-bearing as the
26687
+ // patterns: the loop takes the MOST SEVERE match per segment, but within one
26688
+ // op the first pattern wins, so anything more specific than a plain `git push`
26689
+ // has to sit above it.
26690
+ // `npm unpublish` is NOT reached by the publish pattern — `\bpublish\b` needs
26691
+ // a word boundary that `unpublish` does not provide between `n` and `p`. That
26692
+ // is why it classified as nothing for as long as it did.
26693
+ { op: "unpublish", re: new RegExp(`${P}(npm|pnpm|yarn)\\s+${G}unpublish\\b`, "i") },
26694
+ { op: "deprecate", re: new RegExp(`${P}(npm|pnpm|yarn)\\s+${G}deprecate\\b`, "i") },
26695
+ {
26696
+ op: "release-asset",
26697
+ re: new RegExp(`${P}gh\\s+release\\s+(upload|edit|delete)\\b`, "i")
26698
+ },
26699
+ {
26700
+ op: "protection",
26701
+ re: new RegExp(
26702
+ `${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`,
26703
+ "i"
26704
+ )
26705
+ },
26706
+ {
26707
+ op: "workflow-toggle",
26708
+ re: new RegExp(`${P}gh\\s+workflow\\s+(disable|enable)\\b`, "i")
26709
+ },
26710
+ {
26711
+ op: "repo-lifecycle",
26712
+ re: new RegExp(
26713
+ `${P}gh\\s+repo\\s+(archive|delete|unarchive)\\b|${P}gh\\s+repo\\s+edit\\b(?=[^\\n]*--visibility\\b)`,
26714
+ "i"
26715
+ )
26716
+ },
26717
+ {
26718
+ op: "review-approve",
26719
+ re: new RegExp(`${P}gh\\s+pr\\s+review\\b(?=[^\\n]*--approve\\b)`, "i")
26720
+ },
26721
+ // Above `push`, and both spellings: `--force` and `--force-with-lease`. The
26722
+ // "safe force" is in the same class here because what it is safe against is a
26723
+ // stale local view, not a collaborator who pushed after your fetch.
26724
+ {
26725
+ op: "force-push",
26726
+ re: new RegExp(`${P}git\\s+${G}push\\b[^\\n;|&]*(--force\\b|--force-with-lease\\b|\\s-f\\b)`, "i")
26727
+ },
26728
+ {
26729
+ op: "worktree-remove",
26730
+ re: new RegExp(`${P}git\\s+worktree\\s+remove\\b[^\\n;|&]*(--force\\b|\\s-f\\b)`, "i")
26731
+ },
26732
+ // `-x` (or `-X`) anywhere in the flag cluster is what reaches ignored files.
26733
+ {
26734
+ op: "clean-ignored",
26735
+ re: new RegExp(`${P}git\\s+clean\\b[^\\n;|&]*-[A-Za-z]*x`, "i")
26736
+ },
26737
+ { op: "tag-force", re: new RegExp(`${P}git\\s+tag\\b[^\\n;|&]*(-f\\b|--force\\b)`, "i") },
26738
+ { op: "rebase", re: new RegExp(`${P}git\\s+${G}rebase\\b`, "i") },
26739
+ { op: "reset-hard", re: new RegExp(`${P}git\\s+${G}reset\\b[^\\n;|&]*--hard\\b`, "i") },
26740
+ { op: "clean", re: new RegExp(`${P}git\\s+clean\\b`, "i") },
26741
+ { op: "stash-drop", re: new RegExp(`${P}git\\s+stash\\s+(drop|clear)\\b`, "i") },
26742
+ // Remote deletion and the local `-D` are one op: both remove a ref, and the
26743
+ // remote form additionally closes any open PR pointing at it.
26744
+ {
26745
+ op: "branch-delete",
26746
+ re: new RegExp(
26747
+ `${P}git\\s+branch\\b[^\\n;|&]*(-D\\b|--delete\\b)|${P}git\\s+${G}push\\b[^\\n;|&]*(--delete\\b|\\s:\\S)`,
26748
+ "i"
26749
+ )
26750
+ },
26751
+ { op: "close", re: new RegExp(`${P}gh\\s+(pr|issue)\\s+close\\b`, "i") },
26171
26752
  { op: "push", re: new RegExp(`${P}git\\s+${G}push\\b`, "i") },
26172
26753
  { op: "commit", re: new RegExp(`${P}git\\s+${G}commit\\b`, "i") },
26173
26754
  { op: "branch", re: new RegExp(`${P}git\\s+${G}(checkout\\s+-b|switch\\s+-c)\\b`, "i") }
@@ -26360,8 +26941,29 @@ function commandOp(command) {
26360
26941
  }
26361
26942
  return found;
26362
26943
  }
26944
+ function mergeTargetOf(command) {
26945
+ const gh = /\bgh\s+pr\s+merge\s+(?:-{1,2}\S+\s+)*?(\d{1,10})\b/i.exec(command);
26946
+ if (gh) {
26947
+ return _boundedTarget(gh[1]);
26948
+ }
26949
+ const api = /\/pulls\/(\d{1,10})\/merge\b/i.exec(command);
26950
+ return api ? _boundedTarget(api[1]) : null;
26951
+ }
26952
+ function _boundedTarget(digits) {
26953
+ const n = Number.parseInt(digits, 10);
26954
+ return Number.isSafeInteger(n) && n > 0 && n <= PR_NUMBER_MAX ? n : null;
26955
+ }
26956
+ function grantCovers(grants, op, target) {
26957
+ if (op !== "pr-merge" || target === null) {
26958
+ return null;
26959
+ }
26960
+ return grants.find(
26961
+ (g) => g.op === "pr-merge" && g.targets.includes(target) && !g.consumed.includes(target)
26962
+ ) ?? null;
26963
+ }
26363
26964
  var LEDGER_MAX_AGE_MS = 30 * 60 * 1e3;
26364
26965
  function _loadLedger(consumer_root, session_id, now) {
26966
+ const grants = readGrants(consumer_root, session_id);
26365
26967
  const candidates = [ledgerFileFor(session_id)];
26366
26968
  if (!candidates.includes(STATE_FILE7)) {
26367
26969
  candidates.push(STATE_FILE7);
@@ -26369,10 +26971,10 @@ function _loadLedger(consumer_root, session_id, now) {
26369
26971
  for (const rel of candidates) {
26370
26972
  const found = _readLedgerFile(path56.join(consumer_root, rel), session_id, now);
26371
26973
  if (found !== null) {
26372
- return found;
26974
+ return { ...found, grants };
26373
26975
  }
26374
26976
  }
26375
- return { authorized: /* @__PURE__ */ new Set(), present: false };
26977
+ return { authorized: /* @__PURE__ */ new Set(), present: false, grants };
26376
26978
  }
26377
26979
  function _readLedgerFile(file, session_id, now) {
26378
26980
  try {
@@ -26396,7 +26998,7 @@ function _readLedgerFile(file, session_id, now) {
26396
26998
  }
26397
26999
  return null;
26398
27000
  }
26399
- function decide(command, ledger) {
27001
+ function decide(command, ledger, impact) {
26400
27002
  if (command === null) {
26401
27003
  return { exit: EXIT_ALLOW11, stdout: "", stderr: "" };
26402
27004
  }
@@ -26404,9 +27006,14 @@ function decide(command, ledger) {
26404
27006
  if (op === null || ledger.authorized.has(op)) {
26405
27007
  return { exit: EXIT_ALLOW11, stdout: "", stderr: "" };
26406
27008
  }
27009
+ const target = mergeTargetOf(command);
27010
+ if (grantCovers(ledger.grants ?? [], op, target) && target !== null) {
27011
+ return { exit: EXIT_ALLOW11, stdout: "", stderr: "", consumedTarget: target };
27012
+ }
26407
27013
  const shown = command.length > 120 ? `${command.slice(0, 117)}\u2026` : command;
26408
27014
  if (BLOCK_OPS.has(op)) {
26409
- 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.`;
27015
+ const scan = op === "pr-merge" && target !== null && impact ? ` ${impact}` : "";
27016
+ 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}.${scan} Ask the user, in one numbered-options block, and run it only after they answer this turn.`;
26410
27017
  return { exit: EXIT_BLOCK, stdout: "", stderr: `${reason}
26411
27018
  `, blockedOp: op };
26412
27019
  }
@@ -26434,10 +27041,24 @@ function run11(stdin_text, options) {
26434
27041
  }
26435
27042
  }
26436
27043
  const session_id = typeof envelope["session_id"] === "string" ? envelope["session_id"] : "";
26437
- const decision = decide(
26438
- extractCommand(envelope),
26439
- _loadLedger(options.consumer_root, session_id, Date.now())
26440
- );
27044
+ const command = extractCommand(envelope);
27045
+ const ledger = _loadLedger(options.consumer_root, session_id, Date.now());
27046
+ let impact;
27047
+ const stage1 = command === null ? null : commandOp(command);
27048
+ const stage2Target = command === null ? null : mergeTargetOf(command);
27049
+ if (stage1 === "pr-merge" && stage2Target !== null && !ledger.authorized.has("pr-merge") && !grantCovers(ledger.grants ?? [], "pr-merge", stage2Target)) {
27050
+ try {
27051
+ impact = describeImpact(
27052
+ stage2Target,
27053
+ analyseMergeImpact(stage2Target, { cwd: options.consumer_root })
27054
+ );
27055
+ } catch {
27056
+ }
27057
+ }
27058
+ const decision = decide(command, ledger, impact);
27059
+ if (decision.consumedTarget !== void 0) {
27060
+ consumeGrantTarget(options.consumer_root, session_id, decision.consumedTarget);
27061
+ }
26441
27062
  if (decision.blockedOp) {
26442
27063
  try {
26443
27064
  atomic_write_json(path56.join(options.consumer_root, pendingFileFor(session_id)), {
@@ -28244,14 +28865,17 @@ var SLOP_RULES = [
28244
28865
  catalogId: "V1",
28245
28866
  severity: "P1",
28246
28867
  engines: ["css"],
28247
- description: "Colored side-stripe border (border-left/right > 1px) \u2014 the #1 AI-UI signature",
28248
- message: "Side-stripe accent border reads as scaffold-default. Remove it, or declare an intentional stripe in DESIGN.md (V1).",
28249
- gated: (ctx) => ctx.has("side stripe", "side-stripe", "stripe accent", "accent border"),
28250
- detect: ({ content }) => cssBlocks(content).filter((b) => !/blockquote|\bquote\b|<q>/i.test(b.selector)).filter(
28251
- (b) => /border-(?:left|right)\s*:\s*(?:[2-9]|\d{2,})px\s+\w+\s+(?:#|rgb|hsl|oklch|var\(|[a-z])/i.test(
28252
- b.body
28253
- )
28254
- ).map((b) => ({ line: b.line, snippet: b.selector.slice(0, 80) }))
28868
+ description: "Colored accent border > 1px \u2014 a side stripe, a four-sided colored border, or a gradient border-image (V1)",
28869
+ message: "Colored accent border reads as scaffold-default. Remove it, or declare an intentional accent border in DESIGN.md (V1).",
28870
+ gated: (ctx) => ctx.has("side stripe", "side-stripe", "stripe accent", "accent border", "card border"),
28871
+ detect: ({ content }) => cssBlocks(content).filter((b) => !/blockquote|\bquote\b|<q>/i.test(b.selector)).filter((b) => {
28872
+ const COLOR = "(?!transparent|currentcolor|inherit|initial|unset)(?:#|rgb|hsl|oklch|var\\(|[a-z])";
28873
+ const WIDTH = "(?:[2-9]|\\d{2,})px";
28874
+ const stripe = new RegExp(`border-(?:left|right)\\s*:\\s*${WIDTH}\\s+\\w+\\s+${COLOR}`, "i");
28875
+ const foursided = new RegExp(`border\\s*:\\s*${WIDTH}\\s+\\w+\\s+${COLOR}`, "i");
28876
+ const gradient = /border-image\s*:[^;]*(?:linear|conic|radial)-gradient/i;
28877
+ return stripe.test(b.body) || foursided.test(b.body) || gradient.test(b.body);
28878
+ }).map((b) => ({ line: b.line, snippet: b.selector.slice(0, 80) }))
28255
28879
  },
28256
28880
  {
28257
28881
  id: "slop-v3-ghost-card",
@@ -29177,7 +29801,7 @@ import path67 from "node:path";
29177
29801
  import { fileURLToPath as fileURLToPath43, pathToFileURL as pathToFileURL36 } from "node:url";
29178
29802
 
29179
29803
  // src/scripts/code_graph/detect.ts
29180
- import { execFileSync as execFileSync3 } from "node:child_process";
29804
+ import { execFileSync as execFileSync4 } from "node:child_process";
29181
29805
  import * as fs66 from "node:fs";
29182
29806
  import * as path66 from "node:path";
29183
29807
 
@@ -29228,7 +29852,7 @@ function validateGraph(g) {
29228
29852
  var CONSUMER_CANDIDATES = ["graph.json", "code-graph.json", ".code-graph/graph.json"];
29229
29853
  function gitLastCommitEpoch(root) {
29230
29854
  try {
29231
- const out = execFileSync3("git", ["-C", root, "log", "-1", "--format=%ct"], {
29855
+ const out = execFileSync4("git", ["-C", root, "log", "-1", "--format=%ct"], {
29232
29856
  env: hardenedSpawnEnv(),
29233
29857
  encoding: "utf-8",
29234
29858
  stdio: ["ignore", "pipe", "ignore"]
@@ -29241,7 +29865,7 @@ function gitLastCommitEpoch(root) {
29241
29865
  }
29242
29866
  function commitsBehind(root, sha) {
29243
29867
  try {
29244
- const out = execFileSync3("git", ["-C", root, "rev-list", "--count", `${sha}..HEAD`], {
29868
+ const out = execFileSync4("git", ["-C", root, "rev-list", "--count", `${sha}..HEAD`], {
29245
29869
  env: hardenedSpawnEnv(),
29246
29870
  encoding: "utf-8",
29247
29871
  stdio: ["ignore", "pipe", "ignore"]