browser_review_gate 0.1.1 → 0.1.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fc436627f031a3389801655161462b2da163e43679b3bc42b0856b28bd89b999
4
- data.tar.gz: e22daa7ab54df4bff78e8926d18bfd627e5e615e5209547f9363a32591d6ce71
3
+ metadata.gz: 1d796f7cca8b1df8cb9baf1dde44f2ce6c7417a97df23904d83f57ff1d2b6585
4
+ data.tar.gz: 2dca99945ced333c56b66906d0e5603e6c1f6c71b65557a8acfbd5c364bafcbc
5
5
  SHA512:
6
- metadata.gz: 36f2ef20d5d315c52fa483ce14e4279f6a2b16b798e71b9936a6c5946998e7e24def8c63e7d7b9408b6f156edbe382f02e38d509882a155d661ff618f38299eb
7
- data.tar.gz: 165540cabff36b17c1dadcfcc225f23051ae0b558d912bf9d762127844daebb30913c0f00abb579b9987ff7bce5ba8c23de2cd9332cceaf77106d7957d4c9cd7
6
+ metadata.gz: 16f2cac1e96bfaa0f6b2a7e2033e1375b358f3323bc9095282eb59f28718f94ec5a225b1ace02f8e3379266887972c16b9ec333c93046ba40967929181c0e996
7
+ data.tar.gz: 3731b22121fd1cd20d16f0af4f44f375247966e56a5faab65301def109784823de3b8ff01d047174ef19967ef559d4fcdbbc4d5457bbf28af29d64d19c8de657
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.2
4
+
5
+ - Nothing holds a command back any more. The before-command hooks of 0.1.1, which stopped
6
+ `gh pr create` and review requests while a run was owed, are removed: running the verification is
7
+ the author's choice, and only CI decides whether a review request stays. The installer deletes the
8
+ old hook entries; until then they do nothing.
9
+
3
10
  ## 0.1.1
4
11
 
5
12
  - Reviewers whose requests were taken back are asked again automatically once the run is published.
data/README.md CHANGED
@@ -84,7 +84,7 @@ Local commands need the `gh` CLI, signed in.
84
84
  - **Reviewers come back on their own.** A request taken back is remembered; once the run is published, the same people are asked again.
85
85
  - **A waiver for what cannot be run.** Someone other than the author adds the `browser-verification-waived` label and review proceeds without a run.
86
86
  - **A status next to the checks.** `browser-verification` is pending while a run is owed and green otherwise. Make it a required check if you want it to block merging.
87
- - **A reminder before the PR.** In Claude Code, Cursor and Codex, `gh pr create` and review requests are held with a message when a run is still owed. The agent runs the verification first, or repeats the command with `BROWSER_REVIEW_GATE_SKIP=1` when the person says to go ahead.
87
+ - **Nothing is forced.** Opening a PR never requires a run. The PR comment says when one is needed and how to do it; it only becomes necessary when the author wants a reviewer.
88
88
  - **The agent does not start your app.** It uses the one that is running and asks you to start it otherwise.
89
89
 
90
90
  ## What it does not do
@@ -16,7 +16,6 @@ module BrowserReviewGate
16
16
  save --report PATH keep results until the PR for this commit is opened
17
17
  request-assessment NUMBER ask CI to assess a PR that has no decision yet
18
18
  hook AGENT after a command: publish the saved run after `gh pr create`
19
- hook AGENT --before before a command: say when a browser run is still owed
20
19
 
21
20
  For CI
22
21
  ci NUMBER assess the PR and take back premature reviewer requests
@@ -118,13 +117,14 @@ module BrowserReviewGate
118
117
  0
119
118
  end
120
119
 
120
+ # Version 0.1.1 installed a `--before` hook that held commands back. It is gone; a project that
121
+ # still has the entry gets a silent no-op until the installer removes it.
121
122
  def hook
122
- before = !@argv.delete("--before").nil?
123
+ return 0 if @argv.delete("--before")
124
+
123
125
  agent = @argv.shift
124
- hook = Hook.new(publisher_factory: -> { Publisher.new(github: github, config: config) },
125
- status_factory: -> { Status.new(github: github, config: config) }, config: config)
126
- message = before ? hook.before(@stdin.read) : hook.after(@stdin.read)
127
- output = Hook.render(message, agent, before: before)
126
+ message = Hook.new(publisher_factory: -> { Publisher.new(github: github, config: config) }).after(@stdin.read)
127
+ output = Hook.render(message, agent)
128
128
  @stdout.puts output if output
129
129
  0
130
130
  end
@@ -1,27 +1,23 @@
1
1
  require "json"
2
2
 
3
3
  module BrowserReviewGate
4
- # Runs around an agent's shell commands.
5
- #
6
- # After a command that opened a PR, the browser run saved for this commit is published.
7
- # Before a command that opens a PR or asks for review, the agent is told when a browser run is still
8
- # owed, so the request is not made only to be taken back. It never fails the agent's session.
4
+ # Runs after an agent's shell command. When the command opened a PR, the browser run saved for this
5
+ # commit is published. It never holds a command back and never fails the agent: running the
6
+ # verification is the author's choice, and the gate on review requests lives in CI.
9
7
  class Hook
10
8
  OPEN_PR = "gh pr create"
11
- REVIEW_REQUEST = /gh pr (create|edit)\b.*(--reviewer|--add-reviewer|-r )|requested_reviewers/
12
- SKIP = "BROWSER_REVIEW_GATE_SKIP=1"
13
9
 
14
- def initialize(publisher_factory:, status_factory: nil, config: Config.new, shell: Shell.new)
10
+ def initialize(publisher_factory:, shell: Shell.new)
15
11
  @publisher_factory = publisher_factory
16
- @status_factory = status_factory
17
- @config = config
18
12
  @shell = shell
19
13
  end
20
14
 
21
15
  # `payload` is the raw hook input. Returns a message for the agent, or nil when nothing was done.
22
16
  def after(payload)
23
17
  return unless payload.to_s.include?(OPEN_PR)
24
- return unless saved_run?
18
+
19
+ sha = @shell.call("git", "rev-parse", "HEAD").strip
20
+ return unless SavedReport.new(sha: sha, shell: @shell).exist?
25
21
 
26
22
  verified = @publisher_factory.call.publish
27
23
  verified ? "Saved browser verification published; human review can be requested." :
@@ -30,58 +26,12 @@ module BrowserReviewGate
30
26
  "Saved browser verification was not published: #{error.message}"
31
27
  end
32
28
 
33
- # Returns the reason the command should wait, or nil to let it run.
34
- def before(payload)
35
- text = payload.to_s
36
- return if text.include?(SKIP)
37
-
38
- reason = review_request_reason(text) if text.match?(REVIEW_REQUEST)
39
- reason ||= open_pr_reason if text.include?(OPEN_PR)
40
- reason && "#{reason} If the person asked to go ahead anyway, run the same command prefixed with #{SKIP}."
41
- rescue Error
42
- nil
43
- end
44
-
45
- # Claude Code and Codex read a JSON object; Cursor reads its own shape.
46
- def self.render(message, agent, before: false)
29
+ # Claude Code and Codex read a JSON object; other agents show plain text.
30
+ def self.render(message, agent)
47
31
  return if message.nil?
48
- return JSON.generate(permission: "deny", agentMessage: message, userMessage: message) if before && agent == "cursor"
49
32
  return message unless %w[claude codex].include?(agent)
50
- return JSON.generate(hookSpecificOutput: { hookEventName: "PostToolUse", additionalContext: message }) unless before
51
-
52
- JSON.generate(hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: message })
53
- end
54
-
55
- private
56
-
57
- def saved_run?
58
- SavedReport.new(sha: @shell.call("git", "rev-parse", "HEAD").strip, shell: @shell).exist?
59
- end
60
-
61
- # No PR exists yet, so CI has not decided; only the changed paths can tell.
62
- def open_pr_reason
63
- return if saved_run? || changed_files.all? { |path| @config.ignored?(path) }
64
-
65
- "No browser run is saved for this commit, and the change may alter browser behavior. " \
66
- "Run #{@config.command} first: the run is published when the PR is opened and review requests stay."
67
- end
68
-
69
- def review_request_reason(text)
70
- status = @status_factory.call.to_h(text[/gh pr edit\s+(\d+)/, 1]&.to_i)
71
- case status["next"]
72
- when "publish_saved" then "A browser run is saved but not published. Run `browser-review-gate publish` first, or the review request will be taken back."
73
- when "run_and_publish", "ask_to_start_app" then "This PR needs a browser run (#{@config.command} #{status["pull_request"]}); a review request made now will be taken back."
74
- end
75
- end
76
33
 
77
- # What the PR would contain. When no base branch can be found the change counts as unknown.
78
- def changed_files
79
- %w[origin/HEAD origin/main origin/master].each do |base|
80
- return @shell.call("git", "diff", "--name-only", "#{base}...HEAD").lines.map(&:chomp)
81
- rescue Error
82
- next
83
- end
84
- [ "unknown" ]
34
+ JSON.generate(hookSpecificOutput: { hookEventName: "PostToolUse", additionalContext: message })
85
35
  end
86
36
  end
87
37
  end
@@ -110,25 +110,23 @@ module BrowserReviewGate
110
110
 
111
111
  def install_claude
112
112
  managed(".claude/skills/browser-pr-verification/SKILL.md", render("claude_skill.md"))
113
- add_hooks(".claude/settings.json", "claude") do |hooks, command, before|
114
- (hooks[before ? "PreToolUse" : "PostToolUse"] ||= []) <<
115
- { "matcher" => "Bash", "hooks" => [ { "type" => "command", "command" => command, "timeout" => 60 } ] }
113
+ add_hook(".claude/settings.json", "claude", %w[PreToolUse]) do |hooks, command|
114
+ (hooks["PostToolUse"] ||= []) << { "matcher" => "Bash", "hooks" => [ { "type" => "command", "command" => command, "timeout" => 60 } ] }
116
115
  end
117
116
  end
118
117
 
119
118
  def install_cursor
120
119
  managed(".cursor/commands/browser-pr-verification.md", render("cursor_command.md"))
121
- add_hooks(".cursor/hooks.json", "cursor", "version" => 1) do |hooks, command, before|
122
- (hooks[before ? "beforeShellExecution" : "afterShellExecution"] ||= []) << { "command" => command }
120
+ add_hook(".cursor/hooks.json", "cursor", %w[beforeShellExecution], "version" => 1) do |hooks, command|
121
+ (hooks["afterShellExecution"] ||= []) << { "command" => command }
123
122
  end
124
123
  end
125
124
 
126
125
  # Codex runs a project hook only after the person trusts it in /hooks, so the AGENTS.md section also
127
126
  # tells the agent to publish after it opens the PR.
128
127
  def install_codex
129
- add_hooks(".codex/hooks.json", "codex") do |hooks, command, before|
130
- (hooks[before ? "PreToolUse" : "PostToolUse"] ||= []) <<
131
- { "matcher" => "^Bash$", "hooks" => [ { "type" => "command", "command" => command, "timeout" => 60 } ] }
128
+ add_hook(".codex/hooks.json", "codex", %w[PreToolUse]) do |hooks, command|
129
+ (hooks["PostToolUse"] ||= []) << { "matcher" => "^Bash$", "hooks" => [ { "type" => "command", "command" => command, "timeout" => 60 } ] }
132
130
  end
133
131
  path = "AGENTS.md"
134
132
  current = exist?(path) ? File.read(File.join(@root, path)) : nil
@@ -139,8 +137,8 @@ module BrowserReviewGate
139
137
 
140
138
  # A machine without the gem (a cloud agent, a fresh container) must not see a failing hook after
141
139
  # every command.
142
- def hook_command(agent, before)
143
- "command -v browser-review-gate >/dev/null 2>&1 && #{HOOK} #{agent}#{" --before" if before} || true"
140
+ def hook_command(agent)
141
+ "command -v browser-review-gate >/dev/null 2>&1 && #{HOOK} #{agent} || true"
144
142
  end
145
143
 
146
144
  def render(name)
@@ -161,14 +159,22 @@ module BrowserReviewGate
161
159
  write(path, content, current)
162
160
  end
163
161
 
164
- # Adds the after-command and before-command hook entries the file does not have yet.
165
- def add_hooks(path, agent, defaults = {})
162
+ # Adds the after-command hook entry when the file lacks it, and removes the before-command entries
163
+ # version 0.1.1 put under `stale_events`: nothing holds a command back any more.
164
+ def add_hook(path, agent, stale_events, defaults = {})
166
165
  current = exist?(path) ? File.read(File.join(@root, path)) : nil
167
- missing = [ false, true ].reject { |before| current.to_s.match?(before ? /hook #{agent} --before/ : /hook #{agent}(?! --before)/) }
168
- return @log.puts(" unchanged #{path}") if missing.empty?
166
+ present = current.to_s.match?(/hook #{agent}(?! --before)/)
167
+ return @log.puts(" unchanged #{path}") if present && !current.include?("hook #{agent} --before")
169
168
 
170
169
  settings = defaults.merge(current ? JSON.parse(current) : {})
171
- missing.each { |before| yield(settings["hooks"] ||= {}, hook_command(agent, before), before) }
170
+ hooks = (settings["hooks"] ||= {})
171
+ stale_events.each do |event|
172
+ next unless hooks[event]
173
+
174
+ hooks[event].reject! { |entry| JSON.generate(entry).include?("hook #{agent} --before") }
175
+ hooks.delete(event) if hooks[event].empty?
176
+ end
177
+ yield(hooks, hook_command(agent)) unless present
172
178
  write(path, "#{JSON.pretty_generate(settings)}\n", current)
173
179
  end
174
180
 
@@ -1,3 +1,3 @@
1
1
  module BrowserReviewGate
2
- VERSION = "0.1.1"
2
+ VERSION = "0.1.2"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: browser_review_gate
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - JetRockets
@@ -66,7 +66,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
66
66
  - !ruby/object:Gem::Version
67
67
  version: '0'
68
68
  requirements: []
69
- rubygems_version: 4.0.10
69
+ rubygems_version: 4.0.1
70
70
  specification_version: 4
71
71
  summary: Keeps human review of a pull request behind a browser run of its browser-visible
72
72
  changes.