ace-git 0.23.1 → 0.25.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.
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Git
5
+ # Normalized evidence types produced by every provider implementation.
6
+ #
7
+ # Provider packages translate their CLI responses into exactly these
8
+ # shapes, so consumers can stay forge-neutral. All fields except the
9
+ # identifying ones may be nil when a provider cannot supply them.
10
+ #
11
+ # Pull request state values: :open, :merged, :closed. Issue state values:
12
+ # :open, :closed.
13
+ ProviderPullRequest = Data.define(
14
+ :server_name, :number, :title, :state, :head_ref, :base_ref,
15
+ :head_sha, :author, :url, :draft, :merged_at,
16
+ :head_repository_url, :base_repository_url, :merge_commit_sha
17
+ )
18
+
19
+ ProviderIssue = Data.define(:server_name, :number, :title, :state, :author, :url, :labels)
20
+
21
+ # Normalized check/CI evidence for one check run.
22
+ ProviderCheck = Data.define(:server_name, :name, :state, :conclusion, :url)
23
+
24
+ # Normalized repository evidence.
25
+ ProviderRepository = Data.define(:server_name, :full_name, :default_branch, :url)
26
+
27
+ # Exact pull request source/base identity used for idempotent create
28
+ # reconciliation, expected-head proof, and cleanup consent binding.
29
+ #
30
+ # `head_sha` is the exact head commit the identity was captured at; every
31
+ # mutation decision that depends on head state must compare against it.
32
+ ProviderPullRequestIdentity = Data.define(
33
+ :head_repository_url, :head_ref, :base_repository_url, :base_ref, :head_sha
34
+ ) do
35
+ # @return [Hash] plain, JSON-serializable representation
36
+ def to_h
37
+ {
38
+ head_repository_url: head_repository_url,
39
+ head_ref: head_ref,
40
+ base_repository_url: base_repository_url,
41
+ base_ref: base_ref,
42
+ head_sha: head_sha
43
+ }
44
+ end
45
+ end
46
+
47
+ # Normalized result of one provider lifecycle mutation (create, update,
48
+ # ready, merge). Receipts are immutable evidence: the operation performed,
49
+ # the pull request state observed afterwards, and — for creates only —
50
+ # whether the request created a new PR or reconciled to an existing one
51
+ # (`:created` / `:existing`; nil for non-create operations).
52
+ ProviderMutationReceipt = Data.define(:server_name, :operation, :pull_request, :idempotency) do
53
+ # @return [Hash] plain, JSON-serializable representation
54
+ def to_h
55
+ {
56
+ server_name: server_name,
57
+ operation: operation,
58
+ idempotency: idempotency,
59
+ pull_request: pull_request&.to_h
60
+ }
61
+ end
62
+ end
63
+
64
+ # Cleanup proof status vocabulary. Only :merged is confirmed remote merge
65
+ # proof; every other status retains the candidate. Provider transport
66
+ # failures (:offline, :authentication_error, :malformed) are distinct from
67
+ # object states and never relabel a failed lookup as merged.
68
+ CLEANUP_PROOF_STATUSES = %i[
69
+ no_pr open closed_unmerged merged offline authentication_error malformed
70
+ ].freeze
71
+
72
+ # Normalized provider proof about the pull request behind a cleanup
73
+ # candidate (worktree, local ref, or remote ref). `status` is one of
74
+ # {CLEANUP_PROOF_STATUSES}; pr/head fields are nil unless a PR was found.
75
+ ProviderCleanupProof = Data.define(
76
+ :server_name, :status, :pr_number, :pr_url, :head_sha, :merge_commit_sha
77
+ ) do
78
+ # @return [Boolean] true only for confirmed remote merge proof
79
+ def merged?
80
+ status == :merged
81
+ end
82
+
83
+ # @return [Hash] plain, JSON-serializable representation
84
+ def to_h
85
+ {
86
+ server_name: server_name,
87
+ status: status,
88
+ pr_number: pr_number,
89
+ pr_url: pr_url,
90
+ head_sha: head_sha,
91
+ merge_commit_sha: merge_commit_sha
92
+ }
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "errors"
4
+ require_relative "providers/base"
5
+ require_relative "providers/evidence"
6
+
7
+ module Ace
8
+ module Git
9
+ # Registry of provider implementations owned by provider packages.
10
+ #
11
+ # The core never executes provider CLIs. Provider packages register their
12
+ # contract implementation here when loaded; downstream consumers then
13
+ # obtain a provider via {Providers.for}.
14
+ #
15
+ # @example Registering (inside a provider package)
16
+ # Ace::Git::Providers.register(:acme, Ace::Git::Acme::Provider)
17
+ #
18
+ # @example Resolving (in consumers)
19
+ # server = Ace::Git::ServerRegistry.resolve_default
20
+ # provider = Ace::Git::Providers.for(server)
21
+ # pr = provider.pull_request(number: 25)
22
+ module Providers
23
+ @registry = {}
24
+ @mutex = Mutex.new
25
+
26
+ class << self
27
+ # Register a provider implementation for a provider type.
28
+ #
29
+ # @param type [Symbol, String] provider type (e.g. :acme)
30
+ # @param provider_class [Class] class implementing Providers::Base;
31
+ # must respond to .new
32
+ # @raise [ArgumentError] when type or provider_class is invalid
33
+ def register(type, provider_class)
34
+ key = normalize_type(type)
35
+ unless provider_class.respond_to?(:new)
36
+ raise ArgumentError, "Provider for #{key.inspect} must be a class (got #{provider_class.inspect})"
37
+ end
38
+
39
+ @mutex.synchronize { @registry[key] = provider_class }
40
+ provider_class
41
+ end
42
+
43
+ # Build the provider implementation for a resolved server.
44
+ #
45
+ # Provider packages register themselves when required. On a miss, the
46
+ # provider libraries listed in the `git.providers` configuration are
47
+ # required once (mono-repo default: both shipped provider gems) and
48
+ # the registry is re-checked; a still-missing provider is a classified
49
+ # failure, never a fallback.
50
+ #
51
+ # @param server [ResolvedServer] exactly-resolved server identity
52
+ # @param timeout [Integer, nil] provider operation timeout in seconds
53
+ # @param runner [Proc, nil] optional command runner injection for tests;
54
+ # see Providers::Base
55
+ # @return [Providers::Base] provider instance bound to the server
56
+ # @raise [UnknownProviderError] when no provider is registered for the
57
+ # server's provider type
58
+ def for(server, timeout: nil, runner: nil)
59
+ key = normalize_type(server.provider)
60
+ provider_class = @mutex.synchronize { @registry[key] }
61
+ unless provider_class
62
+ require_configured_providers
63
+ provider_class = @mutex.synchronize { @registry[key] }
64
+ end
65
+ unless provider_class
66
+ raise UnknownProviderError,
67
+ "No provider registered for type #{key.inspect} (server '#{server.name}'); " \
68
+ "install the provider package that owns #{key.inspect} and list it in git.providers"
69
+ end
70
+
71
+ provider_class.new(server: server, timeout: timeout, runner: runner)
72
+ end
73
+
74
+ # @param type [Symbol, String] provider type
75
+ # @return [Boolean] true when a provider is registered for the type
76
+ def registered?(type)
77
+ key = normalize_type(type)
78
+ @mutex.synchronize { @registry.key?(key) }
79
+ end
80
+
81
+ # @return [Array<Symbol>] registered provider types, sorted
82
+ def types
83
+ @mutex.synchronize { @registry.keys.sort }
84
+ end
85
+
86
+ # Clear all registrations (test seam).
87
+ def reset!
88
+ @mutex.synchronize do
89
+ @registry.clear
90
+ @required_configured = {}
91
+ end
92
+ end
93
+
94
+ private
95
+
96
+ def normalize_type(type)
97
+ normalized = type.to_s.strip.downcase
98
+ raise ArgumentError, "Provider type must be a non-empty symbol or string" if normalized.empty?
99
+
100
+ normalized.to_sym
101
+ end
102
+
103
+ # Require the provider libraries configured under `git.providers`
104
+ # (e.g. "github" -> ace/git/github). Load failures are ignored here;
105
+ # a missing provider surfaces as UnknownProviderError at resolution.
106
+ def require_configured_providers
107
+ @required_configured ||= {}
108
+ Array(Ace::Git.config["providers"]).each do |name|
109
+ library = name.to_s.strip.downcase
110
+ next if library.empty? || @required_configured[library]
111
+
112
+ require "ace/git/#{library}"
113
+ @required_configured[library] = true
114
+ rescue LoadError
115
+ @required_configured[library] = true
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Git
5
+ # An exactly-resolved forge server identity.
6
+ #
7
+ # Produced by ServerRegistry (by explicit name, by default resolution, or by
8
+ # remote URL matching). Carries the configured server name, the provider
9
+ # type that owns its behavior, and the configured base URL.
10
+ #
11
+ # @example
12
+ # ResolvedServer.new(name: "forge-lab", provider: :acme, url: "https://forge.example.com")
13
+ ResolvedServer = Data.define(:name, :provider, :url) do
14
+ # @return [Hash] Plain hash representation
15
+ def to_h
16
+ {name: name, provider: provider, url: url}
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,202 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "atoms/command_executor"
4
+ require_relative "atoms/server_url"
5
+ require_relative "errors"
6
+ require_relative "resolved_server"
7
+
8
+ module Ace
9
+ module Git
10
+ # Resolves forge server identity from configuration, deterministically.
11
+ #
12
+ # Configuration lives under the `git.servers` key (see
13
+ # `.ace-defaults/git/config.yml`). Each entry is a uniquely named server:
14
+ #
15
+ # ```yaml
16
+ # servers:
17
+ # - name: forge-lab
18
+ # provider: acme
19
+ # url: https://forge.example.com/owner/repo
20
+ # default: true
21
+ # - name: forge-mirror
22
+ # provider: other
23
+ # url: https://other.example.com/owner/repo
24
+ # ```
25
+ #
26
+ # At most one server may be marked `default: true`. Resolution never falls
27
+ # back silently: every failure raises a classified error from the failure
28
+ # taxonomy.
29
+ class ServerRegistry
30
+ # Internal configured entry: resolved identity plus its default flag.
31
+ Entry = Data.define(:server, :default)
32
+
33
+ class << self
34
+ # All configured servers, in configuration order.
35
+ #
36
+ # @return [Array<ResolvedServer>]
37
+ # @raise [DuplicateServerNameError] when two servers share a name
38
+ # @raise [ConfigError] when an entry is malformed
39
+ def servers
40
+ entries.map(&:server)
41
+ end
42
+
43
+ # Resolve a server by its explicit, configured name.
44
+ #
45
+ # @param name [String, Symbol] configured server name
46
+ # @return [ResolvedServer]
47
+ # @raise [UnknownServerNameError] when no server has that name
48
+ def resolve(name)
49
+ wanted = name.to_s
50
+ entries.map(&:server).find { |server| server.name == wanted } ||
51
+ raise(UnknownServerNameError, "No server named '#{wanted}' is configured " \
52
+ "(configured: #{servers.map(&:name).join(", ")}); add it to git.servers")
53
+ end
54
+
55
+ # Resolve the explicitly configured default server.
56
+ #
57
+ # @return [ResolvedServer]
58
+ # @raise [NoDefaultServerConfiguredError] when zero servers are default
59
+ # @raise [MultipleDefaultServersError] when more than one is default
60
+ def resolve_default
61
+ defaults = entries.select(&:default)
62
+ if defaults.empty?
63
+ raise NoDefaultServerConfiguredError,
64
+ "No default server configured; mark exactly one entry in git.servers with default: true"
65
+ end
66
+ if defaults.size > 1
67
+ raise MultipleDefaultServersError,
68
+ "Multiple servers marked as default: #{defaults.map { |e| e.server.name }.join(", ")}; " \
69
+ "keep exactly one"
70
+ end
71
+
72
+ defaults.first.server
73
+ end
74
+
75
+ # Resolve server identity from a git remote URL, matching configured
76
+ # servers deterministically (no hostname assumptions).
77
+ #
78
+ # @param remote_name [String, nil] git remote name (default: configured
79
+ # `remote`, then "origin")
80
+ # @return [ResolvedServer] the single matching configured server
81
+ # @raise [AmbiguousRemoteError] when the remote matches no configured
82
+ # server, or more than one
83
+ def resolve_remote(remote_name = nil)
84
+ remote_name ||= Ace::Git.config["remote"] || "origin"
85
+ remote_url = read_remote_url(remote_name)
86
+ matches = entries.map(&:server).select { |server| Atoms::ServerUrl.match?(server.url, remote_url) }
87
+
88
+ if matches.empty?
89
+ raise AmbiguousRemoteError,
90
+ "Remote '#{remote_name}' (#{remote_url || "missing"}) matches no configured server " \
91
+ "(configured: #{servers.map(&:name).join(", ")})"
92
+ end
93
+ if matches.size > 1
94
+ raise AmbiguousRemoteError,
95
+ "Remote '#{remote_name}' (#{remote_url}) matches multiple configured servers: " \
96
+ "#{matches.map(&:name).join(", ")}; make server URLs distinct"
97
+ end
98
+
99
+ matches.first
100
+ end
101
+
102
+ # Resolve exactly one server for an operation from CLI-style selection.
103
+ #
104
+ # @param server_name [String, Symbol, nil] explicit configured server name
105
+ # @param use_default [Boolean] resolve the configured default server
106
+ # @param remote_name [String, nil] remote used when nothing is selected
107
+ # @return [ResolvedServer]
108
+ # @raise [ConfigError] when explicit selection and default conflict
109
+ # @raise [UnknownServerNameError, NoDefaultServerConfiguredError,
110
+ # MultipleDefaultServersError, AmbiguousRemoteError] per resolution
111
+ def resolve_for(server_name: nil, use_default: false, remote_name: nil)
112
+ if server_name && use_default
113
+ raise ConfigError,
114
+ "Server selection is ambiguous: an explicit server ('#{server_name}') and " \
115
+ "the default-server request are mutually exclusive"
116
+ end
117
+
118
+ if server_name
119
+ resolve(server_name)
120
+ elsif use_default
121
+ resolve_default
122
+ else
123
+ resolve_remote(remote_name)
124
+ end
125
+ end
126
+
127
+ # Configured servers whose URL matches the repository URL exactly.
128
+ #
129
+ # @param repository_url [String] repository URL in any supported shape
130
+ # @return [Array<ResolvedServer>] matching servers (may be empty)
131
+ def matching_servers(repository_url)
132
+ entries.map(&:server).select { |server| Atoms::ServerUrl.match?(server.url, repository_url) }
133
+ end
134
+
135
+ # Configured servers whose repository path equals `owner/repo`
136
+ # (any host). Never assumes a forge hostname.
137
+ #
138
+ # @param owner_repo [String] "owner/repo"
139
+ # @return [Array<ResolvedServer>] matching servers (may be empty)
140
+ def servers_for_owner_repo(owner_repo)
141
+ wanted = owner_repo.to_s.downcase
142
+ entries.map(&:server).select do |server|
143
+ path = Atoms::ServerUrl.normalize(server.url).to_s.split("/", 2)[1]
144
+ path == wanted
145
+ end
146
+ end
147
+
148
+ private
149
+
150
+ # Validated configured entries, in configuration order.
151
+ def entries
152
+ raw = Ace::Git.config["servers"]
153
+ return [] if raw.nil?
154
+
155
+ raise ConfigError, "servers config must be a list" unless raw.is_a?(Array)
156
+
157
+ list = raw.map { |entry| build_entry(entry) }
158
+ assert_unique_names!(list)
159
+ list
160
+ end
161
+
162
+ # Read a remote URL with local git only (no network, no forge CLI).
163
+ def read_remote_url(remote_name)
164
+ result = Atoms::CommandExecutor.execute("git", "remote", "get-url", remote_name.to_s)
165
+ result[:success] ? result[:output].to_s.strip : nil
166
+ end
167
+
168
+ # Build one internal entry from a config entry, validating structure.
169
+ def build_entry(entry)
170
+ unless entry.is_a?(Hash)
171
+ raise ConfigError, "Each git.servers entry must be a mapping with name, provider, url"
172
+ end
173
+
174
+ normalized = entry.transform_keys(&:to_s)
175
+ %w[name provider url].each do |key|
176
+ next unless normalized[key].nil? || normalized[key].to_s.empty?
177
+
178
+ raise ConfigError, "git.servers entry is missing '#{key}': #{entry.inspect}"
179
+ end
180
+
181
+ Entry.new(
182
+ server: ResolvedServer.new(
183
+ name: normalized["name"].to_s,
184
+ provider: normalized["provider"].to_s.to_sym,
185
+ url: normalized["url"].to_s
186
+ ),
187
+ default: normalized["default"] == true || normalized["default"].to_s == "true"
188
+ )
189
+ end
190
+
191
+ def assert_unique_names!(list)
192
+ names = list.map { |entry| entry.server.name }
193
+ duplicates = names.tally.select { |_name, count| count > 1 }.keys
194
+ return if duplicates.empty?
195
+
196
+ raise DuplicateServerNameError,
197
+ "Duplicate server names in git.servers: #{duplicates.join(", ")}; server names must be unique"
198
+ end
199
+ end
200
+ end
201
+ end
202
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ace
4
4
  module Git
5
- VERSION = "0.23.1"
5
+ VERSION = "0.25.0"
6
6
  end
7
7
  end
data/lib/ace/git.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "ace/support/config"
4
4
  require_relative "git/version"
5
+ require_relative "git/errors"
5
6
 
6
7
  module Ace
7
8
  module Git
@@ -12,14 +13,9 @@ module Ace
12
13
  # - Organisms: Orchestrate molecules, propagate or wrap exceptions
13
14
  # - Commands: Catch exceptions and return exit codes (0=success, 1=error)
14
15
  #
15
- # All custom exceptions inherit from Ace::Git::Error for consistent catching.
16
- class Error < StandardError; end
17
- class GitError < Error; end
18
- class ConfigError < Error; end
19
- class GhNotInstalledError < Error; end
20
- class GhAuthenticationError < Error; end
21
- class PrNotFoundError < Error; end
22
- class TimeoutError < Error; end
16
+ # All custom exceptions inherit from Ace::Git::Error for consistent catching;
17
+ # they are defined in git/errors.rb (local git failures plus the forge server
18
+ # resolution and provider interaction taxonomy).
23
19
 
24
20
  # Mutex for thread-safe config initialization
25
21
  @config_mutex = Mutex.new
@@ -124,7 +120,7 @@ module Ace
124
120
  normalized = normalize_keys(git_section)
125
121
 
126
122
  # Copy top-level settings
127
- %w[default_branch remote verbose timeout network_timeout].each do |key|
123
+ %w[default_branch remote verbose timeout network_timeout servers providers].each do |key|
128
124
  config[key] = normalized[key] if normalized.key?(key)
129
125
  end
130
126
 
@@ -170,7 +166,7 @@ module Ace
170
166
  config["timeout"] || 30
171
167
  end
172
168
 
173
- # Timeout for network operations (gh CLI, remote operations)
169
+ # Timeout for network operations (provider CLIs, remote operations)
174
170
  # @return [Integer] Timeout in seconds (default: 60)
175
171
  def self.network_timeout
176
172
  config["network_timeout"] || 60
@@ -198,6 +194,9 @@ end
198
194
 
199
195
  # Require ATOM architecture components
200
196
  require_relative "git/atoms/command_executor"
197
+ require_relative "git/atoms/server_url"
198
+ require_relative "git/atoms/pr_reference"
199
+ require_relative "git/atoms/pr_identifier"
201
200
  require_relative "git/atoms/pattern_filter"
202
201
  require_relative "git/atoms/diff_parser"
203
202
  require_relative "git/atoms/diff_numstat_parser"
@@ -205,7 +204,6 @@ require_relative "git/atoms/file_grouper"
205
204
  require_relative "git/atoms/grouped_stats_formatter"
206
205
  require_relative "git/atoms/date_resolver"
207
206
  require_relative "git/atoms/task_pattern_extractor"
208
- require_relative "git/atoms/pr_identifier_parser"
209
207
  require_relative "git/atoms/repository_state_detector"
210
208
  require_relative "git/atoms/repository_checker"
211
209
  require_relative "git/atoms/git_scope_filter"
@@ -218,14 +216,17 @@ require_relative "git/molecules/diff_generator"
218
216
  require_relative "git/molecules/config_loader"
219
217
  require_relative "git/molecules/diff_filter"
220
218
  require_relative "git/molecules/branch_reader"
221
- require_relative "git/molecules/pr_metadata_fetcher"
222
219
  require_relative "git/molecules/recent_commits_fetcher"
223
220
  require_relative "git/molecules/git_status_fetcher"
224
- require_relative "git/molecules/gh_cli_executor"
225
- require_relative "git/molecules/github_issue_sync"
226
221
 
227
222
  require_relative "git/organisms/diff_orchestrator"
228
223
  require_relative "git/organisms/repo_status_loader"
224
+ require_relative "git/organisms/pull_request_lifecycle"
225
+ require_relative "git/resolved_server"
226
+ require_relative "git/server_registry"
227
+ require_relative "git/providers/base"
228
+ require_relative "git/providers/evidence"
229
+ require_relative "git/providers"
229
230
 
230
231
  require_relative "git/models/diff_result"
231
232
  require_relative "git/models/diff_config"
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ace-git
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.23.1
4
+ version: 0.25.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michal Czyz
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-09-05 00:00:00.000000000 Z
10
+ date: 2026-09-29 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: ace-support-config
@@ -94,8 +94,8 @@ dependencies:
94
94
  - !ruby/object:Gem::Version
95
95
  version: '13.0'
96
96
  description: ace-git gives developers and coding agents focused git context commands
97
- and guided workflows for rebases, pull requests, and commit reorganization, with
98
- smart diff output and Git 2.23+ guardrails.
97
+ and guided workflows for rebases and commit reorganization, with smart diff output,
98
+ a forge server registry, and a provider contract backed by pluggable provider gems.
99
99
  email:
100
100
  - mc@cs3b.com
101
101
  executables:
@@ -149,9 +149,11 @@ files:
149
149
  - lib/ace/git/atoms/grouped_stats_formatter.rb
150
150
  - lib/ace/git/atoms/lock_error_detector.rb
151
151
  - lib/ace/git/atoms/pattern_filter.rb
152
- - lib/ace/git/atoms/pr_identifier_parser.rb
152
+ - lib/ace/git/atoms/pr_identifier.rb
153
+ - lib/ace/git/atoms/pr_reference.rb
153
154
  - lib/ace/git/atoms/repository_checker.rb
154
155
  - lib/ace/git/atoms/repository_state_detector.rb
156
+ - lib/ace/git/atoms/server_url.rb
155
157
  - lib/ace/git/atoms/stale_lock_cleaner.rb
156
158
  - lib/ace/git/atoms/status_formatter.rb
157
159
  - lib/ace/git/atoms/task_pattern_extractor.rb
@@ -161,6 +163,7 @@ files:
161
163
  - lib/ace/git/cli/commands/diff.rb
162
164
  - lib/ace/git/cli/commands/pr.rb
163
165
  - lib/ace/git/cli/commands/status.rb
166
+ - lib/ace/git/errors.rb
164
167
  - lib/ace/git/models/diff_config.rb
165
168
  - lib/ace/git/models/diff_result.rb
166
169
  - lib/ace/git/models/repo_status.rb
@@ -168,22 +171,25 @@ files:
168
171
  - lib/ace/git/molecules/config_loader.rb
169
172
  - lib/ace/git/molecules/diff_filter.rb
170
173
  - lib/ace/git/molecules/diff_generator.rb
171
- - lib/ace/git/molecules/gh_cli_executor.rb
172
174
  - lib/ace/git/molecules/git_status_fetcher.rb
173
- - lib/ace/git/molecules/github_issue_sync.rb
174
- - lib/ace/git/molecules/pr_metadata_fetcher.rb
175
175
  - lib/ace/git/molecules/recent_commits_fetcher.rb
176
176
  - lib/ace/git/organisms/diff_orchestrator.rb
177
+ - lib/ace/git/organisms/pull_request_lifecycle.rb
177
178
  - lib/ace/git/organisms/repo_status_loader.rb
179
+ - lib/ace/git/providers.rb
180
+ - lib/ace/git/providers/base.rb
181
+ - lib/ace/git/providers/evidence.rb
182
+ - lib/ace/git/resolved_server.rb
183
+ - lib/ace/git/server_registry.rb
178
184
  - lib/ace/git/version.rb
179
- homepage: https://github.com/cs3b/ace/tree/main/ace-git
185
+ homepage: https://forgejo.tail6c0887.ts.net/cs3b/ace
180
186
  licenses:
181
187
  - MIT
182
188
  metadata:
183
189
  allowed_push_host: https://rubygems.org
184
- homepage_uri: https://github.com/cs3b/ace/tree/main/ace-git
185
- source_code_uri: https://github.com/cs3b/ace/tree/main/ace-git/
186
- changelog_uri: https://github.com/cs3b/ace/blob/main/ace-git/CHANGELOG.md
190
+ homepage_uri: https://forgejo.tail6c0887.ts.net/cs3b/ace
191
+ source_code_uri: https://forgejo.tail6c0887.ts.net/cs3b/ace/src/branch/main/ace-git
192
+ changelog_uri: https://forgejo.tail6c0887.ts.net/cs3b/ace/src/branch/main/ace-git/CHANGELOG.md
187
193
  rdoc_options: []
188
194
  require_paths:
189
195
  - lib
@@ -200,5 +206,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
200
206
  requirements: []
201
207
  rubygems_version: 3.6.9
202
208
  specification_version: 4
203
- summary: Git workflows and context commands for developers and AI agents
209
+ summary: Forge-neutral Git workflows and context commands for developers and AI agents
204
210
  test_files: []