|
2785 | 2785 | "expected_exit": 0 |
2786 | 2786 | } |
2787 | 2787 | ] |
| 2788 | + }, |
| 2789 | + { |
| 2790 | + "id": "warn-greptile-review-extraction-by-created-at", |
| 2791 | + "description": "Warn when a Bash command extracts Greptile review state but uses created_at / chronology / first-commit-URL capture — Greptile edits the same comment in-place, so updated_at + the \"Last reviewed commit:\" footer hash are the authoritative signals (memory feedback_greptile_match_head_not_chronology.md)", |
| 2792 | + "applies_to": [ |
| 2793 | + "Bash" |
| 2794 | + ], |
| 2795 | + "match": [ |
| 2796 | + { |
| 2797 | + "field": "command", |
| 2798 | + "pattern": "gh[[:space:]]+api\\b.*\\bcomments\\b" |
| 2799 | + }, |
| 2800 | + { |
| 2801 | + "field": "command", |
| 2802 | + "pattern": "greptile", |
| 2803 | + "flags": "i" |
| 2804 | + }, |
| 2805 | + { |
| 2806 | + "field": "command", |
| 2807 | + "pattern": "(\\bcreated_at\\b|\\b(head|tail)[[:space:]]+-n?[[:space:]]*[0-9]+|capture\\([^)]*/commit/)" |
| 2808 | + }, |
| 2809 | + { |
| 2810 | + "field": "command", |
| 2811 | + "not_pattern": "(Last reviewed commit|\\bupdated_at\\b)" |
| 2812 | + } |
| 2813 | + ], |
| 2814 | + "action": "warn", |
| 2815 | + "bypass_marker": "greptile-extraction-acknowledged", |
| 2816 | + "memory_ref": "feedback_greptile_match_head_not_chronology.md", |
| 2817 | + "references": [ |
| 2818 | + "feedback_tail_with_desc_ordering.md — sister rule on DESC|tail bug", |
| 2819 | + "CIV-728 PR #114 forensic 2026-05-25 — stale 3/5 review picked over 5/5 HEAD review" |
| 2820 | + ], |
| 2821 | + "message": "WARN: Bash command extracts Greptile review state with a chronology-based\npattern (created_at / head -N / tail -N / capture(.../commit/...)).\n\nGreptile App on GitHub EDITS the same review comment in-place on each\nre-review. As a result:\n - comment.created_at is FROZEN at original posting; it DOES NOT\n change when Greptile re-analyzes a new commit.\n - comment.updated_at IS what moves with each re-review.\n - The \"<sub>Last reviewed commit: ...(commit/HASH)</sub>\" footer in\n the body is the authoritative source for \"which commit this review\n is about\" — NOT the first /commit/ URL captured anywhere in the\n body (which is often a permalink quoted inside a finding).\n\nUse this pattern instead (copy-paste-able):\n\n HEAD=$(gh pr view <N> --json headRefOid --jq '.headRefOid')\n export SHORT=${HEAD:0:10}\n gh api \"repos/<owner>/<repo>/issues/<N>/comments\" --jq '\n [.[] | select(.user.login | test(\"greptile\";\"i\")) | {\n updated: .updated_at,\n commit: ((.body | split(\"Last reviewed commit:\")[1] // \"\" |\n split(\"commit/\")[1] // \"\" | split(\")\")[0])[0:10]),\n score: ((.body | capture(\"<h3>Confidence Score: (?<s>[0-9]/5)</h3>\").s) // \"?\")\n }] | map(select(.commit == env.SHORT)) | sort_by(.updated) | last'\n\nThis:\n 1. Pulls the PR's actual HEAD SHA up front.\n 2. Filters Greptile comments to ONLY those whose \"Last reviewed\n commit:\" footer matches HEAD (so a stale older review can't win).\n 3. Among the matching reviews, picks the latest by updated_at.\n\nPer feedback_greptile_match_head_not_chronology.md: out-of-order parallel\nre-runs make creation order misleading. Per feedback_tail_with_desc_\nordering.md: same failure mode (trust an implicit order without\nvalidating it).\n\nBypass marker (greptile-extraction-acknowledged) ONLY for legitimate\ncases (e.g., you explicitly WANT the creation order to audit posting\ncadence, not the latest state).\n", |
| 2822 | + "tests": [ |
| 2823 | + { |
| 2824 | + "name": "warns-on-sort-by-created-at-greptile-comments", |
| 2825 | + "input": { |
| 2826 | + "tool_input": { |
| 2827 | + "command": "gh api repos/o/r/issues/1/comments --jq '[.[] | select(.user.login | test(\"greptile\";\"i\"))] | sort_by(.created_at) | last'" |
| 2828 | + } |
| 2829 | + }, |
| 2830 | + "expected_exit": 0, |
| 2831 | + "expected_stderr_contains": "warn-greptile-review-extraction-by-created-at" |
| 2832 | + }, |
| 2833 | + { |
| 2834 | + "name": "warns-on-greptile-grep-then-tail", |
| 2835 | + "input": { |
| 2836 | + "tool_input": { |
| 2837 | + "command": "gh api repos/o/r/issues/1/comments --jq '.[].body' | grep -i greptile | tail -1" |
| 2838 | + } |
| 2839 | + }, |
| 2840 | + "expected_exit": 0, |
| 2841 | + "expected_stderr_contains": "warn-greptile-review-extraction-by-created-at" |
| 2842 | + }, |
| 2843 | + { |
| 2844 | + "name": "warns-on-greptile-grep-then-head", |
| 2845 | + "input": { |
| 2846 | + "tool_input": { |
| 2847 | + "command": "gh api repos/o/r/issues/1/comments --jq '.[] | select(.body | test(\"greptile\";\"i\"))' | grep 'Confidence Score' | head -1" |
| 2848 | + } |
| 2849 | + }, |
| 2850 | + "expected_exit": 0, |
| 2851 | + "expected_stderr_contains": "warn-greptile-review-extraction-by-created-at" |
| 2852 | + }, |
| 2853 | + { |
| 2854 | + "name": "warns-on-capture-commit-sha-from-greptile-body", |
| 2855 | + "input": { |
| 2856 | + "tool_input": { |
| 2857 | + "command": "gh api repos/o/r/issues/1/comments --jq '[.[] | select(.user.login | test(\"greptile\";\"i\")) | {commit: (.body | capture(\"/commit/(?<sha>[0-9a-f]+)\").sha)}]'" |
| 2858 | + } |
| 2859 | + }, |
| 2860 | + "expected_exit": 0, |
| 2861 | + "expected_stderr_contains": "warn-greptile-review-extraction-by-created-at" |
| 2862 | + }, |
| 2863 | + { |
| 2864 | + "name": "allows-correct-pattern-updated-at-and-last-reviewed-commit", |
| 2865 | + "input": { |
| 2866 | + "tool_input": { |
| 2867 | + "command": "gh api \"repos/o/r/issues/1/comments\" --jq '\n [.[] | select(.user.login | test(\"greptile\";\"i\")) | {\n updated: .updated_at,\n commit: ((.body | split(\"Last reviewed commit:\")[1] // \"\" | split(\"commit/\")[1] // \"\" | split(\")\")[0])[0:10]),\n score: ((.body | capture(\"<h3>Confidence Score: (?<s>[0-9]/5)</h3>\").s) // \"?\")\n }] | sort_by(.updated) | last'\n" |
| 2868 | + } |
| 2869 | + }, |
| 2870 | + "expected_exit": 0 |
| 2871 | + }, |
| 2872 | + { |
| 2873 | + "name": "allows-non-greptile-comments-call", |
| 2874 | + "input": { |
| 2875 | + "tool_input": { |
| 2876 | + "command": "gh api repos/o/r/issues/1/comments --jq '[.[] | .body] | sort_by(.created_at)'" |
| 2877 | + } |
| 2878 | + }, |
| 2879 | + "expected_exit": 0 |
| 2880 | + }, |
| 2881 | + { |
| 2882 | + "name": "allows-greptile-with-updated-at-and-sort", |
| 2883 | + "input": { |
| 2884 | + "tool_input": { |
| 2885 | + "command": "gh api repos/o/r/issues/1/comments --jq '[.[] | select(.user.login | test(\"greptile\";\"i\"))] | sort_by(.updated_at) | last'" |
| 2886 | + } |
| 2887 | + }, |
| 2888 | + "expected_exit": 0 |
| 2889 | + }, |
| 2890 | + { |
| 2891 | + "name": "allows-gh-search-greptile-not-api-comments", |
| 2892 | + "input": { |
| 2893 | + "tool_input": { |
| 2894 | + "command": "gh search prs --label greptile-approved" |
| 2895 | + } |
| 2896 | + }, |
| 2897 | + "expected_exit": 0 |
| 2898 | + }, |
| 2899 | + { |
| 2900 | + "name": "allows-bypass-marker", |
| 2901 | + "input": { |
| 2902 | + "tool_input": { |
| 2903 | + "command": "gh api repos/o/r/issues/1/comments --jq '[.[] | select(.user.login | test(\"greptile\";\"i\"))] | sort_by(.created_at) | last' # hook-bypass: greptile-extraction-acknowledged" |
| 2904 | + } |
| 2905 | + }, |
| 2906 | + "expected_exit": 0 |
| 2907 | + } |
| 2908 | + ] |
2788 | 2909 | } |
2789 | 2910 | ] |
0 commit comments