@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.
- package/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +88 -0
- package/dist/agent-src/rules/security-sensitive-stop.md +7 -5
- package/dist/agent-src/skills/accessibility-auditor/SKILL.md +20 -2
- package/dist/agent-src/skills/design-intelligence/data/ux-guidelines.csv +99 -99
- package/dist/agent-src/skills/fe-design/SKILL.md +6 -3
- package/dist/agent-src/skills/fe-design/references/design-patterns.md +28 -0
- package/dist/agent-src/skills/iconography/SKILL.md +37 -6
- package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +1 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli-delegate/cmd_conformance.js +655 -34
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +7 -7
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/dispatch.js +669 -45
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +661 -35
- package/dist/router.json +1 -1
- package/docs/guidelines/design-antipatterns.md +10 -2
- package/package.json +1 -1
- package/src/config/gate-violation-baselines.json +2 -2
- package/src/config/rule-enforcement-baseline.json +0 -1
- package/src/scripts/_lib/release_highlights.ts +63 -7
- package/src/scripts/design_slop_rules.ts +19 -9
- package/src/scripts/git_authorization_hook.ts +719 -27
- package/src/scripts/hooks/block_unauthorized_git.ts +250 -9
- package/src/scripts/hooks/merge_impact.ts +275 -0
- package/src/scripts/release.ts +9 -34
- package/src/scripts/release_drill.ts +180 -8
- package/src/scripts/release_env.ts +31 -0
- package/src/scripts/release_publication.ts +158 -1
package/dist/hooks/dispatch.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
25873
|
-
//
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
25962
|
-
const
|
|
25963
|
-
|
|
25964
|
-
|
|
25965
|
-
|
|
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:
|
|
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
|
|
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
|
|
26438
|
-
|
|
26439
|
-
|
|
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
|
|
28248
|
-
message: "
|
|
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
|
-
|
|
28252
|
-
|
|
28253
|
-
)
|
|
28254
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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"]
|