@event4u/agent-config 14.14.1 → 14.15.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 +30 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli-delegate/cmd_conformance.js +512 -16
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +1 -1
- 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 +1 -1
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +1 -1
- package/dist/hooks/dispatch.js +515 -19
- package/dist/mcp/registry-manifest.json +1 -1
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +518 -17
- 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/git_authorization_hook.ts +476 -6
- package/src/scripts/hooks/block_unauthorized_git.ts +238 -9
- package/src/scripts/hooks/merge_impact.ts +207 -0
- package/src/scripts/release.ts +5 -17
- package/src/scripts/release_drill.ts +26 -7
- package/src/scripts/release_env.ts +31 -0
- package/src/scripts/release_publication.ts +67 -1
package/dist/mcp/server.mjs
CHANGED
|
@@ -37289,6 +37289,65 @@ function splitFences(prompt) {
|
|
|
37289
37289
|
function _commandLines(fence) {
|
|
37290
37290
|
return fence.split("\n").map((l) => l.trim()).filter((l) => l.length > 0).filter((l) => !/^(remote:|error:|hint:|fatal:|warning:|To |!\s|\s*\^)/i.test(l)).map((l) => l.replace(/^[$>#]\s*/, "")).filter((l) => /^(git|gh|npm|pnpm|yarn|task)\b/.test(l));
|
|
37291
37291
|
}
|
|
37292
|
+
function extractMergeTargets(prompt) {
|
|
37293
|
+
const out = /* @__PURE__ */ new Set();
|
|
37294
|
+
const re = /(?:#|\bpr[- ]?|\bpull[- ]request\s+#?)(\d{1,7})\b/giu;
|
|
37295
|
+
let m;
|
|
37296
|
+
while ((m = re.exec(prompt)) !== null) {
|
|
37297
|
+
const n = Number.parseInt(m[1], 10);
|
|
37298
|
+
if (Number.isSafeInteger(n) && n > 0) {
|
|
37299
|
+
out.add(n);
|
|
37300
|
+
}
|
|
37301
|
+
}
|
|
37302
|
+
return [...out].sort((a, b) => a - b);
|
|
37303
|
+
}
|
|
37304
|
+
function isRevocation(prompt) {
|
|
37305
|
+
return REVOKE_RE.test(prompt);
|
|
37306
|
+
}
|
|
37307
|
+
function _grantId(session_id, at, targets) {
|
|
37308
|
+
return `${sessionSlug(session_id) || "nosession"}:${at}:${targets.join(",")}`;
|
|
37309
|
+
}
|
|
37310
|
+
function foldGrants(prior, prompt, authorized, session_id, now) {
|
|
37311
|
+
if (isRevocation(prompt)) {
|
|
37312
|
+
return [];
|
|
37313
|
+
}
|
|
37314
|
+
const carried = prior.filter((g) => g.consumed.length < g.targets.length);
|
|
37315
|
+
if (!authorized.includes("pr-merge")) {
|
|
37316
|
+
return carried;
|
|
37317
|
+
}
|
|
37318
|
+
const targets = extractMergeTargets(prompt);
|
|
37319
|
+
if (targets.length === 0) {
|
|
37320
|
+
return carried;
|
|
37321
|
+
}
|
|
37322
|
+
const at = now.toISOString();
|
|
37323
|
+
const already = new Set(carried.flatMap((g) => g.targets));
|
|
37324
|
+
const fresh = targets.filter((t) => !already.has(t));
|
|
37325
|
+
if (fresh.length === 0) {
|
|
37326
|
+
return carried;
|
|
37327
|
+
}
|
|
37328
|
+
return [
|
|
37329
|
+
...carried,
|
|
37330
|
+
{
|
|
37331
|
+
id: _grantId(session_id, at, fresh),
|
|
37332
|
+
op: "pr-merge",
|
|
37333
|
+
targets: fresh,
|
|
37334
|
+
consumed: [],
|
|
37335
|
+
granted_at: at,
|
|
37336
|
+
evidence: prompt.trim().slice(0, 120)
|
|
37337
|
+
}
|
|
37338
|
+
];
|
|
37339
|
+
}
|
|
37340
|
+
function negatedBefore(text, index, matched = "") {
|
|
37341
|
+
let start = 0;
|
|
37342
|
+
for (const mark of [".", "!", "?", "\n"]) {
|
|
37343
|
+
const at = text.lastIndexOf(mark, index - 1);
|
|
37344
|
+
if (at + 1 > start) {
|
|
37345
|
+
start = at + 1;
|
|
37346
|
+
}
|
|
37347
|
+
}
|
|
37348
|
+
const before = text.slice(Math.max(start, index - 30), index);
|
|
37349
|
+
return NEGATION_WORD.test(before) || NEGATION_WORD.test(matched);
|
|
37350
|
+
}
|
|
37292
37351
|
function classifyAuthorization(prompt) {
|
|
37293
37352
|
const { prose, fences } = splitFences(prompt);
|
|
37294
37353
|
const authorized = /* @__PURE__ */ new Set();
|
|
@@ -37296,7 +37355,7 @@ function classifyAuthorization(prompt) {
|
|
|
37296
37355
|
const instruction = prose.split("\n").filter((l) => !OUTPUT_LINE.test(l)).join("\n");
|
|
37297
37356
|
for (const { op, re } of isInterrogative(instruction) ? [] : PHRASES) {
|
|
37298
37357
|
const m = re.exec(instruction);
|
|
37299
|
-
if (m) {
|
|
37358
|
+
if (m && !negatedBefore(instruction, m.index, m[0])) {
|
|
37300
37359
|
authorized.add(op);
|
|
37301
37360
|
evidence[op] = `prose: "${m[0]}"`;
|
|
37302
37361
|
}
|
|
@@ -37318,6 +37377,68 @@ function classifyAuthorization(prompt) {
|
|
|
37318
37377
|
}
|
|
37319
37378
|
return { authorized: [...authorized], evidence };
|
|
37320
37379
|
}
|
|
37380
|
+
function readGrants(consumer_root, session_id) {
|
|
37381
|
+
try {
|
|
37382
|
+
const raw = fs65.readFileSync(path66.join(consumer_root, ledgerFileFor(session_id)), "utf8");
|
|
37383
|
+
const decoded = JSON.parse(raw);
|
|
37384
|
+
if (!_isObject4(decoded) || !Array.isArray(decoded["grants"])) {
|
|
37385
|
+
return [];
|
|
37386
|
+
}
|
|
37387
|
+
const owner = typeof decoded["session_id"] === "string" ? decoded["session_id"] : "";
|
|
37388
|
+
if (session_id && owner && owner !== session_id) {
|
|
37389
|
+
return [];
|
|
37390
|
+
}
|
|
37391
|
+
return decoded["grants"].flatMap((g) => {
|
|
37392
|
+
if (!_isObject4(g)) return [];
|
|
37393
|
+
const op = g["op"];
|
|
37394
|
+
if (typeof op !== "string" || !ALL_OPS.includes(op)) return [];
|
|
37395
|
+
const targets = Array.isArray(g["targets"]) ? g["targets"] : [];
|
|
37396
|
+
const consumed = Array.isArray(g["consumed"]) ? g["consumed"] : [];
|
|
37397
|
+
return [
|
|
37398
|
+
{
|
|
37399
|
+
id: String(g["id"] ?? ""),
|
|
37400
|
+
op,
|
|
37401
|
+
targets: targets.filter((t) => typeof t === "number"),
|
|
37402
|
+
consumed: consumed.filter((t) => typeof t === "number"),
|
|
37403
|
+
granted_at: String(g["granted_at"] ?? ""),
|
|
37404
|
+
evidence: String(g["evidence"] ?? "")
|
|
37405
|
+
}
|
|
37406
|
+
];
|
|
37407
|
+
});
|
|
37408
|
+
} catch {
|
|
37409
|
+
return [];
|
|
37410
|
+
}
|
|
37411
|
+
}
|
|
37412
|
+
function consumeGrantTarget(consumer_root, session_id, target) {
|
|
37413
|
+
const file = path66.join(consumer_root, ledgerFileFor(session_id));
|
|
37414
|
+
let decoded;
|
|
37415
|
+
try {
|
|
37416
|
+
decoded = JSON.parse(fs65.readFileSync(file, "utf8"));
|
|
37417
|
+
} catch {
|
|
37418
|
+
return;
|
|
37419
|
+
}
|
|
37420
|
+
if (!_isObject4(decoded) || !Array.isArray(decoded["grants"])) {
|
|
37421
|
+
return;
|
|
37422
|
+
}
|
|
37423
|
+
let touched = false;
|
|
37424
|
+
for (const g of decoded["grants"]) {
|
|
37425
|
+
if (!_isObject4(g) || !Array.isArray(g["targets"]) || !Array.isArray(g["consumed"])) {
|
|
37426
|
+
continue;
|
|
37427
|
+
}
|
|
37428
|
+
const targets = g["targets"];
|
|
37429
|
+
const consumed = g["consumed"];
|
|
37430
|
+
if (targets.includes(target) && !consumed.includes(target)) {
|
|
37431
|
+
consumed.push(target);
|
|
37432
|
+
touched = true;
|
|
37433
|
+
}
|
|
37434
|
+
}
|
|
37435
|
+
if (touched) {
|
|
37436
|
+
try {
|
|
37437
|
+
atomic_write_json(file, decoded);
|
|
37438
|
+
} catch {
|
|
37439
|
+
}
|
|
37440
|
+
}
|
|
37441
|
+
}
|
|
37321
37442
|
function takePending(consumer_root, session_id, now) {
|
|
37322
37443
|
const file = path66.join(consumer_root, pendingFileFor(session_id));
|
|
37323
37444
|
let raw;
|
|
@@ -37386,12 +37507,24 @@ function run2(stdin_text, options) {
|
|
|
37386
37507
|
authorized.push(pending.op);
|
|
37387
37508
|
evidence[pending.op] = `confirmation of the refused \`${pending.op}\` (refused ${pending.refused_at}): "${prompt.trim().slice(0, 40)}"`;
|
|
37388
37509
|
}
|
|
37510
|
+
const now = /* @__PURE__ */ new Date();
|
|
37389
37511
|
const ledger = {
|
|
37390
37512
|
session_id: typeof envelope["session_id"] === "string" ? envelope["session_id"] : "",
|
|
37391
|
-
detected_at:
|
|
37513
|
+
detected_at: now.toISOString(),
|
|
37392
37514
|
authorized,
|
|
37393
37515
|
evidence,
|
|
37394
|
-
prompt_chars: prompt.length
|
|
37516
|
+
prompt_chars: prompt.length,
|
|
37517
|
+
// Grants survive this write; `authorized` does not. That asymmetry IS the
|
|
37518
|
+
// decision. A bare operation name stays one-shot exactly as `commit-policy`
|
|
37519
|
+
// requires, and only an authorization that also froze its objects earns a
|
|
37520
|
+
// life longer than the turn. See ADR-252.
|
|
37521
|
+
grants: foldGrants(
|
|
37522
|
+
readGrants(options.consumer_root, session),
|
|
37523
|
+
prompt,
|
|
37524
|
+
authorized,
|
|
37525
|
+
session,
|
|
37526
|
+
now
|
|
37527
|
+
)
|
|
37395
37528
|
};
|
|
37396
37529
|
try {
|
|
37397
37530
|
atomic_write_json(path66.join(options.consumer_root, ledgerFileFor(session)), ledger);
|
|
@@ -37432,7 +37565,7 @@ function _isCliEntry19() {
|
|
|
37432
37565
|
return false;
|
|
37433
37566
|
}
|
|
37434
37567
|
}
|
|
37435
|
-
var EXIT_ALLOW2, STATE_FILE2, STATE_DIR2, PENDING_DIR, PENDING_FILE, PENDING_MAX_AGE_MS, ALL_OPS, PHRASES, OUTPUT_LINE, PASTED_COMMANDS;
|
|
37568
|
+
var EXIT_ALLOW2, STATE_FILE2, STATE_DIR2, PENDING_DIR, PENDING_FILE, PENDING_MAX_AGE_MS, ALL_OPS, PHRASES, OUTPUT_LINE, PASTED_COMMANDS, REVOKE_RE, NEGATION_WORD;
|
|
37436
37569
|
var init_git_authorization_hook = __esm({
|
|
37437
37570
|
"src/scripts/git_authorization_hook.ts"() {
|
|
37438
37571
|
"use strict";
|
|
@@ -37455,12 +37588,39 @@ var init_git_authorization_hook = __esm({
|
|
|
37455
37588
|
"pr-merge-auto",
|
|
37456
37589
|
"tag",
|
|
37457
37590
|
"release",
|
|
37458
|
-
"publish"
|
|
37591
|
+
"publish",
|
|
37592
|
+
"unpublish",
|
|
37593
|
+
"deprecate",
|
|
37594
|
+
"release-asset",
|
|
37595
|
+
"protection",
|
|
37596
|
+
"workflow-toggle",
|
|
37597
|
+
"repo-lifecycle",
|
|
37598
|
+
"review-approve",
|
|
37599
|
+
"force-push",
|
|
37600
|
+
"worktree-remove",
|
|
37601
|
+
"clean-ignored",
|
|
37602
|
+
"tag-force",
|
|
37603
|
+
"rebase",
|
|
37604
|
+
"reset-hard",
|
|
37605
|
+
"clean",
|
|
37606
|
+
"stash-drop",
|
|
37607
|
+
"branch-delete",
|
|
37608
|
+
"close"
|
|
37459
37609
|
];
|
|
37460
37610
|
PHRASES = [
|
|
37461
37611
|
{ op: "commit", re: /\b(commit(e|et|te|ten)?|committe|einchecken)\b/i },
|
|
37462
37612
|
{ op: "push", re: /\b(push(e|en|t)?|hochladen|hochschieben|raufschieben)\b/i },
|
|
37463
|
-
|
|
37613
|
+
// A CREATION VERB IS REQUIRED, as for `pr-create` above and for the same
|
|
37614
|
+
// reason. The bare noun could not tell an authorization from a mention, and
|
|
37615
|
+
// the mention is the dangerous half: "den branch nicht löschen" names the
|
|
37616
|
+
// object precisely in order to forbid acting on it, yet authorized branch
|
|
37617
|
+
// creation — the negation trails a bare-noun match, so neither the
|
|
37618
|
+
// look-behind nor the span check in `negatedBefore` can reach it. Widened
|
|
37619
|
+
// 2026-09-03 alongside the destructive-coverage work that measured it.
|
|
37620
|
+
{
|
|
37621
|
+
op: "branch",
|
|
37622
|
+
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
|
|
37623
|
+
},
|
|
37464
37624
|
// A creation verb is required — "schau dir den PR an" is not "open a PR".
|
|
37465
37625
|
{
|
|
37466
37626
|
op: "pr-create",
|
|
@@ -37497,7 +37657,111 @@ var init_git_authorization_hook = __esm({
|
|
|
37497
37657
|
op: "release",
|
|
37498
37658
|
re: /\b(releasen?|ver(ö|oe)ffentlich(e|en))\b(?!\s*[- ]?(notes?|branch|candidate|pr\b|datum|date|zweig))/i
|
|
37499
37659
|
},
|
|
37500
|
-
{ op: "publish", re: /\b(publish(e|en)?|publiziere[n]?)\b/i }
|
|
37660
|
+
{ op: "publish", re: /\b(publish(e|en)?|publiziere[n]?)\b/i },
|
|
37661
|
+
// ── Authorization vocabulary for the ops added 2026-09-03.
|
|
37662
|
+
//
|
|
37663
|
+
// NON-NEGOTIABLE, and the reason it ships in the same change as the new BLOCK
|
|
37664
|
+
// ops: a blocked operation with no phrase that authorizes it is a dead end.
|
|
37665
|
+
// The guard refuses, the user says "doch, mach das", the classifier records
|
|
37666
|
+
// nothing, and the refusal repeats forever. Every op the guard can block must
|
|
37667
|
+
// have a sentence that unblocks it, or the user's override is a claim rather
|
|
37668
|
+
// than a mechanism.
|
|
37669
|
+
//
|
|
37670
|
+
// Tight on purpose. Under-matching costs one confirmation; over-matching
|
|
37671
|
+
// authorizes something the user did not ask for, so each pattern demands the
|
|
37672
|
+
// object noun rather than a bare verb wherever the verb is ordinary German.
|
|
37673
|
+
//
|
|
37674
|
+
// `unpublish` before nothing in particular — `\bpublish\b` does not match
|
|
37675
|
+
// inside "unpublish" (no word boundary between `n` and `p`), so the two never
|
|
37676
|
+
// collide.
|
|
37677
|
+
// TWO SHAPES EVERY GERMAN PATTERN BELOW MUST CARRY, both learned from tests
|
|
37678
|
+
// that went red rather than from inspection:
|
|
37679
|
+
//
|
|
37680
|
+
// 1. THE IMPERATIVE DROPS ITS `-e`. "entfern die worktree", "archivier das
|
|
37681
|
+
// repo", "deaktivier den workflow" are what people type; `entfern(e|en)`
|
|
37682
|
+
// matches none of them. Every verb ending is therefore `(e|en)?`.
|
|
37683
|
+
// 2. WORD ORDER GOES BOTH WAYS. "archivier das repo" and "das repo
|
|
37684
|
+
// archivieren" are one instruction, and a pattern anchored on the noun
|
|
37685
|
+
// first sees only the second.
|
|
37686
|
+
//
|
|
37687
|
+
// The object noun stays REQUIRED in every pattern that has one. That is what
|
|
37688
|
+
// keeps the loosened verb endings from over-authorizing: "lösch" alone
|
|
37689
|
+
// authorizes nothing, "lösch den branch" authorizes a branch deletion.
|
|
37690
|
+
{ op: "unpublish", re: /\bunpublish(e|en)?\b/i },
|
|
37691
|
+
{
|
|
37692
|
+
op: "deprecate",
|
|
37693
|
+
re: /\bdeprecate(d|n)?\b|\bals\s+veraltet\s+markier(e|en)?\b/i
|
|
37694
|
+
},
|
|
37695
|
+
{
|
|
37696
|
+
op: "release-asset",
|
|
37697
|
+
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
|
|
37698
|
+
},
|
|
37699
|
+
// A VERB IS REQUIRED, like `tag` and `release` above and for the same reason.
|
|
37700
|
+
// A bare noun cannot tell an authorization from a mention, and here the
|
|
37701
|
+
// mention is the dangerous half: "die branch-protection nicht anfassen" names
|
|
37702
|
+
// the object precisely in order to forbid touching it. Measured — this was the
|
|
37703
|
+
// one row of the negation corpus that neither the look-behind nor the
|
|
37704
|
+
// span check could reach, because the negation trails a bare-noun match.
|
|
37705
|
+
{
|
|
37706
|
+
op: "protection",
|
|
37707
|
+
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
|
|
37708
|
+
},
|
|
37709
|
+
{
|
|
37710
|
+
op: "workflow-toggle",
|
|
37711
|
+
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
|
|
37712
|
+
},
|
|
37713
|
+
{
|
|
37714
|
+
op: "repo-lifecycle",
|
|
37715
|
+
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
|
|
37716
|
+
},
|
|
37717
|
+
{
|
|
37718
|
+
op: "review-approve",
|
|
37719
|
+
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
|
|
37720
|
+
},
|
|
37721
|
+
{
|
|
37722
|
+
op: "force-push",
|
|
37723
|
+
re: /\bforce[-\s]?push(e|en|st)?\b|--force(-with-lease)?\b|\bmit\s+gewalt\s+push(e|en)?\b/i
|
|
37724
|
+
},
|
|
37725
|
+
{
|
|
37726
|
+
op: "worktree-remove",
|
|
37727
|
+
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
|
|
37728
|
+
},
|
|
37729
|
+
// The `-x` flag is the whole distinction: without it `git clean` spares
|
|
37730
|
+
// ignored files, with it `.env` and local certificates go too.
|
|
37731
|
+
{
|
|
37732
|
+
op: "clean-ignored",
|
|
37733
|
+
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
|
|
37734
|
+
},
|
|
37735
|
+
{
|
|
37736
|
+
op: "tag-force",
|
|
37737
|
+
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
|
|
37738
|
+
},
|
|
37739
|
+
{ op: "rebase", re: /\brebase(n|st|d)?\b/i },
|
|
37740
|
+
{
|
|
37741
|
+
op: "reset-hard",
|
|
37742
|
+
re: /\breset\s+--hard\b|\bhard\s+reset\b|\bhart\s+zur(ü|ue)cksetz(e|en)?\b/i
|
|
37743
|
+
},
|
|
37744
|
+
{ op: "clean", re: /\bgit\s+clean\b/i },
|
|
37745
|
+
{
|
|
37746
|
+
op: "stash-drop",
|
|
37747
|
+
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
|
|
37748
|
+
},
|
|
37749
|
+
{
|
|
37750
|
+
op: "branch-delete",
|
|
37751
|
+
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
|
|
37752
|
+
},
|
|
37753
|
+
{
|
|
37754
|
+
op: "close",
|
|
37755
|
+
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
|
|
37756
|
+
},
|
|
37757
|
+
// Enabling auto-merge is its own op on the guard side, so it needs its own
|
|
37758
|
+
// sentence. Without one it is the dead end this block exists to prevent: the
|
|
37759
|
+
// plain merge phrase authorizes `pr-merge` and never `pr-merge-auto`, so the
|
|
37760
|
+
// refusal would have repeated forever.
|
|
37761
|
+
{
|
|
37762
|
+
op: "pr-merge-auto",
|
|
37763
|
+
re: /\bauto[-\s]?merge\b|\bmerge\b[^.\n]{0,20}\bautomatisch\b|\bautomatisch\b[^.\n]{0,20}\bmerg(e|en)\b/i
|
|
37764
|
+
}
|
|
37501
37765
|
];
|
|
37502
37766
|
OUTPUT_LINE = /^\s*(To\s+\S+|remote:|error:|fatal:|hint:|warning:|!\s|\?\?\s|\s*\^|[-+]{3}\s|@@\s|\$\s|>\s|\d+\s+(pass|fail)|npm ERR!|Error:|Traceback|at\s+\S+:\d+)/i;
|
|
37503
37767
|
PASTED_COMMANDS = [
|
|
@@ -37520,12 +37784,107 @@ var init_git_authorization_hook = __esm({
|
|
|
37520
37784
|
{ op: "commit", re: /\bgit\s+commit\b/i },
|
|
37521
37785
|
{ op: "branch", re: /\bgit\s+(checkout\s+-b|switch\s+-c|branch)\b/i }
|
|
37522
37786
|
];
|
|
37787
|
+
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;
|
|
37788
|
+
NEGATION_WORD = /\b(nicht|nichts|kein(e|en|em|er|es)?|niemals|nie|no|not|dont|don't|never|without|ohne)\b/i;
|
|
37523
37789
|
if (_isCliEntry19()) {
|
|
37524
37790
|
process.exit(main23());
|
|
37525
37791
|
}
|
|
37526
37792
|
}
|
|
37527
37793
|
});
|
|
37528
37794
|
|
|
37795
|
+
// src/scripts/hooks/merge_impact.ts
|
|
37796
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
37797
|
+
function classifyDiff(patch) {
|
|
37798
|
+
if (patch.trim() === "") {
|
|
37799
|
+
return { verdict: "undecidable", markers: [], reason: "empty diff" };
|
|
37800
|
+
}
|
|
37801
|
+
const markers = [];
|
|
37802
|
+
for (const { label, re } of DESTRUCTIVE_MARKERS) {
|
|
37803
|
+
if (re.test(patch)) {
|
|
37804
|
+
markers.push(label);
|
|
37805
|
+
}
|
|
37806
|
+
}
|
|
37807
|
+
const bump = MAJOR_BUMP.exec(patch);
|
|
37808
|
+
if (bump && bump[1] !== bump[2]) {
|
|
37809
|
+
markers.push(`major version bump ${bump[1]}.x to ${bump[2]}.x`);
|
|
37810
|
+
}
|
|
37811
|
+
const filesChanged = (patch.match(/^diff --git /gm) ?? []).length;
|
|
37812
|
+
if (markers.length > 0) {
|
|
37813
|
+
return { verdict: "destructive", markers, filesChanged };
|
|
37814
|
+
}
|
|
37815
|
+
return { verdict: "additive", markers: [], filesChanged };
|
|
37816
|
+
}
|
|
37817
|
+
function fetchPatch(pr, opts = {}) {
|
|
37818
|
+
const run6 = opts.exec ?? execFileSync2;
|
|
37819
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
37820
|
+
const capture = (file, args) => run6(file, args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 15e3 });
|
|
37821
|
+
for (const ref of [`refs/pull/${pr}/head`, `pull/${pr}/head`]) {
|
|
37822
|
+
try {
|
|
37823
|
+
const base = capture("git", ["merge-base", "HEAD", ref]).trim();
|
|
37824
|
+
if (base) {
|
|
37825
|
+
const patch = capture("git", ["diff", `${base}...${ref}`]);
|
|
37826
|
+
if (patch.trim() !== "") {
|
|
37827
|
+
return { patch, source: "local" };
|
|
37828
|
+
}
|
|
37829
|
+
}
|
|
37830
|
+
} catch {
|
|
37831
|
+
}
|
|
37832
|
+
}
|
|
37833
|
+
if (opts.allowNetwork === false) {
|
|
37834
|
+
return { patch: "", source: "none", reason: "no local ref and network reads disabled" };
|
|
37835
|
+
}
|
|
37836
|
+
try {
|
|
37837
|
+
const patch = capture("gh", ["pr", "diff", String(pr), "--patch"]);
|
|
37838
|
+
if (patch.trim() !== "") {
|
|
37839
|
+
return { patch, source: "gh" };
|
|
37840
|
+
}
|
|
37841
|
+
return { patch: "", source: "none", reason: "gh returned an empty diff" };
|
|
37842
|
+
} catch {
|
|
37843
|
+
return { patch: "", source: "none", reason: "no local ref, and gh pr diff failed" };
|
|
37844
|
+
}
|
|
37845
|
+
}
|
|
37846
|
+
function analyseMergeImpact(pr, opts = {}) {
|
|
37847
|
+
const got = fetchPatch(pr, opts);
|
|
37848
|
+
if (got.source === "none") {
|
|
37849
|
+
return { verdict: "undecidable", markers: [], ...got.reason ? { reason: got.reason } : {} };
|
|
37850
|
+
}
|
|
37851
|
+
return classifyDiff(got.patch);
|
|
37852
|
+
}
|
|
37853
|
+
function describeImpact(pr, impact) {
|
|
37854
|
+
const say = `Say "merge #${pr}" to authorize it.`;
|
|
37855
|
+
switch (impact.verdict) {
|
|
37856
|
+
case "additive":
|
|
37857
|
+
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}`;
|
|
37858
|
+
case "destructive":
|
|
37859
|
+
return `Impact scan: PR #${pr} is NOT additive \u2014 ${impact.markers.join("; ")}. Read the diff before authorizing. ${say}`;
|
|
37860
|
+
default:
|
|
37861
|
+
return `Impact scan: could not read the diff for PR #${pr} (${impact.reason ?? "unknown"}), so nothing is known about what it changes. ${say}`;
|
|
37862
|
+
}
|
|
37863
|
+
}
|
|
37864
|
+
var DESTRUCTIVE_MARKERS, MAJOR_BUMP;
|
|
37865
|
+
var init_merge_impact = __esm({
|
|
37866
|
+
"src/scripts/hooks/merge_impact.ts"() {
|
|
37867
|
+
"use strict";
|
|
37868
|
+
DESTRUCTIVE_MARKERS = [
|
|
37869
|
+
// Schema removal. Matched on ADDED lines only — a migration that *removes* a
|
|
37870
|
+
// dropColumn call is the opposite of destructive, and matching the raw patch
|
|
37871
|
+
// would score it the same way.
|
|
37872
|
+
{ label: "migration drops a column or table", re: /^\+.*\b(dropColumn|dropTable|dropIfExists|removeColumn|drop_column|drop_table)\b/im },
|
|
37873
|
+
{ label: "raw DROP or TRUNCATE in a migration", re: /^\+.*\b(DROP\s+(TABLE|COLUMN|INDEX|CONSTRAINT)|TRUNCATE\s+TABLE)\b/im },
|
|
37874
|
+
// The author said so themselves. Conventional Commits marks a breaking change
|
|
37875
|
+
// with a `!` before the colon or a body trailer.
|
|
37876
|
+
{ label: "author marked a BREAKING CHANGE", re: /^\+.*BREAKING[ -]CHANGE/m },
|
|
37877
|
+
// A removed export is the shape most likely to break an importer.
|
|
37878
|
+
{ 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 },
|
|
37879
|
+
{ label: "a public PHP or Python symbol was removed", re: /^-\s*(public\s+function|def\s+[a-z_]+\s*\()/m },
|
|
37880
|
+
// Route removal breaks callers that never imported anything.
|
|
37881
|
+
{ 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 },
|
|
37882
|
+
{ label: "a whole file was deleted", re: /^deleted file mode /m }
|
|
37883
|
+
];
|
|
37884
|
+
MAJOR_BUMP = /^-\s*"version"\s*:\s*"(\d+)\.[^"]*"[\s\S]{0,200}?^\+\s*"version"\s*:\s*"(\d+)\./m;
|
|
37885
|
+
}
|
|
37886
|
+
});
|
|
37887
|
+
|
|
37529
37888
|
// src/scripts/hooks/block_unauthorized_git.ts
|
|
37530
37889
|
import fs66 from "node:fs";
|
|
37531
37890
|
import path67 from "node:path";
|
|
@@ -37716,7 +38075,24 @@ function commandOp(command) {
|
|
|
37716
38075
|
}
|
|
37717
38076
|
return found;
|
|
37718
38077
|
}
|
|
38078
|
+
function mergeTargetOf(command) {
|
|
38079
|
+
const gh = /\bgh\s+pr\s+merge\s+(?:-{1,2}\S+\s+)*?(\d{1,7})\b/i.exec(command);
|
|
38080
|
+
if (gh) {
|
|
38081
|
+
return Number.parseInt(gh[1], 10);
|
|
38082
|
+
}
|
|
38083
|
+
const api = /\/pulls\/(\d{1,7})\/merge\b/i.exec(command);
|
|
38084
|
+
return api ? Number.parseInt(api[1], 10) : null;
|
|
38085
|
+
}
|
|
38086
|
+
function grantCovers(grants, op, target) {
|
|
38087
|
+
if (op !== "pr-merge" || target === null) {
|
|
38088
|
+
return null;
|
|
38089
|
+
}
|
|
38090
|
+
return grants.find(
|
|
38091
|
+
(g) => g.op === "pr-merge" && g.targets.includes(target) && !g.consumed.includes(target)
|
|
38092
|
+
) ?? null;
|
|
38093
|
+
}
|
|
37719
38094
|
function _loadLedger(consumer_root, session_id, now) {
|
|
38095
|
+
const grants = readGrants(consumer_root, session_id);
|
|
37720
38096
|
const candidates = [ledgerFileFor(session_id)];
|
|
37721
38097
|
if (!candidates.includes(STATE_FILE2)) {
|
|
37722
38098
|
candidates.push(STATE_FILE2);
|
|
@@ -37724,10 +38100,10 @@ function _loadLedger(consumer_root, session_id, now) {
|
|
|
37724
38100
|
for (const rel of candidates) {
|
|
37725
38101
|
const found = _readLedgerFile(path67.join(consumer_root, rel), session_id, now);
|
|
37726
38102
|
if (found !== null) {
|
|
37727
|
-
return found;
|
|
38103
|
+
return { ...found, grants };
|
|
37728
38104
|
}
|
|
37729
38105
|
}
|
|
37730
|
-
return { authorized: /* @__PURE__ */ new Set(), present: false };
|
|
38106
|
+
return { authorized: /* @__PURE__ */ new Set(), present: false, grants };
|
|
37731
38107
|
}
|
|
37732
38108
|
function _readLedgerFile(file, session_id, now) {
|
|
37733
38109
|
try {
|
|
@@ -37751,7 +38127,7 @@ function _readLedgerFile(file, session_id, now) {
|
|
|
37751
38127
|
}
|
|
37752
38128
|
return null;
|
|
37753
38129
|
}
|
|
37754
|
-
function decide(command, ledger) {
|
|
38130
|
+
function decide(command, ledger, impact) {
|
|
37755
38131
|
if (command === null) {
|
|
37756
38132
|
return { exit: EXIT_ALLOW3, stdout: "", stderr: "" };
|
|
37757
38133
|
}
|
|
@@ -37759,9 +38135,14 @@ function decide(command, ledger) {
|
|
|
37759
38135
|
if (op === null || ledger.authorized.has(op)) {
|
|
37760
38136
|
return { exit: EXIT_ALLOW3, stdout: "", stderr: "" };
|
|
37761
38137
|
}
|
|
38138
|
+
const target = mergeTargetOf(command);
|
|
38139
|
+
if (grantCovers(ledger.grants ?? [], op, target) && target !== null) {
|
|
38140
|
+
return { exit: EXIT_ALLOW3, stdout: "", stderr: "", consumedTarget: target };
|
|
38141
|
+
}
|
|
37762
38142
|
const shown = command.length > 120 ? `${command.slice(0, 117)}\u2026` : command;
|
|
37763
38143
|
if (BLOCK_OPS.has(op)) {
|
|
37764
|
-
const
|
|
38144
|
+
const scan2 = op === "pr-merge" && target !== null && impact ? ` ${impact}` : "";
|
|
38145
|
+
const reason = `Blocked: \`${op}\` with no authorization in this turn's prompt. This operation is irreversible and non-destructive-by-default already declares it never-autonomous. Command: ${shown}.${scan2} Ask the user, in one numbered-options block, and run it only after they answer this turn.`;
|
|
37765
38146
|
return { exit: EXIT_BLOCK, stdout: "", stderr: `${reason}
|
|
37766
38147
|
`, blockedOp: op };
|
|
37767
38148
|
}
|
|
@@ -37789,10 +38170,24 @@ function run3(stdin_text, options) {
|
|
|
37789
38170
|
}
|
|
37790
38171
|
}
|
|
37791
38172
|
const session_id = typeof envelope["session_id"] === "string" ? envelope["session_id"] : "";
|
|
37792
|
-
const
|
|
37793
|
-
|
|
37794
|
-
|
|
37795
|
-
);
|
|
38173
|
+
const command = extractCommand(envelope);
|
|
38174
|
+
const ledger = _loadLedger(options.consumer_root, session_id, Date.now());
|
|
38175
|
+
let impact;
|
|
38176
|
+
const stage1 = command === null ? null : commandOp(command);
|
|
38177
|
+
const stage2Target = command === null ? null : mergeTargetOf(command);
|
|
38178
|
+
if (stage1 === "pr-merge" && stage2Target !== null && !ledger.authorized.has("pr-merge") && !grantCovers(ledger.grants ?? [], "pr-merge", stage2Target)) {
|
|
38179
|
+
try {
|
|
38180
|
+
impact = describeImpact(
|
|
38181
|
+
stage2Target,
|
|
38182
|
+
analyseMergeImpact(stage2Target, { cwd: options.consumer_root })
|
|
38183
|
+
);
|
|
38184
|
+
} catch {
|
|
38185
|
+
}
|
|
38186
|
+
}
|
|
38187
|
+
const decision = decide(command, ledger, impact);
|
|
38188
|
+
if (decision.consumedTarget !== void 0) {
|
|
38189
|
+
consumeGrantTarget(options.consumer_root, session_id, decision.consumedTarget);
|
|
38190
|
+
}
|
|
37796
38191
|
if (decision.blockedOp) {
|
|
37797
38192
|
try {
|
|
37798
38193
|
atomic_write_json(path67.join(options.consumer_root, pendingFileFor(session_id)), {
|
|
@@ -37849,6 +38244,7 @@ var init_block_unauthorized_git = __esm({
|
|
|
37849
38244
|
"src/scripts/hooks/block_unauthorized_git.ts"() {
|
|
37850
38245
|
"use strict";
|
|
37851
38246
|
init_hook_stdin();
|
|
38247
|
+
init_merge_impact();
|
|
37852
38248
|
init_state_io();
|
|
37853
38249
|
init_git_authorization_hook();
|
|
37854
38250
|
EXIT_ALLOW3 = 0;
|
|
@@ -37860,13 +38256,52 @@ var init_block_unauthorized_git = __esm({
|
|
|
37860
38256
|
"pr-merge",
|
|
37861
38257
|
// Enabling auto-merge commits the outcome to a condition the agent does not
|
|
37862
38258
|
// control, so it is blocked on the same terms as the merge it schedules.
|
|
37863
|
-
"pr-merge-auto"
|
|
38259
|
+
"pr-merge-auto",
|
|
38260
|
+
// ── Added 2026-09-03. Every one is already never-autonomous in the
|
|
38261
|
+
// `non-destructive-by-default` prose; the measurement that drove it is in the
|
|
38262
|
+
// `GitOp` union's own note — 17 of 25 borderline operations classified as
|
|
38263
|
+
// NOTHING, including three worse than anything the guard already caught.
|
|
38264
|
+
/** The undo that is more destructive than the act: lockfiles break. */
|
|
38265
|
+
"unpublish",
|
|
38266
|
+
/** Metadata only, and every install on earth starts warning. */
|
|
38267
|
+
"deprecate",
|
|
38268
|
+
/** Replaces bytes consumers already hold a checksum for. */
|
|
38269
|
+
"release-asset",
|
|
38270
|
+
/** Deletes the guard rather than the data — everything after is unguarded. */
|
|
38271
|
+
"protection",
|
|
38272
|
+
/** No data lost; the gate that protects everything downstream is off. */
|
|
38273
|
+
"workflow-toggle",
|
|
38274
|
+
/** Reach outside the repository, and mostly one-way. */
|
|
38275
|
+
"repo-lifecycle",
|
|
38276
|
+
/** No code moves, and an armed auto-merge is released. */
|
|
38277
|
+
"review-approve",
|
|
38278
|
+
/** Discards commits that landed after the last fetch, on a shared ref. */
|
|
38279
|
+
"force-push",
|
|
38280
|
+
/** Destroys uncommitted work belonging to a session this one cannot see. */
|
|
38281
|
+
"worktree-remove",
|
|
38282
|
+
/** `-x` is the flag that takes `.env` and local certificates. */
|
|
38283
|
+
"clean-ignored"
|
|
37864
38284
|
]);
|
|
37865
38285
|
WARN_OPS = /* @__PURE__ */ new Set([
|
|
37866
38286
|
"commit",
|
|
37867
38287
|
"push",
|
|
37868
38288
|
"pr-create",
|
|
37869
|
-
"branch"
|
|
38289
|
+
"branch",
|
|
38290
|
+
// ── Added 2026-09-03, deliberately at warn rather than block.
|
|
38291
|
+
//
|
|
38292
|
+
// Each is destructive in some reading, and each is also routine enough that
|
|
38293
|
+
// blocking it would be the friction that gets the whole guard routed around —
|
|
38294
|
+
// the failure mode this estate has already recorded twice. `reset-hard` is the
|
|
38295
|
+
// sharpest case and stays here for a decidable reason: the Hard Floor's
|
|
38296
|
+
// qualifier is "past unpushed work", and the command text cannot say whether
|
|
38297
|
+
// the work was pushed.
|
|
38298
|
+
"tag-force",
|
|
38299
|
+
"rebase",
|
|
38300
|
+
"reset-hard",
|
|
38301
|
+
"clean",
|
|
38302
|
+
"stash-drop",
|
|
38303
|
+
"branch-delete",
|
|
38304
|
+
"close"
|
|
37870
38305
|
]);
|
|
37871
38306
|
G = "(?:-[A-Za-z-]+(?:=\\S+)?(?:\\s+\\S+)?\\s+)*";
|
|
37872
38307
|
P = "^(?:\\S*\\/)?";
|
|
@@ -37906,6 +38341,72 @@ var init_block_unauthorized_git = __esm({
|
|
|
37906
38341
|
op: "pr-create",
|
|
37907
38342
|
re: new RegExp(`${P}gh\\s+pr\\s+create\\b|${ghApiWrite("\\/pulls\\b").source}`, "i")
|
|
37908
38343
|
},
|
|
38344
|
+
// ── Added 2026-09-03. Placement inside this table is as load-bearing as the
|
|
38345
|
+
// patterns: the loop takes the MOST SEVERE match per segment, but within one
|
|
38346
|
+
// op the first pattern wins, so anything more specific than a plain `git push`
|
|
38347
|
+
// has to sit above it.
|
|
38348
|
+
// `npm unpublish` is NOT reached by the publish pattern — `\bpublish\b` needs
|
|
38349
|
+
// a word boundary that `unpublish` does not provide between `n` and `p`. That
|
|
38350
|
+
// is why it classified as nothing for as long as it did.
|
|
38351
|
+
{ op: "unpublish", re: new RegExp(`${P}(npm|pnpm|yarn)\\s+${G}unpublish\\b`, "i") },
|
|
38352
|
+
{ op: "deprecate", re: new RegExp(`${P}(npm|pnpm|yarn)\\s+${G}deprecate\\b`, "i") },
|
|
38353
|
+
{
|
|
38354
|
+
op: "release-asset",
|
|
38355
|
+
re: new RegExp(`${P}gh\\s+release\\s+(upload|edit|delete)\\b`, "i")
|
|
38356
|
+
},
|
|
38357
|
+
{
|
|
38358
|
+
op: "protection",
|
|
38359
|
+
re: new RegExp(
|
|
38360
|
+
`${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`,
|
|
38361
|
+
"i"
|
|
38362
|
+
)
|
|
38363
|
+
},
|
|
38364
|
+
{
|
|
38365
|
+
op: "workflow-toggle",
|
|
38366
|
+
re: new RegExp(`${P}gh\\s+workflow\\s+(disable|enable)\\b`, "i")
|
|
38367
|
+
},
|
|
38368
|
+
{
|
|
38369
|
+
op: "repo-lifecycle",
|
|
38370
|
+
re: new RegExp(
|
|
38371
|
+
`${P}gh\\s+repo\\s+(archive|delete|unarchive)\\b|${P}gh\\s+repo\\s+edit\\b(?=[^\\n]*--visibility\\b)`,
|
|
38372
|
+
"i"
|
|
38373
|
+
)
|
|
38374
|
+
},
|
|
38375
|
+
{
|
|
38376
|
+
op: "review-approve",
|
|
38377
|
+
re: new RegExp(`${P}gh\\s+pr\\s+review\\b(?=[^\\n]*--approve\\b)`, "i")
|
|
38378
|
+
},
|
|
38379
|
+
// Above `push`, and both spellings: `--force` and `--force-with-lease`. The
|
|
38380
|
+
// "safe force" is in the same class here because what it is safe against is a
|
|
38381
|
+
// stale local view, not a collaborator who pushed after your fetch.
|
|
38382
|
+
{
|
|
38383
|
+
op: "force-push",
|
|
38384
|
+
re: new RegExp(`${P}git\\s+${G}push\\b[^\\n;|&]*(--force\\b|--force-with-lease\\b|\\s-f\\b)`, "i")
|
|
38385
|
+
},
|
|
38386
|
+
{
|
|
38387
|
+
op: "worktree-remove",
|
|
38388
|
+
re: new RegExp(`${P}git\\s+worktree\\s+remove\\b[^\\n;|&]*(--force\\b|\\s-f\\b)`, "i")
|
|
38389
|
+
},
|
|
38390
|
+
// `-x` (or `-X`) anywhere in the flag cluster is what reaches ignored files.
|
|
38391
|
+
{
|
|
38392
|
+
op: "clean-ignored",
|
|
38393
|
+
re: new RegExp(`${P}git\\s+clean\\b[^\\n;|&]*-[A-Za-z]*x`, "i")
|
|
38394
|
+
},
|
|
38395
|
+
{ op: "tag-force", re: new RegExp(`${P}git\\s+tag\\b[^\\n;|&]*(-f\\b|--force\\b)`, "i") },
|
|
38396
|
+
{ op: "rebase", re: new RegExp(`${P}git\\s+${G}rebase\\b`, "i") },
|
|
38397
|
+
{ op: "reset-hard", re: new RegExp(`${P}git\\s+${G}reset\\b[^\\n;|&]*--hard\\b`, "i") },
|
|
38398
|
+
{ op: "clean", re: new RegExp(`${P}git\\s+clean\\b`, "i") },
|
|
38399
|
+
{ op: "stash-drop", re: new RegExp(`${P}git\\s+stash\\s+(drop|clear)\\b`, "i") },
|
|
38400
|
+
// Remote deletion and the local `-D` are one op: both remove a ref, and the
|
|
38401
|
+
// remote form additionally closes any open PR pointing at it.
|
|
38402
|
+
{
|
|
38403
|
+
op: "branch-delete",
|
|
38404
|
+
re: new RegExp(
|
|
38405
|
+
`${P}git\\s+branch\\b[^\\n;|&]*(-D\\b|--delete\\b)|${P}git\\s+${G}push\\b[^\\n;|&]*(--delete\\b|\\s:\\S)`,
|
|
38406
|
+
"i"
|
|
38407
|
+
)
|
|
38408
|
+
},
|
|
38409
|
+
{ op: "close", re: new RegExp(`${P}gh\\s+(pr|issue)\\s+close\\b`, "i") },
|
|
37909
38410
|
{ op: "push", re: new RegExp(`${P}git\\s+${G}push\\b`, "i") },
|
|
37910
38411
|
{ op: "commit", re: new RegExp(`${P}git\\s+${G}commit\\b`, "i") },
|
|
37911
38412
|
{ op: "branch", re: new RegExp(`${P}git\\s+${G}(checkout\\s+-b|switch\\s+-c)\\b`, "i") }
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@event4u/agent-config",
|
|
3
3
|
"mcpName": "io.github.event4u-app/agent-config",
|
|
4
|
-
"version": "14.
|
|
4
|
+
"version": "14.15.0",
|
|
5
5
|
"description": "Governed skills, rules and commands for AI coding tools (Claude Code, Cursor, Windsurf, Copilot) — every claim machine-checked, including the counts in these badges.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"private": false,
|