pwn 0.5.647 → 0.5.650
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 +4 -4
- data/documentation/Agent-Tool-Registry.md +10 -5
- data/documentation/Home.md +1 -1
- data/documentation/How-PWN-Works.md +1 -1
- data/documentation/Reinforcement-Learning.md +36 -1
- data/documentation/What-is-PWN.md +1 -1
- data/documentation/pwn-ai-Agent.md +1 -1
- data/lib/pwn/ai/agent/curriculum.rb +125 -5
- data/lib/pwn/ai/agent/extrospection.rb +18 -4
- data/lib/pwn/ai/agent/learning.rb +171 -63
- data/lib/pwn/ai/agent/loop.rb +72 -9
- data/lib/pwn/ai/agent/metrics.rb +69 -10
- data/lib/pwn/ai/agent/registry.rb +21 -2
- data/lib/pwn/ai/agent/reward.rb +171 -5
- data/lib/pwn/ai/agent/tools/curriculum.rb +23 -0
- data/lib/pwn/ai/agent/tools/reward.rb +20 -0
- data/lib/pwn/ai/agent/tools/ruby_eval.rb +29 -6
- data/lib/pwn/version.rb +1 -1
- data/spec/integration/reinforced_feedback_loop_spec.rb +299 -0
- data/spec/lib/pwn/ai/agent/tools/ruby_eval_spec.rb +39 -1
- data/third_party/pwn_rdoc.jsonl +9 -2
- metadata +1 -1
data/lib/pwn/ai/agent/reward.rb
CHANGED
|
@@ -121,6 +121,40 @@ module PWN
|
|
|
121
121
|
v = llm_judge(request: request, final: final, trace: trace)
|
|
122
122
|
v ||= heuristic_judge(request: request, final: final, trace: trace)
|
|
123
123
|
|
|
124
|
+
# P1 — local/heuristic calibration: thin judges must not be treated
|
|
125
|
+
# as ground truth when proxy_distrust is already high. Two levers:
|
|
126
|
+
# (1) low :confidence so Metrics.effective_rate haircuts blend
|
|
127
|
+
# weight (distrust × confidence) instead of replacing proxy;
|
|
128
|
+
# (2) score-path caps only for decisive failure floors and for
|
|
129
|
+
# local no-trace highs (false "solved"). Do NOT pull known
|
|
130
|
+
# wrong (0.0 failure-language) toward 0.5, and do NOT deflate
|
|
131
|
+
# tool-backed heuristic scores that already cleared the bar —
|
|
132
|
+
# confidence handles that in the bandit blend.
|
|
133
|
+
eng = (PWN::Env.dig(:ai, :active) if defined?(PWN::Env)).to_s.downcase
|
|
134
|
+
local = eng == 'ollama' || eng.empty?
|
|
135
|
+
if v[:source].to_s == 'heuristic'
|
|
136
|
+
v[:confidence] = local ? 0.35 : 0.5
|
|
137
|
+
raw = v[:score].to_f
|
|
138
|
+
v[:score_raw] = raw
|
|
139
|
+
# preserve empty / failure-language / polite floors exactly (raw <= 0.15)
|
|
140
|
+
# and pass-through non-capped heuristics via the default arm.
|
|
141
|
+
if local && raw > 0.15 && trace.empty? && raw >= 0.6 && final.length < 400
|
|
142
|
+
v[:score] = [raw, 0.45].min
|
|
143
|
+
v[:rationale] = "#{v[:rationale]} | P1:local_no_trace_cap"
|
|
144
|
+
elsif local && raw > 0.15 && trace.length < 2 && raw >= 0.85
|
|
145
|
+
# thin-evidence local highs: mild shrink toward 0.5
|
|
146
|
+
v[:score] = (0.5 + ((raw - 0.5) * 0.7)).round(3).clamp(0.0, 1.0)
|
|
147
|
+
else
|
|
148
|
+
v[:score] = raw
|
|
149
|
+
end
|
|
150
|
+
v[:verdict] = if v[:score] >= 0.6 then :solved
|
|
151
|
+
elsif v[:score] >= 0.3 then :partial
|
|
152
|
+
else :wrong
|
|
153
|
+
end
|
|
154
|
+
else
|
|
155
|
+
v[:confidence] ||= 0.85
|
|
156
|
+
end
|
|
157
|
+
|
|
124
158
|
ground = verify_as_reward(final: final)
|
|
125
159
|
unless ground.nil?
|
|
126
160
|
# Ground-truth override: a browser-refuted claim caps score at
|
|
@@ -129,13 +163,17 @@ module PWN
|
|
|
129
163
|
v[:score] = [v[:score], 0.2].min if ground[:verdict] == :refuted
|
|
130
164
|
v[:score] = [v[:score], 0.6].max if ground[:verdict] == :confirmed
|
|
131
165
|
v[:grounded] = ground
|
|
166
|
+
v[:confidence] = [v[:confidence].to_f, ground[:confidence].to_f].max if ground[:confidence]
|
|
132
167
|
end
|
|
133
168
|
|
|
134
169
|
v[:success] = v[:score] >= 0.6
|
|
135
|
-
|
|
170
|
+
v[:engine] = eng
|
|
171
|
+
# P1 — sentinel stores confidence so distrust math can haircut
|
|
172
|
+
# heuristic-heavy windows differently from LLM ORM windows.
|
|
173
|
+
record_sentinel(proxy: opts[:proxy_ok], judge: v[:score], confidence: v[:confidence]) if commit
|
|
136
174
|
v
|
|
137
175
|
rescue StandardError => e
|
|
138
|
-
{ score: 0.5, verdict: :unknown, rationale: "judge error: #{e.class}", success: !final.strip.empty?, error: e.message }
|
|
176
|
+
{ score: 0.5, verdict: :unknown, rationale: "judge error: #{e.class}", success: !final.strip.empty?, error: e.message, confidence: 0.2, source: :error }
|
|
139
177
|
end
|
|
140
178
|
|
|
141
179
|
# ----------------------------------------------------------------
|
|
@@ -429,6 +467,14 @@ module PWN
|
|
|
429
467
|
claim = final[Learning::CLAIM_RX] if defined?(Learning)
|
|
430
468
|
return nil if claim.to_s.empty?
|
|
431
469
|
|
|
470
|
+
# P26 — drop metric crumbs ("cap 0.2") that match loose patterns
|
|
471
|
+
if defined?(Learning) && Learning.respond_to?(:checkable_claim?, true)
|
|
472
|
+
return nil unless Learning.send(:checkable_claim?, claim: claim)
|
|
473
|
+
elsif claim.match?(/\A(?:cap|share|proxy|judge|success|only|now|gap|score|rate)\b/i) ||
|
|
474
|
+
(claim.match?(/\d+\.\d+/) && !claim.match?(/\d+\.\d+\.\d+|CVE-/i))
|
|
475
|
+
return nil
|
|
476
|
+
end
|
|
477
|
+
|
|
432
478
|
# 1.5 — sampled E3: always when flag true; never when false;
|
|
433
479
|
# nil/auto → always on frontier, ~10% on local when CLAIM_RX hits.
|
|
434
480
|
flag = agent_flag(key: :verify_as_reward, default: nil)
|
|
@@ -470,6 +516,19 @@ module PWN
|
|
|
470
516
|
WRITE_SOURCE_CAP = 0.40
|
|
471
517
|
WRITE_SOURCE_WINDOW = 100
|
|
472
518
|
|
|
519
|
+
# P0 — target online generator mix for W1. Gates alone cannot fill
|
|
520
|
+
# an empty promote; the controller must *prefer underfilled* sources
|
|
521
|
+
# (counterfactual / critic / curriculum / user_correction) when
|
|
522
|
+
# resolve already dominates. Shares are soft targets, not hard caps
|
|
523
|
+
# (hard cap remains WRITE_SOURCE_CAP). Trajectory-only still applies.
|
|
524
|
+
TARGET_SOURCE_MIX = {
|
|
525
|
+
'mistakes_resolve' => 0.30,
|
|
526
|
+
'curriculum' => 0.25,
|
|
527
|
+
'counterfactual' => 0.20,
|
|
528
|
+
'critic' => 0.15,
|
|
529
|
+
'user_correction' => 0.10
|
|
530
|
+
}.freeze
|
|
531
|
+
|
|
473
532
|
public_class_method def self.record_preference(opts = {})
|
|
474
533
|
prompt = opts[:prompt].to_s
|
|
475
534
|
rejected = opts[:rejected].to_s
|
|
@@ -525,20 +584,112 @@ module PWN
|
|
|
525
584
|
public_class_method def self.write_source_quota(opts = {})
|
|
526
585
|
source = opts[:source].to_s
|
|
527
586
|
recent = preferences(limit: WRITE_SOURCE_WINDOW)
|
|
528
|
-
return { over_cap: false, share: 0.0, n: 0, window: recent.length } if recent.length < 10
|
|
587
|
+
return { over_cap: false, share: 0.0, n: 0, window: recent.length, underfilled: true } if recent.length < 10
|
|
529
588
|
|
|
530
589
|
n = recent.count { |r| r[:source].to_s == source }
|
|
531
590
|
share = n.to_f / recent.length
|
|
591
|
+
target = TARGET_SOURCE_MIX[source]
|
|
532
592
|
{
|
|
533
593
|
over_cap: share > WRITE_SOURCE_CAP,
|
|
534
594
|
share: share.round(3),
|
|
535
595
|
n: n,
|
|
536
596
|
window: recent.length,
|
|
537
597
|
source: source,
|
|
538
|
-
cap: WRITE_SOURCE_CAP
|
|
598
|
+
cap: WRITE_SOURCE_CAP,
|
|
599
|
+
target: target,
|
|
600
|
+
underfilled: target ? share < (target * 0.5) : share < 0.05,
|
|
601
|
+
deficit: target ? (target - share).round(3) : nil
|
|
602
|
+
}
|
|
603
|
+
rescue StandardError
|
|
604
|
+
{ over_cap: false, share: 0.0, underfilled: true }
|
|
605
|
+
end
|
|
606
|
+
|
|
607
|
+
# P0 — online generator mix report + urgency flags. Controllers
|
|
608
|
+
# (auto_introspect, practice, counterfactual gate) consult this so
|
|
609
|
+
# underfilled sources get scheduling priority while over-cap
|
|
610
|
+
# resolve stops flooding. Returns {by_source, trajectory_fraction,
|
|
611
|
+
# urgent:[], suppress:[], healthy:}.
|
|
612
|
+
public_class_method def self.generator_mix(opts = {})
|
|
613
|
+
limit = opts[:limit] || WRITE_SOURCE_WINDOW
|
|
614
|
+
rows = preferences(limit: limit)
|
|
615
|
+
usable = rows.select { |r| usable_preference?(row: r) }
|
|
616
|
+
by = Hash.new(0)
|
|
617
|
+
usable.each { |r| by[r[:source].to_s] += 1 }
|
|
618
|
+
n = usable.length
|
|
619
|
+
shares = {}
|
|
620
|
+
TARGET_SOURCE_MIX.each_key { |k| shares[k] = n.zero? ? 0.0 : (by[k].to_f / n).round(3) }
|
|
621
|
+
by.each_key { |k| shares[k] ||= (by[k].to_f / n).round(3) }
|
|
622
|
+
|
|
623
|
+
traj_n = usable.count { |r| TRAJECTORY_SHAPES.include?(r[:shape].to_s) }
|
|
624
|
+
traj_f = n.zero? ? 0.0 : (traj_n.to_f / n).round(3)
|
|
625
|
+
|
|
626
|
+
urgent = []
|
|
627
|
+
suppress = []
|
|
628
|
+
TARGET_SOURCE_MIX.each do |src, target|
|
|
629
|
+
sh = shares[src].to_f
|
|
630
|
+
urgent << src if sh < (target * 0.5) && n >= 5
|
|
631
|
+
suppress << src if sh > WRITE_SOURCE_CAP && n >= 10
|
|
632
|
+
end
|
|
633
|
+
suppress << 'mistakes_resolve' if shares['mistakes_resolve'].to_f > WRITE_SOURCE_CAP && n >= 10 && !suppress.include?('mistakes_resolve')
|
|
634
|
+
|
|
635
|
+
healthy = urgent.empty? && suppress.empty? && traj_f >= 0.5 && n >= 10
|
|
636
|
+
{
|
|
637
|
+
n: n,
|
|
638
|
+
raw_n: rows.length,
|
|
639
|
+
by_source: by,
|
|
640
|
+
shares: shares,
|
|
641
|
+
targets: TARGET_SOURCE_MIX,
|
|
642
|
+
trajectory_fraction: traj_f,
|
|
643
|
+
urgent: urgent.uniq,
|
|
644
|
+
suppress: suppress.uniq,
|
|
645
|
+
healthy: healthy,
|
|
646
|
+
recommendation: if healthy
|
|
647
|
+
'mix_ok'
|
|
648
|
+
elsif n < 10
|
|
649
|
+
'need_more_pairs'
|
|
650
|
+
elsif traj_f < 0.5
|
|
651
|
+
'need_trajectory_shape'
|
|
652
|
+
elsif urgent.any?
|
|
653
|
+
"boost:#{urgent.join(',')}"
|
|
654
|
+
else
|
|
655
|
+
"suppress:#{suppress.join(',')}"
|
|
656
|
+
end
|
|
539
657
|
}
|
|
658
|
+
rescue StandardError => e
|
|
659
|
+
{
|
|
660
|
+
n: 0, healthy: false, error: "#{e.class}: #{e.message}",
|
|
661
|
+
urgent: %w[curriculum counterfactual critic user_correction],
|
|
662
|
+
suppress: []
|
|
663
|
+
}
|
|
664
|
+
end
|
|
665
|
+
|
|
666
|
+
# P0 ops — infer trajectory shape for legacy ledger rows that predate
|
|
667
|
+
# P21/P25 shape tags. Used by scrub_preferences rewrite so generator_mix
|
|
668
|
+
# trajectory_fraction reflects content, not missing keys.
|
|
669
|
+
public_class_method def self.infer_shape(opts = {})
|
|
670
|
+
r = opts.is_a?(Hash) && opts.key?(:row) ? opts[:row] : opts
|
|
671
|
+
r = r.transform_keys(&:to_sym) if r.respond_to?(:transform_keys)
|
|
672
|
+
existing = r[:shape].to_s
|
|
673
|
+
return existing if TRAJECTORY_SHAPES.include?(existing) || existing == 'fix_prose'
|
|
674
|
+
|
|
675
|
+
chosen = r[:chosen].to_s
|
|
676
|
+
source = r[:source].to_s
|
|
677
|
+
# tool-call / trace markers → winning_trace
|
|
678
|
+
if chosen.match?(/\b(shell|pwn_eval|memory_|sessions_|reward_|curriculum_|extro_|mistakes_)\b/i) &&
|
|
679
|
+
(chosen.include?('→') || chosen.include?('tool_call') || chosen.include?('"name"') ||
|
|
680
|
+
chosen.lines.count { |l| l.strip.start_with?('{') || l.include?('arguments') } >= 1)
|
|
681
|
+
return 'winning_trace'
|
|
682
|
+
end
|
|
683
|
+
# long revised answer from critic / user / CF → revised_answer
|
|
684
|
+
return 'revised_answer' if chosen.length >= 200 && %w[critic user_correction counterfactual curriculum].include?(source)
|
|
685
|
+
|
|
686
|
+
# counterfactual real dispatch tag in meta
|
|
687
|
+
meta = r[:meta].is_a?(Hash) ? r[:meta] : {}
|
|
688
|
+
return 'real_dispatch' if meta[:mode].to_s == 'real_dispatch' || meta['mode'].to_s == 'real_dispatch'
|
|
689
|
+
|
|
690
|
+
existing.empty? ? nil : existing
|
|
540
691
|
rescue StandardError
|
|
541
|
-
|
|
692
|
+
nil
|
|
542
693
|
end
|
|
543
694
|
|
|
544
695
|
# P15 — keep only usable preference pairs for balance/export/promote.
|
|
@@ -586,6 +737,11 @@ module PWN
|
|
|
586
737
|
next
|
|
587
738
|
end
|
|
588
739
|
if usable_preference?(row: r)
|
|
740
|
+
# P0 ops — backfill shape so trajectory_fraction is meaningful
|
|
741
|
+
if r[:shape].to_s.empty?
|
|
742
|
+
inferred = infer_shape(row: r)
|
|
743
|
+
r = r.merge(shape: inferred) if inferred
|
|
744
|
+
end
|
|
589
745
|
kept << r
|
|
590
746
|
else
|
|
591
747
|
why = if r[:chosen].to_s.match?(/\A\s*CORRECTION:\s*/i)
|
|
@@ -640,6 +796,11 @@ module PWN
|
|
|
640
796
|
traj_n = rows.count { |r| TRAJECTORY_SHAPES.include?(r[:shape].to_s) }
|
|
641
797
|
traj_frac = total.zero? ? 0.0 : (traj_n.to_f / total).round(3)
|
|
642
798
|
monoculture = total.positive? && (by.values.max.to_f / total) > 0.7
|
|
799
|
+
mix = begin
|
|
800
|
+
generator_mix(limit: limit)
|
|
801
|
+
rescue StandardError
|
|
802
|
+
nil
|
|
803
|
+
end
|
|
643
804
|
{
|
|
644
805
|
total: before,
|
|
645
806
|
kept: total,
|
|
@@ -651,12 +812,15 @@ module PWN
|
|
|
651
812
|
by_shape_fraction: shape_frac,
|
|
652
813
|
trajectory_fraction: traj_frac,
|
|
653
814
|
monoculture: monoculture,
|
|
815
|
+
generator_mix: mix,
|
|
654
816
|
advice: if total < 12
|
|
655
817
|
'W1 thin: need more trajectory-shaped pairs before LoRA promote.'
|
|
656
818
|
elsif monoculture
|
|
657
819
|
'W1 monoculture: run Reward.scrub_preferences; enable :counterfactual/:critic; stop resolve-prose flood.'
|
|
658
820
|
elsif traj_frac < 0.30
|
|
659
821
|
'W1 geometry weak: <30% trajectory-shaped chosen sides — DPO would teach commentary.'
|
|
822
|
+
elsif mix && !mix[:healthy]
|
|
823
|
+
"W1 generator mix: #{mix[:recommendation]}"
|
|
660
824
|
else
|
|
661
825
|
'W1 source mix OK for gated export'
|
|
662
826
|
end
|
|
@@ -902,6 +1066,8 @@ module PWN
|
|
|
902
1066
|
# write must not poison rolling means forever.
|
|
903
1067
|
judge = opts[:judge].to_f.clamp(0.0, 1.0)
|
|
904
1068
|
entry = { judge: judge, at: Time.now.utc.iso8601 }
|
|
1069
|
+
# P1 — optional per-sample confidence (heuristic < LLM ORM)
|
|
1070
|
+
entry[:confidence] = opts[:confidence].to_f.clamp(0.0, 1.0) unless opts[:confidence].nil?
|
|
905
1071
|
# 1.3 — only roll proxy into the window when the caller actually
|
|
906
1072
|
# supplied a R4-aligned proxy_ok. Pre-ORM boolean noise no longer
|
|
907
1073
|
# dilutes gap_proxy_judge. Proxy is ALWAYS 0.0 or 1.0 when present.
|
|
@@ -144,3 +144,26 @@ PWN::AI::Agent::Registry.register(
|
|
|
144
144
|
check: -> { defined?(PWN::AI::Agent::Curriculum) && PWN::AI::Agent::Curriculum.respond_to?(:preference_balance) },
|
|
145
145
|
handler: ->(args) { PWN::AI::Agent::Curriculum.preference_balance(limit: args[:limit]) }
|
|
146
146
|
)
|
|
147
|
+
|
|
148
|
+
PWN::AI::Agent::Registry.register(
|
|
149
|
+
name: 'curriculum_practice_kpi',
|
|
150
|
+
toolset: 'curriculum',
|
|
151
|
+
schema: {
|
|
152
|
+
name: 'curriculum_practice_kpi',
|
|
153
|
+
description: 'P1 — Outer curriculum KPI: unresolved [REPEATING] count + week-over-week trend (improving|flat|regressing). Snapshots land in ~/.pwn/curriculum_kpi.jsonl from practice().',
|
|
154
|
+
parameters: {
|
|
155
|
+
type: 'object',
|
|
156
|
+
properties: {
|
|
157
|
+
limit: { type: 'integer', description: 'Trend window snapshots (default 14)' }
|
|
158
|
+
},
|
|
159
|
+
required: []
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
check: -> { defined?(PWN::AI::Agent::Curriculum) && PWN::AI::Agent::Curriculum.respond_to?(:repeating_trend) },
|
|
163
|
+
handler: lambda { |args|
|
|
164
|
+
{
|
|
165
|
+
trend: PWN::AI::Agent::Curriculum.repeating_trend(limit: args[:limit]),
|
|
166
|
+
snapshot: PWN::AI::Agent::Curriculum.practice_kpi(results: [])
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
)
|
|
@@ -192,3 +192,23 @@ PWN::AI::Agent::Registry.register(
|
|
|
192
192
|
)
|
|
193
193
|
}
|
|
194
194
|
)
|
|
195
|
+
|
|
196
|
+
PWN::AI::Agent::Registry.register(
|
|
197
|
+
name: 'reward_generator_mix',
|
|
198
|
+
toolset: 'reward',
|
|
199
|
+
schema: {
|
|
200
|
+
name: 'reward_generator_mix',
|
|
201
|
+
description: 'P0 — Online W1 generator mix vs TARGET_SOURCE_MIX. Returns urgent/suppress sources, trajectory_fraction, recommendation (boost:… / suppress:…). Controllers use this to schedule counterfactual/critic/curriculum over resolve flood.',
|
|
202
|
+
parameters: {
|
|
203
|
+
type: 'object',
|
|
204
|
+
properties: {
|
|
205
|
+
limit: { type: 'integer', description: 'Window size (default WRITE_SOURCE_WINDOW)' }
|
|
206
|
+
},
|
|
207
|
+
required: []
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
check: -> { defined?(PWN::AI::Agent::Reward) && PWN::AI::Agent::Reward.respond_to?(:generator_mix) },
|
|
211
|
+
handler: lambda { |args|
|
|
212
|
+
PWN::AI::Agent::Reward.generator_mix(limit: args[:limit])
|
|
213
|
+
}
|
|
214
|
+
)
|
|
@@ -39,19 +39,42 @@ PWN::AI::Agent::Registry.register(
|
|
|
39
39
|
# INTENTIONAL: this IS the pwn-ai → PWN bridge
|
|
40
40
|
# As YTCracker says, "It ain't a bug, it's a featcha."
|
|
41
41
|
# https://www.youtube.com/watch?v=2nALqqSqdDw
|
|
42
|
-
#
|
|
42
|
+
#
|
|
43
|
+
# File label '(pwn_eval)' (not __FILE__) so SyntaxError / NameError
|
|
44
|
+
# messages cite the *payload* line, not ruby_eval.rb:49 glued to source
|
|
45
|
+
# (e.g. "ruby_eval:49results = probes.map do |R|").
|
|
46
|
+
# Rescue ScriptError (SyntaxError, LoadError, NotImplementedError) as
|
|
47
|
+
# well as StandardError — Dispatch only catches StandardError, so an
|
|
48
|
+
# unrescued SyntaxError previously escaped the tool loop. Returning a
|
|
49
|
+
# structured error lets the model self-heal (fix code and retry).
|
|
50
|
+
# rubocop:disable Style/EvalWithLocation -- intentional file label
|
|
51
|
+
# '(pwn_eval)' so SyntaxError/NameError messages cite the payload, not
|
|
52
|
+
# this library line (specs + model self-heal depend on that string).
|
|
43
53
|
proc = eval(
|
|
44
|
-
"proc { #{code}
|
|
54
|
+
"proc { #{code}\n}",
|
|
45
55
|
TOPLEVEL_BINDING,
|
|
46
|
-
|
|
47
|
-
|
|
56
|
+
'(pwn_eval)',
|
|
57
|
+
1
|
|
48
58
|
)
|
|
59
|
+
# rubocop:enable Style/EvalWithLocation
|
|
49
60
|
val = proc.call
|
|
50
61
|
# rubocop:enable Security/Eval
|
|
51
62
|
# rubocop:enable Style/DocumentDynamicEvalDefinition
|
|
52
63
|
{ stdout: buf.string, value: val.inspect }
|
|
53
|
-
|
|
54
|
-
|
|
64
|
+
rescue ScriptError, StandardError => e
|
|
65
|
+
{
|
|
66
|
+
stdout: buf.string,
|
|
67
|
+
error: "#{e.class}: #{e.message}",
|
|
68
|
+
backtrace: Array(e.backtrace).first(5),
|
|
69
|
+
# Hint common payload footguns the model keeps emitting
|
|
70
|
+
hint: (
|
|
71
|
+
if e.is_a?(SyntaxError) && e.message.match?(/formal argument cannot be a constant/)
|
|
72
|
+
'Block parameters must be local variables (lowercase), e.g. |r| not |R|. Also close every do/end.'
|
|
73
|
+
elsif e.is_a?(SyntaxError)
|
|
74
|
+
'Payload failed to parse. Check matching do/end, braces, and that no line-number prefix was glued onto the code.'
|
|
75
|
+
end
|
|
76
|
+
)
|
|
77
|
+
}
|
|
55
78
|
ensure
|
|
56
79
|
$stdout = old_stdout
|
|
57
80
|
end
|
data/lib/pwn/version.rb
CHANGED