@event4u/agent-config 14.15.0 → 14.17.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 (58) 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 +84 -153
  5. package/README.md +1 -1
  6. package/dist/agent-src/commands/analyze/inbox.md +172 -6
  7. package/dist/agent-src/commands/fix/commit-messages.md +336 -0
  8. package/dist/agent-src/commands/fix.md +5 -3
  9. package/dist/agent-src/rules/commit-conventions.md +1 -1
  10. package/dist/agent-src/rules/security-sensitive-stop.md +7 -5
  11. package/dist/agent-src/skills/accessibility-auditor/SKILL.md +20 -2
  12. package/dist/agent-src/skills/aws-infrastructure/SKILL.md +41 -0
  13. package/dist/agent-src/skills/conventional-commits-writing/SKILL.md +234 -4
  14. package/dist/agent-src/skills/design-intelligence/data/ux-guidelines.csv +99 -99
  15. package/dist/agent-src/skills/fe-design/SKILL.md +6 -3
  16. package/dist/agent-src/skills/fe-design/references/design-patterns.md +28 -0
  17. package/dist/agent-src/skills/iconography/SKILL.md +37 -6
  18. package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +1 -1
  19. package/dist/agent-src/skills/terraform/SKILL.md +29 -0
  20. package/dist/agent-src/skills/threat-modeling/data/reasoning.csv +1 -0
  21. package/dist/agent-src/skills/threat-modeling/data/surfaces.csv +1 -0
  22. package/dist/agent-src/skills/threat-modeling/data/threats.csv +5 -0
  23. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  24. package/dist/cli-delegate/cmd_conformance.js +158 -33
  25. package/dist/cli-delegate/cmd_explain.js +24 -112
  26. package/dist/discovery/deprecation-report.md +1 -1
  27. package/dist/discovery/discovery-manifest.json +47 -21
  28. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  29. package/dist/discovery/discovery-manifest.summary.md +3 -3
  30. package/dist/discovery/dormancy-report.md +1 -1
  31. package/dist/discovery/orphan-report.md +1 -1
  32. package/dist/discovery/packs.json +6 -5
  33. package/dist/discovery/trust-report.md +2 -2
  34. package/dist/discovery/workspaces.json +5 -4
  35. package/dist/hooks/dispatch.js +169 -41
  36. package/dist/mcp/registry-manifest.json +3 -3
  37. package/dist/mcp/server.json +2 -2
  38. package/dist/mcp/server.mjs +160 -35
  39. package/dist/router.json +1 -1
  40. package/docs/guidelines/design-antipatterns.md +10 -2
  41. package/package.json +1 -1
  42. package/src/config/gate-violation-baselines.json +2 -2
  43. package/src/config/web-launch-readiness.json +1 -1
  44. package/src/scripts/_cli/explain_last/index.ts +1 -1
  45. package/src/scripts/_cli/explain_last/memory.ts +8 -104
  46. package/src/scripts/check_artefact_count_messaging.ts +9 -0
  47. package/src/scripts/check_release_highlights.ts +77 -4
  48. package/src/scripts/check_web_launch_readiness.ts +295 -15
  49. package/src/scripts/design_slop_rules.ts +19 -9
  50. package/src/scripts/git_authorization_hook.ts +257 -35
  51. package/src/scripts/hooks/block_unauthorized_git.ts +16 -4
  52. package/src/scripts/hooks/merge_impact.ts +68 -0
  53. package/src/scripts/measure_release_mix.ts +384 -0
  54. package/src/scripts/release.ts +4 -17
  55. package/src/scripts/release_drill.ts +154 -1
  56. package/src/scripts/release_mix_taxonomy.json +172 -0
  57. package/src/scripts/release_publication.ts +91 -0
  58. package/src/scripts/attest_artifact.ts +0 -193
@@ -171,6 +171,34 @@ Before adding any animation to a UI element, run through this decision tree:
171
171
  - **Never animate layout properties** (width, height, top, left, padding). Why: triggers browser layout recalculation on every frame; always solvable with `transform`.
172
172
  - **Always add `@media (prefers-reduced-motion: reduce)`** — gentler animation (reduced distance/opacity), NOT display:none. Why: vestibular disorders make motion UI unusable; reducing is better than removing.
173
173
 
174
+ **4. Is the trigger the reader's position, or the reader's action?**
175
+
176
+ Position-triggered motion is the interaction layer's current tell, and it is
177
+ the one case where the decision tree above returns "no" and gets overridden
178
+ anyway. Two shapes, both catalogued:
179
+
180
+ - **Scroll-reveal on every section** (catalog M6). The reader has already
181
+ scrolled to the content; fading it in delays what they asked for. Animate a
182
+ reveal only where position carries meaning — a stepped narrative whose beats
183
+ must land in order, or a long-form piece using position to signal progress —
184
+ and then say so in the brief. A reveal applied per section is a default.
185
+ - **A pointer-tracking spotlight** (catalog M7). The cursor is the one element
186
+ on the page whose position the reader already knows, so highlighting it
187
+ carries no information. Legitimate only where the spotlight *is* the
188
+ interaction: an inspection tool, a reveal mechanic, a magnifier.
189
+
190
+ **5. Does the hover state spend contrast or add it?**
191
+
192
+ A control that lowers its own opacity on hover (catalog M8) reads as retreating
193
+ from the pointer, and it reduces legibility at the moment the reader is
194
+ committing to the target. Hover is carried by brightness, background, border or
195
+ elevation — never by removing contrast. A fade is correct only when it
196
+ communicates a real state change, such as an item being dismissed.
197
+
198
+ The six interaction states every interactive element must assert are
199
+ [`design-review`](../../design-review/SKILL.md)'s; this entry is the motion half
200
+ of the hover one.
201
+
174
202
  **4. What to animate?**
175
203
  Animate `transform` and `opacity` only. Why: these run on the GPU compositor thread, not the main thread; they never trigger layout or paint.
176
204
  `scale(0)` → `scale(1)` is wrong. Why: nothing in the real world appears from nothing. Use `scale(0.95)` + `opacity: 0` → `scale(1)` + `opacity: 1` instead.
@@ -37,12 +37,30 @@ execution:
37
37
  1. **Identify the icon need** — what does the icon communicate? Name the semantic
38
38
  intent (e.g., "download action", "warning state", "user profile") rather than
39
39
  visual description alone.
40
- 2. **Pick the icon set** per stack and brand context. Default open sets:
41
- **Lucide** (clean, Tailwind-native), **Heroicons** (Tailwind/React first-party),
42
- **Phosphor** (multi-weight, flexible), **Tabler** (comprehensive, stroke-based).
43
- Brand/provider marks use **lobe-icons** (`lobe-icons:openai-color`, etc.).
44
- Stack consumers: `react-shadcn-ui` → Lucide; `blade-ui` → Heroicons or Lucide;
45
- `tailwind-engineer` → any web-font-friendly set.
40
+ 2. **Choose the icon set — three rungs, and record which one answered.** The
41
+ set is *chosen*, never inherited: adopting one because a scaffold shipped it
42
+ is the anti-pattern [`icon-consistency`](../../rules/icon-consistency.md)
43
+ § What this gates names by name. Stop at the first rung that answers.
44
+ 1. **A brand token or brand guide that names an icon set** — authoritative,
45
+ stop here ([`brand-source-of-truth`](../../rules/brand-source-of-truth.md)).
46
+ 2. **The set already in use in this project** — detected at the Inspect step
47
+ of the Iconography floor below. One icon system per project; a second
48
+ visual language is the defect, not a preference.
49
+ 3. **Neither exists → select from the open candidates by the criterion the
50
+ surface actually imposes, and state which criterion decided it.** The
51
+ candidates, unordered: **Heroicons**, **Lucide**, **Phosphor**, **Tabler**.
52
+
53
+ | Criterion the surface imposes | Candidates that satisfy it |
54
+ |---|---|
55
+ | Several stroke weights inside one UI (thin captions, bold empty states) | Phosphor (six weights) |
56
+ | One uniform stroke across a dense admin UI | Lucide, Tabler |
57
+ | First-party match to a component library the project already adopted | Heroicons (Tailwind UI), Lucide (shadcn/ui) |
58
+ | Widest glyph coverage for domain-specific nouns | Tabler, Phosphor |
59
+
60
+ Two criteria pointing at different candidates is a real trade-off — name it
61
+ and pick one; do not split the UI across both. Brand and provider marks are
62
+ a separate axis and use **lobe-icons** (`lobe-icons:openai-color`) whatever
63
+ the UI set is.
46
64
  3. **Resolve to a concrete Iconify name** in `set:name` format, e.g.
47
65
  `lucide:arrow-right`, `heroicons:user-solid`, `ph:warning-bold`,
48
66
  `tabler:download`. Verify the name exists on
@@ -54,6 +72,19 @@ execution:
54
72
  5. **Verify** — check the resolved name on `iconify.design/icon/{set}:{name}`;
55
73
  confirm the icon renders as expected before shipping.
56
74
 
75
+ ## Which set each stack ships with — an observation, not a pick
76
+
77
+ Recorded so rung 2 above can recognise an incumbent. Finding one of these in a
78
+ project is rung-2 evidence. Reaching for it because the stack is *new* skips
79
+ rung 3 and is exactly the scaffold inheritance
80
+ [`icon-consistency`](../../rules/icon-consistency.md) forbids.
81
+
82
+ | Stack / consumer | Ships with |
83
+ |---|---|
84
+ | `react-shadcn-ui` | Lucide |
85
+ | `blade-ui` | Heroicons (Tailwind first-party) |
86
+ | `tailwind-engineer` | none of its own — any web-font-friendly set |
87
+
57
88
  ## Iconography floor (design fidelity)
58
89
 
59
90
  Before resolving any new icon, honour the project's existing icon system — the
@@ -122,7 +122,7 @@ Do NOT use when:
122
122
  components in the same surface.
123
123
  - **Anti-AI-slop catalog + linter.** Pull
124
124
  [`docs/guidelines/design-antipatterns.md`](../../../docs/guidelines/design-antipatterns.md)
125
- before the polish step (Visual V1–V7, Layout L1–L8 are the React-component
125
+ before the polish step (Visual V1–V8, Layout L1–L10 are the React-component
126
126
  slop tells); the objective quality floors (WCAG contrast, focus-visible,
127
127
  reduced-motion) are validated via `accessibility-auditor`'s checklist —
128
128
  cite its verdict rather than eyeballing.
@@ -96,6 +96,35 @@ Check existing modules in the project for which registry modules are used.
96
96
  - Security groups follow least-privilege: only allow traffic between known services.
97
97
  - IAM policies use specific resource ARNs, not wildcards (except where unavoidable).
98
98
 
99
+ ### Permissive defaults and missing security blocks
100
+
101
+ The failure that reaches production is almost never a pinned-version mistake;
102
+ it is a resource that works either way and was left on its permissive default.
103
+ Four constructs carry it. Each backstop grep is over the `.tf` tree you are
104
+ changing, not over this repository.
105
+
106
+ | Backstop grep | What a hit means | Override condition |
107
+ |---|---|---|
108
+ | `grep -rn '0\.0\.0\.0/0' --include='*.tf' .` | An ingress or egress rule whose CIDR is 0.0.0.0/0, i.e. open to the whole internet. On a management or database port stop and treat it as CRITICAL - the corpus row enumerates which ports those are. | A deliberately public listener - an ALB or CloudFront origin on 80/443. Nothing else. Record the reason next to the rule. |
109
+ | `grep -rn 'publicly_accessible\|block_public_acls\|ignore_public_acls' --include='*.tf' .` | Either `publicly_accessible = true` on a database, or a bucket with no public-access block present at all. Absence is the more common hit and the grep returning nothing is the failing case. | A bucket that genuinely serves public web assets, with the policy written explicitly rather than inherited from a default. |
110
+ | `grep -rnE '"(Action\|Resource)" *: *"\*"\|= *\["\*"\]' --include='*.tf' .` | A wildcard IAM action or resource. One compromised workload credential then reaches everything in the account. | A named, reviewed exception - `iam:PassRole` scoped by condition, or a bootstrap role. Never the default. |
111
+ | `grep -rL 'encrypt' --include='*.tf' .` (lists files with NO encryption mention; `-L` prints names only, so no `-n`) | A storage, volume, snapshot, or database resource with no encryption block. | None for data at rest. A resource holding no data at all. |
112
+
113
+ Read the surface class before the fix, not after: the mitigation text, the
114
+ abuse case, and the negative test live in the `infrastructure` rows of
115
+ [`threat-modeling`](../threat-modeling/SKILL.md)'s corpus, which is the single
116
+ authority for them. Ask that skill's grounded-corpus step for the
117
+ `infrastructure` surface class - it carries the invocation - and describe the
118
+ change you are making, e.g. *terraform security group ingress open to
119
+ 0.0.0.0/0*. Do not copy the command here: one skill owns that interface, for
120
+ the same reason one file owns the control text.
121
+
122
+ Those rows also carry a `Decided by:` clause stating which check actually
123
+ decides each one. For all four constructs above that clause reads NO CHECK IN
124
+ THIS REPOSITORY - no HCL or IAM-policy parser ships here, so these greps and
125
+ your own IaC policy scanner are the whole enforcement story. Do not write or
126
+ imply otherwise.
127
+
99
128
  ## Common patterns
100
129
 
101
130
  ### ECS service with CodeDeploy (Blue/Green)
@@ -8,3 +8,4 @@ public-endpoint,Rate limit + cache + minimal payload,"{""if_search_endpoint"": "
8
8
  billing,Ledger + idempotency + server-side amounts,"{""if_refunds"": ""one-way-state-machine"", ""if_webhooks_from_psp"": ""verify-signature-and-amount""}",Mutable balance column updates,CRITICAL
9
9
  secrets,Manager + redaction + scan,"{""if_third_party_keys"": ""scope-and-rotate-quarterly"", ""if_ci_pipeline"": ""masked-vars-only""}",Secrets in env committed to VCS,HIGH
10
10
  data-exposure,Allowlist serializers + contract tests,"{""if_admin_fields"": ""separate-serializer"", ""if_logs_contain_payloads"": ""logger-redaction-keys""}",Spreading model attributes into responses,MEDIUM
11
+ infrastructure,Deny-by-default ingress + encryption on + scoped IAM,"{""if_management_or_db_port"": ""no-internet-cidr-use-bastion-or-private-link"", ""if_object_storage"": ""block-public-access-at-account-and-bucket"", ""if_iam_policy"": ""enumerate-actions-and-resource-arns""}",Permissive defaults left in place because the resource works either way,HIGH
@@ -8,3 +8,4 @@ Public unauthenticated endpoint,public endpoint health status unauthenticated an
8
8
  Billing / payment / balance mutation,billing payment charge refund invoice subscription balance credit,billing,money movement,Anything that changes an amount owed/paid
9
9
  Secrets / key handling,secret api-key signing key env vault kms rotation,secrets,credentials,"Storage, rotation, and logging paths"
10
10
  Data export / serializer / log channel,export serializer api-resource log audit csv download report,data-exposure,"PII, internal fields",Response shaping and log sinks
11
+ Infrastructure / IaC resource definition,terraform iac infrastructure provisioning security group ingress egress cidr bucket storage iam role subnet vpc encryption,infrastructure,"network reachability, data at rest, cloud credentials",Any IaC change to networking storage IAM or encryption defaults
@@ -14,3 +14,8 @@ billing,Race condition on balance / double-spend,CWE-362,Parallel requests redee
14
14
  billing,Price/amount tampering,CWE-602,Client-supplied price or currency trusted at checkout,Recompute amounts server-side from catalog; signed cart or server-held cart only,Test: tampered client amount → server total unchanged,CRITICAL
15
15
  secrets,Secret leakage via logs / errors / VCS,"CWE-532, ATT&CK T1552","Key appears in exception payload, debug log, or committed .env",Centralized secret manager; logger redaction list; pre-commit secret scan; scoped short-lived tokens,Test: forced exception on auth path → no credential substring in log sink,HIGH
16
16
  data-exposure,Over-broad serializer / API resource,"CWE-213, OWASP API3","Serializer returns internal fields (cost, flags, foreign tenant refs) the UI ignores but attackers read",Explicit field allowlist per resource; separate admin vs public serializers; contract test on response shape,Test: schema-diff response against documented contract → no undocumented fields,MEDIUM
17
+ infrastructure,Unrestricted ingress to a management or database port,"CWE-1327, ATT&CK T1190",Attacker reaches SSH/RDP or a database engine straight from the internet because a security-group or firewall rule allows 0.0.0.0/0,"Deny-by-default ingress; management and database ports reachable only via bastion, VPN, or private link; CIDR scoped to known ranges; never 0.0.0.0/0 on 22/3389/3306/5432/6379/27017","Test: a rule with cidr_blocks 0.0.0.0/0 on a management or database port is rejected. Decided by: NO CHECK IN THIS REPOSITORY - nothing here parses HCL or cloud config; the deciding check is the consumer's own IaC policy scanner",CRITICAL
18
+ infrastructure,Object storage public by default,"CWE-732, OWASP A05",Bucket or container created without a block-public-access setting so any object URL is world-readable,Block public access at account AND bucket level; explicit policy allowlist instead of a public ACL; serve objects via signed URLs or a CDN origin identity,"Test: storage created with no explicit public-access block is rejected; anonymous GET of a stored object returns 403. Decided by: NO CHECK IN THIS REPOSITORY - the deciding check is the consumer's own IaC policy scanner or a cloud posture tool",HIGH
19
+ infrastructure,Missing encryption at rest or in transit,"CWE-311, CWE-319",Volume snapshot bucket or database instance created with no encryption block; endpoint accepts a plaintext connection,Encryption block mandatory on every storage and database resource; customer-managed key where the data class requires it; TLS-only bucket policy and listener; reject plaintext listeners,"Test: a storage or database resource with no encryption block is rejected; a plaintext connection to the endpoint is refused. Decided by: NO CHECK IN THIS REPOSITORY - the deciding check is the consumer's own IaC policy scanner",HIGH
20
+ infrastructure,Wildcard IAM action or resource,"CWE-269, ATT&CK T1078.004",A role policy grants Action * or Resource * so one compromised workload credential reaches every service and object in the account,Enumerate actions and resource ARNs; one role per workload; permission boundary or service control policy on privileged roles; no wildcard outside a named reviewed exception,"Test: a policy document containing Action ""*"" or Resource ""*"" is rejected or carries a named exception. Decided by: NO CHECK IN THIS REPOSITORY - nothing here parses IAM policy documents; the deciding check is the consumer's own IaC policy scanner",CRITICAL
21
+ infrastructure,Credential in an IaC or vars file instead of a secret manager,"CWE-798, ATT&CK T1552.001",Password token or key written as a literal into a .tf .tfvars or .yaml file and committed,Secret-manager reference or runtime-injected env var; never a literal in IaC or vars files; treat the state file as sensitive and store it encrypted with restricted access,"Test: a literal credential in a tracked IaC or vars file fails the build. Decided by: check_secret_leak (src/scripts/check_secret_leak.ts) inside THIS repository - it has no extension allowlist so .tf and .yaml are in scope; for a CONSUMER the deciding check is the gitleaks workflow the package ships as a template that the consumer must copy",HIGH
@@ -39,7 +39,7 @@ schema_version: 1
39
39
  # CI guard: a release bump of `package.json` must update this value
40
40
  # in lockstep — see scripts/check_template_pin_drift.ts (road-to-
41
41
  # portable-runtime-and-update-check P3.3).
42
- agent_config_version: "14.15.0"
42
+ agent_config_version: "14.17.0"
43
43
 
44
44
  # --- Project identity ---
45
45
  project:
@@ -807,20 +807,22 @@ var PHRASES = [
807
807
  // "never auto-merge" each returned ["pr-merge"] — a prompt saying DO NOT
808
808
  // MERGE authorized a merge. The noun-sense lookahead was never a negator.
809
809
  //
810
- // The lookBEHIND is line-scoped by `[^.!?\n]` on purpose, the same scoping
811
- // `turn_end_gate_hook.ts` uses for its own negation exclusion (`:330`) and
812
- // its negated-claim window (`:490-493`). A sentence boundary ends a
813
- // negation's reach: "Do not push. Merge PR #12." is two instructions, and a
814
- // guard that let the first suppress the second would silently stop
815
- // authorizing merges the user DID order — the failure mode that is worse
816
- // than the defect, because nothing happens and nothing says why.
817
- //
818
- // Reusing that vocabulary rather than inventing a third is deliberate: two
810
+ // The inline negative lookBEHIND that used to live in this pattern was
811
+ // REMOVED on 2026-09-03, and its own comment is why. It warned that "two
819
812
  // negation vocabularies in one tree drift, and the drift is invisible until
820
- // a prompt lands in the gap between them.
813
+ // a prompt lands in the gap between them" — and by then they had drifted:
814
+ // `negatedBefore` became clause-scanned in both directions while this
815
+ // lookbehind stayed a blind 30-character window with no forward half. The
816
+ // prompt that landed in the gap is `"nicht pushen, aber mergen"`, which the
817
+ // lookbehind refused (13 characters, inside its window, across a contrast
818
+ // cue it could not see) while the clause scan correctly authorizes it.
819
+ //
820
+ // Every phrase match already passes through `negatedBefore` at the call
821
+ // site, so the guard is not weakened by the removal — it is applied once,
822
+ // by the one vocabulary this file's comment asked for.
821
823
  {
822
824
  op: "pr-merge",
823
- 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
825
+ re: /\b(merge|merg(e|en|st|t)|zusammenf(ü|ue)hren|reinmergen)\b(?!\s*[- ]?(conflict|konflikt|commit|base|queue|state|status))/i
824
826
  },
825
827
  // A tag is an ACTION here — bare "Tag" is the German word for day, and
826
828
  // "Version" is an ordinary noun. Both authorized a BLOCK op before this.
@@ -942,13 +944,16 @@ function isInterrogative(prose) {
942
944
  if (!t) {
943
945
  return false;
944
946
  }
945
- 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(
947
+ if (/\?\s*$/.test(t)) {
948
+ return true;
949
+ }
950
+ 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(
946
951
  t
947
952
  );
948
953
  if (hasImperative) {
949
954
  return false;
950
955
  }
951
- 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(
956
+ 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(
952
957
  t
953
958
  );
954
959
  }
@@ -995,13 +1000,18 @@ function splitFences(prompt) {
995
1000
  function _commandLines(fence) {
996
1001
  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));
997
1002
  }
1003
+ function _errText(err) {
1004
+ const t = err instanceof Error ? err.message : String(err);
1005
+ return t.replace(/\s+/g, " ").trim().slice(0, 200);
1006
+ }
1007
+ var PR_NUMBER_MAX = 2147483647;
998
1008
  function extractMergeTargets(prompt) {
999
1009
  const out = /* @__PURE__ */ new Set();
1000
- const re = /(?:#|\bpr[- ]?|\bpull[- ]request\s+#?)(\d{1,7})\b/giu;
1010
+ const re = /(?:#|\bpr[- ]?|\bpull[- ]request\s+#?)(\d{1,10})\b/giu;
1001
1011
  let m;
1002
1012
  while ((m = re.exec(prompt)) !== null) {
1003
1013
  const n = Number.parseInt(m[1], 10);
1004
- if (Number.isSafeInteger(n) && n > 0) {
1014
+ if (Number.isSafeInteger(n) && n > 0 && n <= PR_NUMBER_MAX) {
1005
1015
  out.add(n);
1006
1016
  }
1007
1017
  }
@@ -1045,27 +1055,109 @@ function foldGrants(prior, prompt, authorized, session_id, now) {
1045
1055
  ];
1046
1056
  }
1047
1057
  var NEGATION_WORD = /\b(nicht|nichts|kein(e|en|em|er|es)?|niemals|nie|no|not|dont|don't|never|without|ohne)\b/i;
1048
- function negatedBefore(text, index, matched = "") {
1058
+ var CONTRAST_CUE = /\b(aber|sondern|jedoch|but|however|instead)\b/gi;
1059
+ 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;
1060
+ function isSentenceBoundary(text, at) {
1061
+ const mark = text[at];
1062
+ if (mark !== ".") {
1063
+ return true;
1064
+ }
1065
+ if (/\d/.test(text[at - 1] ?? "") && /\d/.test(text[at + 1] ?? "")) {
1066
+ return false;
1067
+ }
1068
+ return !ABBREVIATION_BEFORE_DOT.test(text.slice(0, at + 1));
1069
+ }
1070
+ function sentenceBounds(text, index) {
1049
1071
  let start = 0;
1050
- for (const mark of [".", "!", "?", "\n"]) {
1051
- const at = text.lastIndexOf(mark, index - 1);
1052
- if (at + 1 > start) {
1053
- start = at + 1;
1072
+ for (let i = index - 1; i >= 0; i -= 1) {
1073
+ const c = text[i];
1074
+ if ((c === "." || c === "!" || c === "?" || c === "\n") && isSentenceBoundary(text, i)) {
1075
+ start = i + 1;
1076
+ break;
1054
1077
  }
1055
1078
  }
1056
- const before = text.slice(Math.max(start, index - 30), index);
1057
- return NEGATION_WORD.test(before) || NEGATION_WORD.test(matched);
1079
+ let end = text.length;
1080
+ for (let i = index; i < text.length; i += 1) {
1081
+ const c = text[i];
1082
+ if ((c === "." || c === "!" || c === "?" || c === "\n") && isSentenceBoundary(text, i)) {
1083
+ end = i;
1084
+ break;
1085
+ }
1086
+ }
1087
+ return { start, end };
1088
+ }
1089
+ function _sentences(text) {
1090
+ const out = [];
1091
+ let from = 0;
1092
+ for (let i = 0; i < text.length; i += 1) {
1093
+ const c = text[i];
1094
+ if ((c === "." || c === "!" || c === "?" || c === "\n") && isSentenceBoundary(text, i)) {
1095
+ out.push(text.slice(from, i + 1));
1096
+ from = i + 1;
1097
+ }
1098
+ }
1099
+ if (from < text.length) {
1100
+ out.push(text.slice(from));
1101
+ }
1102
+ return out.filter((t) => t.trim() !== "");
1103
+ }
1104
+ function clauseStart(text, from, to) {
1105
+ let start = from;
1106
+ const seg = text.slice(from, to);
1107
+ for (let i = seg.length - 1; i >= 0; i -= 1) {
1108
+ const c = seg[i];
1109
+ if (c === "," || c === ";" || c === ":") {
1110
+ start = from + i + 1;
1111
+ break;
1112
+ }
1113
+ }
1114
+ CONTRAST_CUE.lastIndex = 0;
1115
+ let m;
1116
+ while ((m = CONTRAST_CUE.exec(seg)) !== null) {
1117
+ const after = from + m.index + m[0].length;
1118
+ if (after > start) {
1119
+ start = after;
1120
+ }
1121
+ }
1122
+ return start;
1123
+ }
1124
+ function clauseEnd(text, from, to) {
1125
+ const seg = text.slice(from, to);
1126
+ let end = to;
1127
+ const punct = seg.search(/[,;:]/);
1128
+ if (punct !== -1) {
1129
+ end = from + punct;
1130
+ }
1131
+ CONTRAST_CUE.lastIndex = 0;
1132
+ const m = CONTRAST_CUE.exec(seg);
1133
+ if (m !== null && from + m.index < end) {
1134
+ end = from + m.index;
1135
+ }
1136
+ return end;
1137
+ }
1138
+ function negatedBefore(text, index, matched = "") {
1139
+ const { start, end } = sentenceBounds(text, index);
1140
+ const before = text.slice(clauseStart(text, start, index), index);
1141
+ const afterFrom = index + matched.length;
1142
+ const after = afterFrom < end ? text.slice(afterFrom, clauseEnd(text, afterFrom, end)) : "";
1143
+ return NEGATION_WORD.test(before) || NEGATION_WORD.test(matched) || NEGATION_WORD.test(after);
1058
1144
  }
1059
1145
  function classifyAuthorization(prompt) {
1060
1146
  const { prose, fences } = splitFences(prompt);
1061
1147
  const authorized = /* @__PURE__ */ new Set();
1062
1148
  const evidence = {};
1063
1149
  const instruction = prose.split("\n").filter((l) => !OUTPUT_LINE.test(l)).join("\n");
1064
- for (const { op, re } of isInterrogative(instruction) ? [] : PHRASES) {
1065
- const m = re.exec(instruction);
1066
- if (m && !negatedBefore(instruction, m.index, m[0])) {
1067
- authorized.add(op);
1068
- evidence[op] = `prose: "${m[0]}"`;
1150
+ for (const { op, re } of PHRASES) {
1151
+ for (const sentence of _sentences(instruction)) {
1152
+ if (isInterrogative(sentence)) {
1153
+ continue;
1154
+ }
1155
+ const m = re.exec(sentence);
1156
+ if (m && !negatedBefore(sentence, m.index, m[0])) {
1157
+ authorized.add(op);
1158
+ evidence[op] = `prose: "${m[0]}"`;
1159
+ break;
1160
+ }
1069
1161
  }
1070
1162
  }
1071
1163
  for (const fence of fences) {
@@ -1143,7 +1235,11 @@ function consumeGrantTarget(consumer_root, session_id, target) {
1143
1235
  if (touched) {
1144
1236
  try {
1145
1237
  atomic_write_json(file, decoded);
1146
- } catch {
1238
+ } catch (err) {
1239
+ process.stderr.write(
1240
+ `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.
1241
+ `
1242
+ );
1147
1243
  }
1148
1244
  }
1149
1245
  }
@@ -1236,7 +1332,11 @@ function run2(stdin_text, options) {
1236
1332
  };
1237
1333
  try {
1238
1334
  atomic_write_json(path2.join(options.consumer_root, ledgerFileFor(session)), ledger);
1239
- } catch {
1335
+ } catch (err) {
1336
+ process.stderr.write(
1337
+ `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.
1338
+ `
1339
+ );
1240
1340
  }
1241
1341
  return EXIT_ALLOW2;
1242
1342
  }
@@ -1300,6 +1400,24 @@ var DESTRUCTIVE_MARKERS = [
1300
1400
  { 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 },
1301
1401
  { label: "a whole file was deleted", re: /^deleted file mode /m }
1302
1402
  ];
1403
+ var SCHEMA_REMOVAL_BODY = /\b(dropColumn|dropTable|dropIfExists|removeColumn|drop_column|drop_table|DROP\s+(TABLE|COLUMN|INDEX|CONSTRAINT)|TRUNCATE\s+TABLE)\b/i;
1404
+ var RENAME_METADATA = /^rename (?:from|to) /m;
1405
+ function fileBlocks(patch) {
1406
+ return patch.split(/^(?=diff --git )/m).filter((b) => b.startsWith("diff --git "));
1407
+ }
1408
+ function renamedSchemaRemoval(patch) {
1409
+ for (const block of fileBlocks(patch)) {
1410
+ if (!RENAME_METADATA.test(block)) {
1411
+ continue;
1412
+ }
1413
+ for (const line of block.split("\n")) {
1414
+ if (line.startsWith("-") && !line.startsWith("---") && SCHEMA_REMOVAL_BODY.test(line)) {
1415
+ return true;
1416
+ }
1417
+ }
1418
+ }
1419
+ return false;
1420
+ }
1303
1421
  var MAJOR_BUMP = /^-\s*"version"\s*:\s*"(\d+)\.[^"]*"[\s\S]{0,200}?^\+\s*"version"\s*:\s*"(\d+)\./m;
1304
1422
  function classifyDiff(patch) {
1305
1423
  if (patch.trim() === "") {
@@ -1311,6 +1429,9 @@ function classifyDiff(patch) {
1311
1429
  markers.push(label);
1312
1430
  }
1313
1431
  }
1432
+ if (renamedSchemaRemoval(patch)) {
1433
+ markers.push("a renamed file drops a column or table");
1434
+ }
1314
1435
  const bump = MAJOR_BUMP.exec(patch);
1315
1436
  if (bump && bump[1] !== bump[2]) {
1316
1437
  markers.push(`major version bump ${bump[1]}.x to ${bump[2]}.x`);
@@ -1732,12 +1853,16 @@ function commandOp(command) {
1732
1853
  return found;
1733
1854
  }
1734
1855
  function mergeTargetOf(command) {
1735
- const gh = /\bgh\s+pr\s+merge\s+(?:-{1,2}\S+\s+)*?(\d{1,7})\b/i.exec(command);
1856
+ const gh = /\bgh\s+pr\s+merge\s+(?:-{1,2}\S+\s+)*?(\d{1,10})\b/i.exec(command);
1736
1857
  if (gh) {
1737
- return Number.parseInt(gh[1], 10);
1858
+ return _boundedTarget(gh[1]);
1738
1859
  }
1739
- const api = /\/pulls\/(\d{1,7})\/merge\b/i.exec(command);
1740
- return api ? Number.parseInt(api[1], 10) : null;
1860
+ const api = /\/pulls\/(\d{1,10})\/merge\b/i.exec(command);
1861
+ return api ? _boundedTarget(api[1]) : null;
1862
+ }
1863
+ function _boundedTarget(digits) {
1864
+ const n = Number.parseInt(digits, 10);
1865
+ return Number.isSafeInteger(n) && n > 0 && n <= PR_NUMBER_MAX ? n : null;
1741
1866
  }
1742
1867
  function grantCovers(grants, op, target) {
1743
1868
  if (op !== "pr-merge" || target === null) {