reanimator 0.1.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +207 -0
- data/LICENSE.txt +21 -0
- data/README.md +483 -0
- data/SECURITY.md +32 -0
- data/exe/reanimator +6 -0
- data/lib/reanimator/acceptance_criterion.rb +75 -0
- data/lib/reanimator/agent_connection.rb +62 -0
- data/lib/reanimator/agent_profile.rb +51 -0
- data/lib/reanimator/agents/adapter.rb +49 -0
- data/lib/reanimator/agents/capability.rb +29 -0
- data/lib/reanimator/agents/constraints.rb +57 -0
- data/lib/reanimator/agents/hosted.rb +38 -0
- data/lib/reanimator/agents/local.rb +46 -0
- data/lib/reanimator/agents/patch.rb +60 -0
- data/lib/reanimator/agents/repair_task.rb +51 -0
- data/lib/reanimator/agents/result.rb +42 -0
- data/lib/reanimator/agents/strategies/arithmetic_fix.rb +51 -0
- data/lib/reanimator/agents/strategies/base.rb +26 -0
- data/lib/reanimator/agents.rb +38 -0
- data/lib/reanimator/autonomy_level.rb +41 -0
- data/lib/reanimator/baseline.rb +106 -0
- data/lib/reanimator/baseline_runner.rb +81 -0
- data/lib/reanimator/bounded_command.rb +87 -0
- data/lib/reanimator/classifier.rb +89 -0
- data/lib/reanimator/cli.rb +273 -0
- data/lib/reanimator/cohort.rb +61 -0
- data/lib/reanimator/commissioning/fixture.rb +69 -0
- data/lib/reanimator/commissioning/report.rb +45 -0
- data/lib/reanimator/commissioning/test.rb +145 -0
- data/lib/reanimator/confidence.rb +66 -0
- data/lib/reanimator/configuration.rb +35 -0
- data/lib/reanimator/contract_file.rb +116 -0
- data/lib/reanimator/contract_snapshot.rb +188 -0
- data/lib/reanimator/criteria_stack.rb +91 -0
- data/lib/reanimator/criterion_result.rb +70 -0
- data/lib/reanimator/detectors/base.rb +49 -0
- data/lib/reanimator/detectors/bundler.rb +68 -0
- data/lib/reanimator/detectors/commands.rb +112 -0
- data/lib/reanimator/detectors/fork.rb +25 -0
- data/lib/reanimator/detectors/github_actions.rb +68 -0
- data/lib/reanimator/detectors/operational_proof.rb +48 -0
- data/lib/reanimator/detectors/rails_app.rb +51 -0
- data/lib/reanimator/detectors/relationships.rb +46 -0
- data/lib/reanimator/detectors/ruby_gem.rb +40 -0
- data/lib/reanimator/detectors/security_controls.rb +36 -0
- data/lib/reanimator/discovered_fact.rb +99 -0
- data/lib/reanimator/discovery.rb +48 -0
- data/lib/reanimator/discovery_result.rb +89 -0
- data/lib/reanimator/drift_detector.rb +227 -0
- data/lib/reanimator/drift_report.rb +86 -0
- data/lib/reanimator/effective_contract.rb +108 -0
- data/lib/reanimator/effective_policy_resolver.rb +116 -0
- data/lib/reanimator/errors.rb +21 -0
- data/lib/reanimator/evaluators/base.rb +55 -0
- data/lib/reanimator/evaluators/command.rb +34 -0
- data/lib/reanimator/evaluators/documentation_check.rb +25 -0
- data/lib/reanimator/evaluators/http_smoke.rb +19 -0
- data/lib/reanimator/evaluators/repository_integrity.rb +22 -0
- data/lib/reanimator/evaluators/security_evidence.rb +24 -0
- data/lib/reanimator/evaluators/support_matrix.rb +25 -0
- data/lib/reanimator/evaluators.rb +41 -0
- data/lib/reanimator/family/gate.rb +40 -0
- data/lib/reanimator/family/gate_result.rb +46 -0
- data/lib/reanimator/family/gate_runner.rb +91 -0
- data/lib/reanimator/family.rb +89 -0
- data/lib/reanimator/family_inference.rb +113 -0
- data/lib/reanimator/git.rb +72 -0
- data/lib/reanimator/isolation.rb +36 -0
- data/lib/reanimator/language_inference.rb +43 -0
- data/lib/reanimator/lifecycle_intent.rb +93 -0
- data/lib/reanimator/obligation.rb +35 -0
- data/lib/reanimator/permission_model.rb +61 -0
- data/lib/reanimator/portfolio.rb +68 -0
- data/lib/reanimator/portfolio_drift.rb +120 -0
- data/lib/reanimator/profile_inference.rb +37 -0
- data/lib/reanimator/profiles.rb +110 -0
- data/lib/reanimator/proof_of_life.rb +45 -0
- data/lib/reanimator/proposed_change.rb +55 -0
- data/lib/reanimator/provenance.rb +39 -0
- data/lib/reanimator/relationship.rb +62 -0
- data/lib/reanimator/repair_run.rb +146 -0
- data/lib/reanimator/reports/agents.rb +28 -0
- data/lib/reanimator/reports/baseline.rb +73 -0
- data/lib/reanimator/reports/commissioning.rb +48 -0
- data/lib/reanimator/reports/contract.rb +70 -0
- data/lib/reanimator/reports/discovery.rb +38 -0
- data/lib/reanimator/reports/drift.rb +63 -0
- data/lib/reanimator/reports/families.rb +41 -0
- data/lib/reanimator/reports/family_gates.rb +41 -0
- data/lib/reanimator/reports/github_check.rb +45 -0
- data/lib/reanimator/reports/inventory.rb +37 -0
- data/lib/reanimator/reports/permissions.rb +32 -0
- data/lib/reanimator/reports/portfolio_baseline.rb +68 -0
- data/lib/reanimator/reports/portfolio_drift.rb +107 -0
- data/lib/reanimator/reports/run.rb +52 -0
- data/lib/reanimator/reports/snapshot.rb +58 -0
- data/lib/reanimator/reports.rb +36 -0
- data/lib/reanimator/repository.rb +96 -0
- data/lib/reanimator/repository_metadata.rb +78 -0
- data/lib/reanimator/repository_state.rb +48 -0
- data/lib/reanimator/routing/decision.rb +35 -0
- data/lib/reanimator/routing/router.rb +122 -0
- data/lib/reanimator/run.rb +48 -0
- data/lib/reanimator/safety_floor.rb +81 -0
- data/lib/reanimator/security_control.rb +87 -0
- data/lib/reanimator/security_posture.rb +80 -0
- data/lib/reanimator/snapshot_store.rb +78 -0
- data/lib/reanimator/task_class.rb +63 -0
- data/lib/reanimator/version.rb +5 -0
- data/lib/reanimator/vitality_profile.rb +44 -0
- data/lib/reanimator/waiver.rb +74 -0
- data/lib/reanimator.rb +214 -0
- metadata +262 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reanimator
|
|
4
|
+
# A policy-constrained execution configuration for one provider, model,
|
|
5
|
+
# sandbox, tool set, and data-handling posture (specification section 02 and
|
|
6
|
+
# section 08). A profile is what commissioning proves *ready*, *degraded*,
|
|
7
|
+
# *incompatible*, or *unavailable* - the readiness verdict is set from a
|
|
8
|
+
# commissioning report, never assumed.
|
|
9
|
+
class AgentProfile
|
|
10
|
+
READINESS = %i[unknown ready degraded incompatible unavailable].freeze
|
|
11
|
+
|
|
12
|
+
attr_reader :connection, :model, :sandbox, :data_handling, :capabilities
|
|
13
|
+
attr_accessor :readiness
|
|
14
|
+
|
|
15
|
+
def initialize(connection:, model: nil, sandbox: :ephemeral, data_handling: nil,
|
|
16
|
+
capabilities: [], readiness: :unknown)
|
|
17
|
+
@connection = connection
|
|
18
|
+
@model = model || connection.models.first
|
|
19
|
+
@sandbox = sandbox
|
|
20
|
+
@data_handling = data_handling || connection.code_egress
|
|
21
|
+
@capabilities = capabilities
|
|
22
|
+
@readiness = readiness.to_sym
|
|
23
|
+
raise UnknownError, "Unknown readiness #{readiness.inspect}." unless READINESS.include?(@readiness)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Builds a profile straight from an adapter, seeding its capabilities.
|
|
27
|
+
def self.from_adapter(adapter, **overrides)
|
|
28
|
+
new(connection: adapter.connection, capabilities: adapter.capabilities, **overrides)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def supports?(capability) = capabilities.include?(capability.to_sym)
|
|
32
|
+
|
|
33
|
+
def provider = connection.provider
|
|
34
|
+
|
|
35
|
+
def ready? = readiness == :ready
|
|
36
|
+
|
|
37
|
+
# Two profiles share a data-handling policy when their egress posture
|
|
38
|
+
# matches - the precondition for any context transfer during fallback
|
|
39
|
+
# (section 08 fallback rules).
|
|
40
|
+
def same_data_handling?(other)
|
|
41
|
+
data_handling == other.data_handling
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def to_h
|
|
45
|
+
{
|
|
46
|
+
provider: provider, model: model, sandbox: sandbox,
|
|
47
|
+
data_handling: data_handling, readiness: readiness
|
|
48
|
+
}
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reanimator
|
|
4
|
+
module Agents
|
|
5
|
+
# The contract every coding-agent provider implements (specification
|
|
6
|
+
# section 08). Keeping this surface tiny is the whole point: swapping
|
|
7
|
+
# providers must never require redefining acceptance criteria, so an
|
|
8
|
+
# adapter only ever *enumerates capabilities* and *proposes a repair* - it
|
|
9
|
+
# does not grade, merge, or decide policy.
|
|
10
|
+
class Adapter
|
|
11
|
+
# @return [AgentConnection]
|
|
12
|
+
attr_reader :connection
|
|
13
|
+
|
|
14
|
+
def initialize(connection)
|
|
15
|
+
@connection = connection
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# @return [Symbol] provider name
|
|
19
|
+
def provider = connection.provider
|
|
20
|
+
|
|
21
|
+
# @return [Boolean] whether the adapter can actually run right now
|
|
22
|
+
# (credentials present and integration available)
|
|
23
|
+
def available?
|
|
24
|
+
raise NotImplementedError, "#{self.class} must implement #available?"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @return [Array<Symbol>] the {Capability} values this adapter offers
|
|
28
|
+
def capabilities
|
|
29
|
+
raise NotImplementedError, "#{self.class} must implement #capabilities"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @param capability [Symbol]
|
|
33
|
+
def supports?(capability) = capabilities.include?(capability.to_sym)
|
|
34
|
+
|
|
35
|
+
# The provider's code-egress posture, inherited from the connection.
|
|
36
|
+
def code_egress = connection.code_egress
|
|
37
|
+
|
|
38
|
+
# Attempt a repair. Returns an untrusted {Result}; never applies it.
|
|
39
|
+
# @param task [RepairTask]
|
|
40
|
+
# @return [Result]
|
|
41
|
+
def repair(task)
|
|
42
|
+
raise NotImplementedError, "#{self.class} must implement #repair"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @return [String, nil] why the adapter is unavailable, for reporting
|
|
46
|
+
def unavailable_reason = nil
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reanimator
|
|
4
|
+
module Agents
|
|
5
|
+
# The capabilities an adapter can declare and a task can require
|
|
6
|
+
# (specification section 08 commissioning step 1: "enumerate allowed
|
|
7
|
+
# capabilities"). Tools are capability-scoped and deny-by-default (section
|
|
8
|
+
# 12), so a task that needs a capability the profile does not offer is
|
|
9
|
+
# routed elsewhere or refused - never silently attempted.
|
|
10
|
+
module Capability
|
|
11
|
+
# Produce a structured remediation plan.
|
|
12
|
+
PLAN = :plan
|
|
13
|
+
# Produce a patch (a set of file edits).
|
|
14
|
+
PATCH = :patch
|
|
15
|
+
# Edit repository files in a workspace.
|
|
16
|
+
REPOSITORY_EDIT = :repository_edit
|
|
17
|
+
# Execute checks in a sandbox.
|
|
18
|
+
SANDBOX_EXEC = :sandbox_exec
|
|
19
|
+
# Reach the network (subject to egress policy).
|
|
20
|
+
NETWORK = :network
|
|
21
|
+
|
|
22
|
+
ALL = [PLAN, PATCH, REPOSITORY_EDIT, SANDBOX_EXEC, NETWORK].freeze
|
|
23
|
+
|
|
24
|
+
module_function
|
|
25
|
+
|
|
26
|
+
def valid?(capability) = ALL.include?(capability.to_sym)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reanimator
|
|
4
|
+
module Agents
|
|
5
|
+
# The immutable envelope a repair must stay inside: which paths it may edit,
|
|
6
|
+
# which are protected, what it may spend, and where code may travel.
|
|
7
|
+
#
|
|
8
|
+
# These four values always travel together, so they are one object rather
|
|
9
|
+
# than a hash handed from caller to caller - and the decision that depends
|
|
10
|
+
# on them ("may I edit this path?") lives here, with the data, instead of
|
|
11
|
+
# being re-derived by whoever happens to hold the hash.
|
|
12
|
+
class Constraints
|
|
13
|
+
attr_reader :allowed_paths, :protected_paths, :max_cost_usd, :code_egress
|
|
14
|
+
|
|
15
|
+
def initialize(allowed_paths: [], protected_paths: [], max_cost_usd: nil, code_egress: nil)
|
|
16
|
+
@allowed_paths = Array(allowed_paths)
|
|
17
|
+
@protected_paths = Array(protected_paths)
|
|
18
|
+
@max_cost_usd = max_cost_usd
|
|
19
|
+
@code_egress = code_egress
|
|
20
|
+
freeze
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Accepts either a Constraints or the plain Hash shape, so callers may
|
|
24
|
+
# pass whichever reads better at the call site.
|
|
25
|
+
#
|
|
26
|
+
# @param value [Constraints, Hash, nil]
|
|
27
|
+
# @return [Constraints]
|
|
28
|
+
def self.from(value)
|
|
29
|
+
return value if value.is_a?(self)
|
|
30
|
+
|
|
31
|
+
hash = value || {}
|
|
32
|
+
new(
|
|
33
|
+
allowed_paths: hash[:allowed_paths] || [],
|
|
34
|
+
protected_paths: hash[:protected_paths] || [],
|
|
35
|
+
max_cost_usd: hash[:max_cost_usd],
|
|
36
|
+
code_egress: hash[:code_egress]
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Whether a repository-relative path may be edited: never a protected
|
|
41
|
+
# path, and inside the allowed set whenever one is declared. Matching is
|
|
42
|
+
# depth-correct, so a protected `dir/**` cannot be evaded by nesting.
|
|
43
|
+
def may_edit?(path)
|
|
44
|
+
return false if SafetyFloor.protects?(path, protected_paths)
|
|
45
|
+
|
|
46
|
+
allowed_paths.empty? || allowed_paths.any? { |glob| SafetyFloor.matches?(glob, path) }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def to_h
|
|
50
|
+
{
|
|
51
|
+
allowed_paths: allowed_paths, protected_paths: protected_paths,
|
|
52
|
+
max_cost_usd: max_cost_usd, code_egress: code_egress
|
|
53
|
+
}.compact
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reanimator
|
|
4
|
+
module Agents
|
|
5
|
+
# A placeholder for hosted providers (Codex, Claude, Amp). It declares the
|
|
6
|
+
# capabilities such a provider would offer so routing and reports can reason
|
|
7
|
+
# about it, but it is honest that no hosted integration ships in this slice:
|
|
8
|
+
# it reports itself unavailable rather than pretending to run. A real
|
|
9
|
+
# adapter - potentially backed by the codenamev/agentic orchestrator -
|
|
10
|
+
# implements #repair by calling its model and returns the same {Result}.
|
|
11
|
+
class Hosted < Adapter
|
|
12
|
+
def available? = false
|
|
13
|
+
|
|
14
|
+
def capabilities
|
|
15
|
+
[Capability::PLAN, Capability::PATCH, Capability::REPOSITORY_EDIT,
|
|
16
|
+
Capability::SANDBOX_EXEC, Capability::NETWORK]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def credentialed?
|
|
20
|
+
ref = connection.credentials_ref
|
|
21
|
+
ref && !ENV[ref].to_s.empty?
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def unavailable_reason
|
|
25
|
+
if credentialed?
|
|
26
|
+
"Credentials are present, but the #{provider} hosted adapter is not implemented in this version."
|
|
27
|
+
else
|
|
28
|
+
"No credentials found#{" (set #{connection.credentials_ref})" if connection.credentials_ref}; " \
|
|
29
|
+
"connect the #{provider} provider first."
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def repair(_task)
|
|
34
|
+
raise Error, "The #{provider} adapter cannot run a repair: #{unavailable_reason}"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reanimator
|
|
4
|
+
module Agents
|
|
5
|
+
# The reference local agent: deterministic, no network, always available.
|
|
6
|
+
# It runs a list of rule-based {Strategies} and proposes the first patch
|
|
7
|
+
# that matches. Because it never reaches the network and keeps code on the
|
|
8
|
+
# user's runner, it satisfies the "local" security posture (section 12) and
|
|
9
|
+
# is the adapter commissioning can always prove end to end.
|
|
10
|
+
class Local < Adapter
|
|
11
|
+
DEFAULT_STRATEGIES = [Strategies::ArithmeticFix].freeze
|
|
12
|
+
|
|
13
|
+
def initialize(connection, strategies: nil)
|
|
14
|
+
super(connection)
|
|
15
|
+
@strategies = strategies || DEFAULT_STRATEGIES.map(&:new)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def available? = true
|
|
19
|
+
|
|
20
|
+
# Local execution never touches the network by design.
|
|
21
|
+
def capabilities
|
|
22
|
+
[Capability::PLAN, Capability::PATCH, Capability::REPOSITORY_EDIT, Capability::SANDBOX_EXEC]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def repair(task)
|
|
26
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
27
|
+
strategy = @strategies.find { |s| s.applies?(task) }
|
|
28
|
+
|
|
29
|
+
if strategy
|
|
30
|
+
plan, patch = strategy.call(task)
|
|
31
|
+
Result.new(plan: plan, patch: patch, cost_usd: 0.0,
|
|
32
|
+
latency_s: elapsed(started), tool_use: [Capability::REPOSITORY_EDIT, Capability::PATCH], network: [])
|
|
33
|
+
else
|
|
34
|
+
Result.new(plan: nil, patch: Patch.new, cost_usd: 0.0, latency_s: elapsed(started),
|
|
35
|
+
message: "No local strategy matched the objective #{task.objective.id.inspect}.")
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def elapsed(started)
|
|
42
|
+
(Process.clock_gettime(Process::CLOCK_MONOTONIC) - started).round(3)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Reanimator
|
|
6
|
+
module Agents
|
|
7
|
+
# A proposed change expressed as data, not as a mutation the agent performs
|
|
8
|
+
# itself. Representing a repair as a set of file edits is what lets the
|
|
9
|
+
# harness - never the agent - apply it into an isolated evaluation
|
|
10
|
+
# workspace (specification section 09: evaluation runs separately from the
|
|
11
|
+
# agent workspace). An agent proposes a Patch; Reanimator decides whether
|
|
12
|
+
# and where to apply it.
|
|
13
|
+
class Patch
|
|
14
|
+
# A single file's new contents (or deletion).
|
|
15
|
+
Edit = Struct.new(:path, :contents, :delete) do
|
|
16
|
+
def delete? = delete
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
attr_reader :edits
|
|
20
|
+
|
|
21
|
+
def initialize(edits = [])
|
|
22
|
+
@edits = edits
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.write(path, contents)
|
|
26
|
+
new([Edit.new(path: path, contents: contents, delete: false)])
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def empty? = edits.empty?
|
|
30
|
+
|
|
31
|
+
def paths = edits.map(&:path)
|
|
32
|
+
|
|
33
|
+
# Applies the patch beneath a root directory. The harness calls this on a
|
|
34
|
+
# disposable copy; agents never call it. Refuses to escape the root.
|
|
35
|
+
#
|
|
36
|
+
# @param root [String]
|
|
37
|
+
# @raise [PolicyViolation] when an edit path escapes the root
|
|
38
|
+
def apply(root)
|
|
39
|
+
edits.each do |edit|
|
|
40
|
+
full = File.expand_path(File.join(root, edit.path))
|
|
41
|
+
unless full.start_with?(File.expand_path(root) + File::SEPARATOR)
|
|
42
|
+
raise PolicyViolation, "Patch edit #{edit.path.inspect} escapes the workspace root."
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
if edit.delete?
|
|
46
|
+
File.delete(full) if File.exist?(full)
|
|
47
|
+
else
|
|
48
|
+
FileUtils.mkdir_p(File.dirname(full))
|
|
49
|
+
File.write(full, edit.contents)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
root
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def to_h
|
|
56
|
+
{edits: edits.map { |e| {path: e.path, delete: e.delete?} }}
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reanimator
|
|
4
|
+
module Agents
|
|
5
|
+
# The immutable objective handed to an agent for one repair (specification
|
|
6
|
+
# section 08 fallback rules and section 17 run ledger). Everything here is
|
|
7
|
+
# frozen: the agent receives the original task, the constraints it may not
|
|
8
|
+
# exceed, and *verified evidence* - never mutable conclusions it could talk
|
|
9
|
+
# itself out of. A fallback agent receives this very same object, not the
|
|
10
|
+
# previous agent's narrative.
|
|
11
|
+
class RepairTask
|
|
12
|
+
# @return [AcceptanceCriterion] the failing criterion to satisfy
|
|
13
|
+
attr_reader :objective
|
|
14
|
+
# @return [String] absolute path to the agent's isolated workspace copy
|
|
15
|
+
attr_reader :workspace
|
|
16
|
+
# @return [Constraints] the immutable envelope this repair must stay
|
|
17
|
+
# inside (allowed paths, protected paths, budget, code egress)
|
|
18
|
+
attr_reader :constraints
|
|
19
|
+
# @return [Baseline] the verified pre-repair evidence
|
|
20
|
+
attr_reader :evidence
|
|
21
|
+
|
|
22
|
+
def initialize(objective:, workspace:, evidence:, constraints: {})
|
|
23
|
+
@objective = objective
|
|
24
|
+
@workspace = workspace
|
|
25
|
+
@evidence = evidence
|
|
26
|
+
@constraints = Constraints.from(constraints)
|
|
27
|
+
freeze
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @return [String] a plain-language statement of the goal
|
|
31
|
+
def goal = objective.claim
|
|
32
|
+
|
|
33
|
+
# --- read-only workspace access for adapters -----------------------------
|
|
34
|
+
|
|
35
|
+
def read(relative)
|
|
36
|
+
full = File.join(workspace, relative)
|
|
37
|
+
File.read(full) if File.file?(full)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def glob(pattern)
|
|
41
|
+
Dir.glob(File.join(workspace, pattern))
|
|
42
|
+
.map { |absolute| absolute.delete_prefix("#{workspace}/") }
|
|
43
|
+
.sort
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Whether a path is inside the constraints' allowed set (and outside the
|
|
47
|
+
# protected set). Adapters should honor this; the harness enforces it.
|
|
48
|
+
def may_edit?(path) = constraints.may_edit?(path)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reanimator
|
|
4
|
+
module Agents
|
|
5
|
+
# What an adapter returns from a repair attempt (specification section 08
|
|
6
|
+
# commissioning steps 3 and 5, section 17 run ledger). A result is an
|
|
7
|
+
# untrusted *proposal*: a structured plan, a patch expressed as data, and
|
|
8
|
+
# the observable cost/latency/tool-use/network the harness records. It is
|
|
9
|
+
# never a verdict - Reanimator computes pass/fail itself.
|
|
10
|
+
class Result
|
|
11
|
+
attr_reader :plan, :patch, :cost_usd, :latency_s, :tool_use, :network, :message
|
|
12
|
+
|
|
13
|
+
def initialize(plan: nil, patch: Patch.new, cost_usd: 0.0, latency_s: 0.0,
|
|
14
|
+
tool_use: [], network: [], message: nil)
|
|
15
|
+
@plan = plan
|
|
16
|
+
@patch = patch
|
|
17
|
+
@cost_usd = cost_usd
|
|
18
|
+
@latency_s = latency_s
|
|
19
|
+
@tool_use = tool_use
|
|
20
|
+
@network = network
|
|
21
|
+
@message = message
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# A well-formed repair proposal has both a structured plan and a
|
|
25
|
+
# non-empty patch (section 08 step 3: "require a structured plan and
|
|
26
|
+
# patch").
|
|
27
|
+
def structured? = !plan.to_s.strip.empty? && !patch.empty?
|
|
28
|
+
|
|
29
|
+
def to_h
|
|
30
|
+
{
|
|
31
|
+
plan: plan,
|
|
32
|
+
patch: patch.to_h,
|
|
33
|
+
cost_usd: cost_usd,
|
|
34
|
+
latency_s: latency_s,
|
|
35
|
+
tool_use: tool_use,
|
|
36
|
+
network: network,
|
|
37
|
+
message: message
|
|
38
|
+
}.compact
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reanimator
|
|
4
|
+
module Agents
|
|
5
|
+
module Strategies
|
|
6
|
+
# A narrow but genuine automated fixer: when an addition helper (a method
|
|
7
|
+
# named add/sum/plus) subtracts its operands, rewrite the subtraction as
|
|
8
|
+
# an addition. It is intentionally small - its job is to exercise the full
|
|
9
|
+
# commissioning pipeline (plan, patch, isolated apply, independent
|
|
10
|
+
# grading) with a real, deterministic repair rather than a stubbed answer.
|
|
11
|
+
class ArithmeticFix < Base
|
|
12
|
+
DEF = /def\s+(?:add|sum|plus)\b/
|
|
13
|
+
SUBTRACTION = /\b([a-z_]\w*)\s*-\s*([a-z_]\w*)\b/
|
|
14
|
+
|
|
15
|
+
def applies?(task) = !locate(task).nil?
|
|
16
|
+
|
|
17
|
+
def call(task)
|
|
18
|
+
path, _source, fixed = locate(task)
|
|
19
|
+
plan = <<~PLAN.strip
|
|
20
|
+
Objective: #{task.goal}
|
|
21
|
+
1. Locate the addition helper that is subtracting its operands (#{path}).
|
|
22
|
+
2. Replace the subtraction with an addition.
|
|
23
|
+
3. Re-run the failing check to confirm the arithmetic is correct.
|
|
24
|
+
PLAN
|
|
25
|
+
[plan, Patch.write(path, fixed)]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
# Finds the first editable Ruby file whose addition helper subtracts,
|
|
31
|
+
# returning [path, original, fixed] or nil.
|
|
32
|
+
def locate(task)
|
|
33
|
+
task.glob("**/*.rb").each do |relative|
|
|
34
|
+
next unless task.may_edit?(relative)
|
|
35
|
+
|
|
36
|
+
source = task.read(relative)
|
|
37
|
+
next unless source&.match?(DEF)
|
|
38
|
+
|
|
39
|
+
index = source.index(DEF)
|
|
40
|
+
after = source[index..]
|
|
41
|
+
next unless after.match?(SUBTRACTION)
|
|
42
|
+
|
|
43
|
+
fixed = source[0...index] + after.sub(SUBTRACTION, '\1 + \2')
|
|
44
|
+
return [relative, source, fixed]
|
|
45
|
+
end
|
|
46
|
+
nil
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reanimator
|
|
4
|
+
module Agents
|
|
5
|
+
# Repair strategies for the local, rule-based agent. A "local agent" is a
|
|
6
|
+
# first-class provider in the specification (section 01: "Support Codex,
|
|
7
|
+
# Claude, Amp, local agents, and future providers"). Each strategy is a
|
|
8
|
+
# deterministic, no-network fixer that inspects a workspace and, when it
|
|
9
|
+
# recognizes a defect, proposes a plan and a patch.
|
|
10
|
+
module Strategies
|
|
11
|
+
class Base
|
|
12
|
+
# @param task [RepairTask]
|
|
13
|
+
# @return [Boolean] whether this strategy recognizes the defect
|
|
14
|
+
def applies?(task)
|
|
15
|
+
raise NotImplementedError, "#{self.class} must implement #applies?"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# @param task [RepairTask]
|
|
19
|
+
# @return [Array(String, Patch)] a plan and a patch
|
|
20
|
+
def call(task)
|
|
21
|
+
raise NotImplementedError, "#{self.class} must implement #call"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reanimator
|
|
4
|
+
# Coding-agent adapters. The agent is a replaceable life-force adapter
|
|
5
|
+
# (specification section 08): the repository's definition of life is defined
|
|
6
|
+
# by its contract, never by whichever provider happens to perform a repair.
|
|
7
|
+
# Every provider - Codex, Claude, Amp, or a local rule-based agent - speaks
|
|
8
|
+
# the same {Agents::Adapter} contract, so profiles are portable and the
|
|
9
|
+
# acceptance criteria never change when the provider does.
|
|
10
|
+
#
|
|
11
|
+
# This module is the registry and factory. It ships one fully working adapter
|
|
12
|
+
# ({Agents::Local}, a deterministic no-network agent) and stubs for hosted
|
|
13
|
+
# providers that honestly report themselves unavailable until credentials and
|
|
14
|
+
# a real integration exist.
|
|
15
|
+
module Agents
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
# Built-in adapters keyed by provider name.
|
|
19
|
+
def registry
|
|
20
|
+
{
|
|
21
|
+
local: Local,
|
|
22
|
+
codex: Hosted,
|
|
23
|
+
claude: Hosted,
|
|
24
|
+
amp: Hosted
|
|
25
|
+
}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Builds an adapter for a connection.
|
|
29
|
+
# @param connection [AgentConnection]
|
|
30
|
+
# @return [Agents::Adapter]
|
|
31
|
+
def for(connection)
|
|
32
|
+
klass = registry.fetch(connection.provider, Hosted)
|
|
33
|
+
klass.new(connection)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def providers = registry.keys
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reanimator
|
|
4
|
+
# One rung of the autonomy ladder (specification section 11): trust grows one
|
|
5
|
+
# bounded capability at a time, from read-only observation to narrowly bounded
|
|
6
|
+
# merge authority. Each rung names the behavior it allows and the GitHub
|
|
7
|
+
# access it requires, so a permission grant can always be tied to the exact
|
|
8
|
+
# action it enables.
|
|
9
|
+
class AutonomyLevel
|
|
10
|
+
attr_reader :key, :rank, :behavior, :github_access
|
|
11
|
+
|
|
12
|
+
def initialize(key:, rank:, behavior:, github_access:)
|
|
13
|
+
@key = key
|
|
14
|
+
@rank = rank
|
|
15
|
+
@behavior = behavior
|
|
16
|
+
@github_access = github_access
|
|
17
|
+
freeze
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def to_sym = key
|
|
21
|
+
|
|
22
|
+
# The ladder in ascending order of authority.
|
|
23
|
+
LADDER = [
|
|
24
|
+
[:observe, "Inventory, detect drift, and report.", "Metadata and read-only contents."],
|
|
25
|
+
[:diagnose, "Run sandboxed checks and produce plans.", "Read-only plus ephemeral execution."],
|
|
26
|
+
[:repair, "Create a bounded patch in an isolated workspace.", "No repository write required until submission."],
|
|
27
|
+
[:submit, "Create or update a branch and pull request.", "Contents and pull-request write for selected repositories."],
|
|
28
|
+
[:merge, "Merge explicitly eligible changes through branch rules.", "Merge capability only where GitHub and policy allow."],
|
|
29
|
+
[:release, "Publish packages or deploy.", "Not part of the initial product promise."]
|
|
30
|
+
].each_with_index.each_with_object({}) do |((key, behavior, access), index), all|
|
|
31
|
+
all[key] = new(key: key, rank: index, behavior: behavior, github_access: access)
|
|
32
|
+
end.freeze
|
|
33
|
+
|
|
34
|
+
def self.fetch(key)
|
|
35
|
+
LADDER.fetch(key.to_sym) { raise UnknownError, "Unknown autonomy level #{key.inspect}." }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @return [Array<Symbol>] the ladder keys in ascending authority
|
|
39
|
+
def self.keys = LADDER.keys
|
|
40
|
+
end
|
|
41
|
+
end
|