pwn 0.5.643 → 0.5.647
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/Cron.md +3 -2
- data/documentation/Home.md +1 -1
- data/documentation/Mistakes.md +13 -0
- data/documentation/Reinforcement-Learning.md +37 -3
- data/documentation/Skills-Memory-Learning.md +5 -3
- data/documentation/diagrams/dot/pwn-ai-feedback-learning-loop.dot +3 -3
- data/documentation/diagrams/dot/reinforcement-learning.dot +8 -8
- data/documentation/diagrams/pwn-ai-feedback-learning-loop.svg +318 -319
- data/documentation/diagrams/reinforcement-learning.svg +188 -187
- data/documentation/pwn-ai-Agent.md +5 -4
- data/lib/pwn/ai/agent/curriculum.rb +368 -32
- data/lib/pwn/ai/agent/learning.rb +159 -13
- data/lib/pwn/ai/agent/loop.rb +75 -4
- data/lib/pwn/ai/agent/metrics.rb +140 -10
- data/lib/pwn/ai/agent/mistakes.rb +26 -8
- data/lib/pwn/ai/agent/registry.rb +5 -2
- data/lib/pwn/ai/agent/reward.rb +279 -5
- data/lib/pwn/ai/agent/tools/reward.rb +66 -0
- data/lib/pwn/cron.rb +14 -2
- data/lib/pwn/version.rb +1 -1
- data/spec/integration/reinforced_feedback_loop_spec.rb +507 -12
- data/spec/lib/pwn/ai/agent/reward_spec.rb +64 -1
- data/third_party/pwn_rdoc.jsonl +24 -2
- metadata +1 -1
data/lib/pwn/ai/agent/reward.rb
CHANGED
|
@@ -297,6 +297,58 @@ module PWN
|
|
|
297
297
|
{ cleared: true, path: SENTINEL_FILE }
|
|
298
298
|
end
|
|
299
299
|
|
|
300
|
+
# P10 — backfill the R3 ring from Learning outcomes so offline/local
|
|
301
|
+
# hosts reach SENTINEL_WINDOW without waiting for live remote
|
|
302
|
+
# introspect. Only fills empty slots; never flushes a warm window.
|
|
303
|
+
# Called by Curriculum.offline_judge and safe to cron.
|
|
304
|
+
public_class_method def self.warm_sentinel(opts = {})
|
|
305
|
+
s = normalize_sentinel(raw: load_sentinel)
|
|
306
|
+
have = Array(s[:window]).length
|
|
307
|
+
return { added: 0, samples: have, status: :full, proxy_distrust: proxy_distrust } if have >= SENTINEL_WINDOW
|
|
308
|
+
return { added: 0, samples: have, status: :no_learning, proxy_distrust: proxy_distrust } unless defined?(Learning)
|
|
309
|
+
|
|
310
|
+
need = SENTINEL_WINDOW - have
|
|
311
|
+
limit = (opts[:limit] || [need * 4, 200].max).to_i
|
|
312
|
+
# Prefer scored rows; fall back to success-boolean so local hosts still warm.
|
|
313
|
+
rows = Learning.outcomes(limit: limit)
|
|
314
|
+
scored, unscored = rows.partition { |r| !r[:score].nil? }
|
|
315
|
+
ordered = scored.reverse + unscored.reverse
|
|
316
|
+
added = 0
|
|
317
|
+
ordered.each do |r|
|
|
318
|
+
break if added >= need
|
|
319
|
+
|
|
320
|
+
judge = if r[:score]
|
|
321
|
+
r[:score].to_f.clamp(0.0, 1.0)
|
|
322
|
+
else
|
|
323
|
+
case r[:success]
|
|
324
|
+
when true, 'true' then 0.75
|
|
325
|
+
when 'soft' then 0.55
|
|
326
|
+
when false, 'false' then 0.25
|
|
327
|
+
else 0.5
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
proxy = case r[:success]
|
|
331
|
+
when true, 'true' then true
|
|
332
|
+
when false, 'false', 'soft' then false
|
|
333
|
+
else judge >= 0.6
|
|
334
|
+
end
|
|
335
|
+
record_sentinel(proxy: proxy, judge: judge)
|
|
336
|
+
added += 1
|
|
337
|
+
end
|
|
338
|
+
final_n = Array(load_sentinel[:window]).length
|
|
339
|
+
# Recompute distrust once window is full so controllers can engage.
|
|
340
|
+
snap = final_n >= SENTINEL_WINDOW ? sentinel : { samples: final_n, status: :insufficient }
|
|
341
|
+
{
|
|
342
|
+
added: added,
|
|
343
|
+
samples: final_n,
|
|
344
|
+
status: (final_n >= SENTINEL_WINDOW ? :warmed_full : :warmed_partial),
|
|
345
|
+
proxy_distrust: proxy_distrust,
|
|
346
|
+
sentinel: snap.is_a?(Hash) ? snap.slice(:samples, :status, :reward_hacked, :proxy_distrust, :proxy, :judge) : nil
|
|
347
|
+
}
|
|
348
|
+
rescue StandardError => e
|
|
349
|
+
{ added: 0, error: "#{e.class}: #{e.message}" }
|
|
350
|
+
end
|
|
351
|
+
|
|
300
352
|
# ----------------------------------------------------------------
|
|
301
353
|
# R4 — Structured tool-result classifier
|
|
302
354
|
# ----------------------------------------------------------------
|
|
@@ -408,6 +460,16 @@ module PWN
|
|
|
408
460
|
# source: 'optional - :user_correction | :mistakes_resolve | :counterfactual | :critic'
|
|
409
461
|
# )
|
|
410
462
|
|
|
463
|
+
# Trajectory-shaped chosen sides that may land DPO without prose flood.
|
|
464
|
+
TRAJECTORY_SHAPES = %w[winning_trace revised_answer real_dispatch].freeze
|
|
465
|
+
|
|
466
|
+
# P9 — write-time source quota (not only export). Prefer diverse online
|
|
467
|
+
# generators over resolve-prose flood. Window is last WRITE_SOURCE_WINDOW
|
|
468
|
+
# pairs; a source already above WRITE_SOURCE_CAP is refused unless
|
|
469
|
+
# force: true (user_correction always forces).
|
|
470
|
+
WRITE_SOURCE_CAP = 0.40
|
|
471
|
+
WRITE_SOURCE_WINDOW = 100
|
|
472
|
+
|
|
411
473
|
public_class_method def self.record_preference(opts = {})
|
|
412
474
|
prompt = opts[:prompt].to_s
|
|
413
475
|
rejected = opts[:rejected].to_s
|
|
@@ -415,20 +477,194 @@ module PWN
|
|
|
415
477
|
return nil if prompt.strip.empty? || chosen.strip.empty? || rejected.strip.empty?
|
|
416
478
|
return nil if chosen.strip == rejected.strip
|
|
417
479
|
|
|
480
|
+
# Reject weak pair geometry: CORRECTION: flaw-prose is not a trajectory.
|
|
481
|
+
return { skipped: :weak_pair_geometry, reason: 'chosen looks like flaw prose, not a revised answer/trace' } if chosen.match?(/\A\s*CORRECTION:\s*/i) && chosen.length < 400 && !opts[:force]
|
|
482
|
+
|
|
483
|
+
source = (opts[:source] || :unknown).to_s
|
|
484
|
+
shape = opts[:shape].to_s
|
|
485
|
+
# P25 — require trajectory shape at write time unless force / user_correction.
|
|
486
|
+
# Stops resolve-prose flood from ever landing in the ledger; export scrub
|
|
487
|
+
# is defense-in-depth, not the primary gate.
|
|
488
|
+
traj = TRAJECTORY_SHAPES.include?(shape)
|
|
489
|
+
# P25 — non-trajectory prose never lands (export scrub is defense-in-depth).
|
|
490
|
+
# user_correction and explicit force: still allowed for human / migration paths.
|
|
491
|
+
unless traj || opts[:force] || source == 'user_correction'
|
|
492
|
+
return {
|
|
493
|
+
skipped: :non_trajectory_shape,
|
|
494
|
+
reason: "shape=#{shape.inspect} not in #{TRAJECTORY_SHAPES.join(',')}; pass force:true or a trajectory shape",
|
|
495
|
+
source: source
|
|
496
|
+
}
|
|
497
|
+
end
|
|
498
|
+
# P9 — write-time source quota still applies to trajectory pairs.
|
|
499
|
+
# P25 made every auto-written row trajectory-shaped; if traj also
|
|
500
|
+
# bypassed the quota, resolve monoculture would return via winning_trace
|
|
501
|
+
# flood. Only user_correction and explicit force:true skip the cap.
|
|
502
|
+
bypass_quota = opts[:force] || source == 'user_correction'
|
|
503
|
+
unless bypass_quota
|
|
504
|
+
quota = write_source_quota(source: source)
|
|
505
|
+
return quota.merge(skipped: :source_quota) if quota[:over_cap]
|
|
506
|
+
end
|
|
507
|
+
|
|
418
508
|
entry = {
|
|
419
509
|
id: Digest::SHA256.hexdigest("#{prompt}|#{rejected}|#{chosen}")[0, 12],
|
|
420
510
|
prompt: prompt[0, 4_000],
|
|
421
511
|
rejected: rejected[0, 4_000],
|
|
422
512
|
chosen: chosen[0, 4_000],
|
|
423
|
-
source:
|
|
513
|
+
source: source,
|
|
424
514
|
engine: (PWN::Env.dig(:ai, :active) if defined?(PWN::Env)).to_s,
|
|
425
515
|
timestamp: Time.now.utc.iso8601
|
|
426
516
|
}
|
|
517
|
+
entry[:meta] = opts[:meta] if opts[:meta].is_a?(Hash)
|
|
518
|
+
entry[:shape] = opts[:shape].to_s if opts[:shape]
|
|
427
519
|
FileUtils.mkdir_p(File.dirname(PREFERENCES_FILE))
|
|
428
520
|
File.open(PREFERENCES_FILE, 'a') { |f| f.puts(JSON.generate(entry)) }
|
|
429
521
|
entry
|
|
430
522
|
end
|
|
431
523
|
|
|
524
|
+
# Share of `source` among the newest WRITE_SOURCE_WINDOW prefs.
|
|
525
|
+
public_class_method def self.write_source_quota(opts = {})
|
|
526
|
+
source = opts[:source].to_s
|
|
527
|
+
recent = preferences(limit: WRITE_SOURCE_WINDOW)
|
|
528
|
+
return { over_cap: false, share: 0.0, n: 0, window: recent.length } if recent.length < 10
|
|
529
|
+
|
|
530
|
+
n = recent.count { |r| r[:source].to_s == source }
|
|
531
|
+
share = n.to_f / recent.length
|
|
532
|
+
{
|
|
533
|
+
over_cap: share > WRITE_SOURCE_CAP,
|
|
534
|
+
share: share.round(3),
|
|
535
|
+
n: n,
|
|
536
|
+
window: recent.length,
|
|
537
|
+
source: source,
|
|
538
|
+
cap: WRITE_SOURCE_CAP
|
|
539
|
+
}
|
|
540
|
+
rescue StandardError
|
|
541
|
+
{ over_cap: false, share: 0.0 }
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
# P15 — keep only usable preference pairs for balance/export/promote.
|
|
545
|
+
# Drops CORRECTION-only chosen, resolve rows without trajectory shape,
|
|
546
|
+
# and chosen≪rejected unless shape is a known trajectory form.
|
|
547
|
+
public_class_method def self.usable_preference?(opts = {})
|
|
548
|
+
r = opts.is_a?(Hash) && opts.key?(:row) ? opts[:row] : opts
|
|
549
|
+
r = r.transform_keys(&:to_sym) if r.respond_to?(:transform_keys)
|
|
550
|
+
chosen = r[:chosen].to_s
|
|
551
|
+
rejected = r[:rejected].to_s
|
|
552
|
+
shape = r[:shape].to_s
|
|
553
|
+
source = r[:source].to_s
|
|
554
|
+
return false if chosen.strip.empty? || rejected.strip.empty?
|
|
555
|
+
return false if chosen.strip == rejected.strip
|
|
556
|
+
return false if chosen.match?(/\A\s*CORRECTION:\s*/i) && chosen.length < 400
|
|
557
|
+
return false if shape == 'fix_prose'
|
|
558
|
+
# P25 — resolve rows must be trajectory-shaped to count as usable
|
|
559
|
+
return false if source == 'mistakes_resolve' && !TRAJECTORY_SHAPES.include?(shape)
|
|
560
|
+
|
|
561
|
+
# chosen ≪ rejected without trajectory shape → commentary, not policy
|
|
562
|
+
unless TRAJECTORY_SHAPES.include?(shape)
|
|
563
|
+
return false if rejected.length >= 200 && chosen.length < (rejected.length * 0.25) && chosen.length < 200
|
|
564
|
+
return false if rejected.length >= 400 && chosen.length < 120
|
|
565
|
+
end
|
|
566
|
+
true
|
|
567
|
+
rescue StandardError
|
|
568
|
+
false
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
# P15 — one-shot ledger hygiene. Filters in place (rewrite jsonl) or
|
|
572
|
+
# report-only. Returns {before:, after:, dropped:, by_reason:, path:}.
|
|
573
|
+
public_class_method def self.scrub_preferences(opts = {})
|
|
574
|
+
dry = opts.key?(:dry_run) ? opts[:dry_run] : false
|
|
575
|
+
path = PREFERENCES_FILE
|
|
576
|
+
return { before: 0, after: 0, dropped: 0, dry_run: dry, path: path } unless File.exist?(path)
|
|
577
|
+
|
|
578
|
+
raw = File.readlines(path)
|
|
579
|
+
kept = []
|
|
580
|
+
reasons = Hash.new(0)
|
|
581
|
+
raw.each do |line|
|
|
582
|
+
begin
|
|
583
|
+
r = JSON.parse(line, symbolize_names: true)
|
|
584
|
+
rescue StandardError
|
|
585
|
+
reasons[:parse_error] += 1
|
|
586
|
+
next
|
|
587
|
+
end
|
|
588
|
+
if usable_preference?(row: r)
|
|
589
|
+
kept << r
|
|
590
|
+
else
|
|
591
|
+
why = if r[:chosen].to_s.match?(/\A\s*CORRECTION:\s*/i)
|
|
592
|
+
:correction_prose
|
|
593
|
+
elsif r[:shape].to_s == 'fix_prose'
|
|
594
|
+
:fix_prose
|
|
595
|
+
elsif r[:chosen].to_s.length < (r[:rejected].to_s.length * 0.25)
|
|
596
|
+
:chosen_too_short
|
|
597
|
+
else
|
|
598
|
+
:weak_geometry
|
|
599
|
+
end
|
|
600
|
+
reasons[why] += 1
|
|
601
|
+
end
|
|
602
|
+
end
|
|
603
|
+
unless dry
|
|
604
|
+
bak = "#{path}.bak-p15-#{Time.now.utc.strftime('%Y%m%d%H%M%S')}"
|
|
605
|
+
FileUtils.cp(path, bak)
|
|
606
|
+
File.open(path, 'w') { |f| kept.each { |r| f.puts(JSON.generate(r)) } }
|
|
607
|
+
end
|
|
608
|
+
{
|
|
609
|
+
before: raw.length,
|
|
610
|
+
after: kept.length,
|
|
611
|
+
dropped: raw.length - kept.length,
|
|
612
|
+
by_reason: reasons,
|
|
613
|
+
dry_run: dry,
|
|
614
|
+
path: path,
|
|
615
|
+
backup: dry ? nil : bak
|
|
616
|
+
}
|
|
617
|
+
rescue StandardError => e
|
|
618
|
+
{ error: "#{e.class}: #{e.message}" }
|
|
619
|
+
end
|
|
620
|
+
|
|
621
|
+
# P15/P5 — geometry-aware source mix. scrub:true uses usable_preference?
|
|
622
|
+
# so operators see the post-hygiene diet (what export_dpo will train on).
|
|
623
|
+
public_class_method def self.preference_balance(opts = {})
|
|
624
|
+
limit = opts[:limit] || 10_000
|
|
625
|
+
scrub = opts.key?(:scrub) ? opts[:scrub] : false
|
|
626
|
+
rows = preferences(limit: limit)
|
|
627
|
+
before = rows.length
|
|
628
|
+
rows = rows.select { |r| usable_preference?(row: r) } if scrub
|
|
629
|
+
by = Hash.new(0)
|
|
630
|
+
by_shape = Hash.new(0)
|
|
631
|
+
rows.each do |r|
|
|
632
|
+
by[r[:source].to_s] += 1
|
|
633
|
+
sh = r[:shape].to_s
|
|
634
|
+
sh = 'unspecified' if sh.empty?
|
|
635
|
+
by_shape[sh] += 1
|
|
636
|
+
end
|
|
637
|
+
total = rows.length
|
|
638
|
+
frac = by.transform_values { |n| total.zero? ? 0.0 : (n.to_f / total).round(3) }
|
|
639
|
+
shape_frac = by_shape.transform_values { |n| total.zero? ? 0.0 : (n.to_f / total).round(3) }
|
|
640
|
+
traj_n = rows.count { |r| TRAJECTORY_SHAPES.include?(r[:shape].to_s) }
|
|
641
|
+
traj_frac = total.zero? ? 0.0 : (traj_n.to_f / total).round(3)
|
|
642
|
+
monoculture = total.positive? && (by.values.max.to_f / total) > 0.7
|
|
643
|
+
{
|
|
644
|
+
total: before,
|
|
645
|
+
kept: total,
|
|
646
|
+
scrubbed: scrub,
|
|
647
|
+
dropped: before - total,
|
|
648
|
+
by_source: by,
|
|
649
|
+
fractions: frac,
|
|
650
|
+
by_shape: by_shape,
|
|
651
|
+
by_shape_fraction: shape_frac,
|
|
652
|
+
trajectory_fraction: traj_frac,
|
|
653
|
+
monoculture: monoculture,
|
|
654
|
+
advice: if total < 12
|
|
655
|
+
'W1 thin: need more trajectory-shaped pairs before LoRA promote.'
|
|
656
|
+
elsif monoculture
|
|
657
|
+
'W1 monoculture: run Reward.scrub_preferences; enable :counterfactual/:critic; stop resolve-prose flood.'
|
|
658
|
+
elsif traj_frac < 0.30
|
|
659
|
+
'W1 geometry weak: <30% trajectory-shaped chosen sides — DPO would teach commentary.'
|
|
660
|
+
else
|
|
661
|
+
'W1 source mix OK for gated export'
|
|
662
|
+
end
|
|
663
|
+
}
|
|
664
|
+
rescue StandardError => e
|
|
665
|
+
{ error: "#{e.class}: #{e.message}" }
|
|
666
|
+
end
|
|
667
|
+
|
|
432
668
|
# Supported Method Parameters::
|
|
433
669
|
# rows = PWN::AI::Agent::Reward.preferences(limit: 500, source: nil)
|
|
434
670
|
|
|
@@ -463,6 +699,15 @@ module PWN
|
|
|
463
699
|
FileUtils.mkdir_p(DPO_DIR)
|
|
464
700
|
out = opts[:out] || File.join(DPO_DIR, "pwn-dpo-#{Time.now.utc.strftime('%Y%m%d')}.jsonl")
|
|
465
701
|
rows = preferences(limit: 100_000)
|
|
702
|
+
# P15 — drop weak geometry before source-cap so resolve prose cannot
|
|
703
|
+
# dominate the kept set after balance. opt-out with scrub: false.
|
|
704
|
+
scrub = opts.key?(:scrub) ? opts[:scrub] : true
|
|
705
|
+
geometry_dropped = 0
|
|
706
|
+
if scrub
|
|
707
|
+
usable = rows.select { |r| usable_preference?(row: r) }
|
|
708
|
+
geometry_dropped = rows.length - usable.length
|
|
709
|
+
rows = usable
|
|
710
|
+
end
|
|
466
711
|
# P5 — downsample so no single source exceeds DPO_SOURCE_CAP of the export.
|
|
467
712
|
# opt-out with balance: false (raw dump for diagnostics).
|
|
468
713
|
balance = opts.key?(:balance) ? opts[:balance] : true
|
|
@@ -484,8 +729,14 @@ module PWN
|
|
|
484
729
|
by_src = selected.group_by { |r| r[:source].to_s }.transform_values(&:length)
|
|
485
730
|
{
|
|
486
731
|
path: out, format: fmt, pairs: selected.length, bytes: File.size(out),
|
|
487
|
-
balanced: balance, dropped: dropped,
|
|
488
|
-
|
|
732
|
+
balanced: balance, dropped: dropped, geometry_dropped: geometry_dropped,
|
|
733
|
+
scrubbed: scrub, by_source: by_src,
|
|
734
|
+
source_cap: balance ? (opts[:source_cap] || DPO_SOURCE_CAP).to_f : nil,
|
|
735
|
+
preference_balance: begin
|
|
736
|
+
preference_balance(limit: 10_000, scrub: true)
|
|
737
|
+
rescue StandardError
|
|
738
|
+
nil
|
|
739
|
+
end
|
|
489
740
|
}
|
|
490
741
|
end
|
|
491
742
|
|
|
@@ -615,7 +866,11 @@ module PWN
|
|
|
615
866
|
j = JSON.parse(l, symbolize_names: true)
|
|
616
867
|
if j[:role].to_s == 'tool'
|
|
617
868
|
ti += 1
|
|
618
|
-
|
|
869
|
+
if rewards[ti]
|
|
870
|
+
j[:step_reward] = rewards[ti]
|
|
871
|
+
# P18 — fold step_reward into Metrics so Registry.rank can bias
|
|
872
|
+
fold_step_reward_to_metrics(content: j[:content], reward: rewards[ti])
|
|
873
|
+
end
|
|
619
874
|
end
|
|
620
875
|
"#{JSON.generate(j)}\n"
|
|
621
876
|
rescue StandardError
|
|
@@ -626,6 +881,21 @@ module PWN
|
|
|
626
881
|
nil
|
|
627
882
|
end
|
|
628
883
|
|
|
884
|
+
# Extract tool name from "name → …" session content and record PRM.
|
|
885
|
+
private_class_method def self.fold_step_reward_to_metrics(opts = {})
|
|
886
|
+
return unless defined?(Metrics) && Metrics.respond_to?(:record_step_reward)
|
|
887
|
+
|
|
888
|
+
content = opts[:content].to_s
|
|
889
|
+
name = content[/\A([a-z_][a-z0-9_]*)\s*→/i, 1] ||
|
|
890
|
+
content[/\A([a-z_][a-z0-9_]*)\s*->/i, 1] ||
|
|
891
|
+
content[/\A([a-z_][a-z0-9_]*)/, 1]
|
|
892
|
+
return if name.to_s.empty?
|
|
893
|
+
|
|
894
|
+
Metrics.record_step_reward(name: name, reward: opts[:reward])
|
|
895
|
+
rescue StandardError
|
|
896
|
+
nil
|
|
897
|
+
end
|
|
898
|
+
|
|
629
899
|
private_class_method def self.record_sentinel(opts = {})
|
|
630
900
|
s = normalize_sentinel(raw: load_sentinel)
|
|
631
901
|
# Clamp judge to [0,1] — LLM/heuristic should already, but a bad
|
|
@@ -872,13 +1142,17 @@ module PWN
|
|
|
872
1142
|
PWN::AI::Agent::Reward.prm(request: req, session_id: sid) # R2 PRM → per-step credit
|
|
873
1143
|
PWN::AI::Agent::Reward.sentinel # R3 reward-hacking detector
|
|
874
1144
|
PWN::AI::Agent::Reward.reset_sentinel # wipe corrupt window + distrust
|
|
1145
|
+
PWN::AI::Agent::Reward.warm_sentinel # P10 fill R3 window from Learning outcomes
|
|
875
1146
|
PWN::AI::Agent::Reward.semantic_ok(name: 'shell', raw: json, args: args) # R4 kills phantom exit≠0 mistakes
|
|
876
1147
|
|
|
877
1148
|
# Tier 5 — preference pairs → DPO
|
|
878
1149
|
PWN::AI::Agent::Reward.record_preference(prompt: p, rejected: r, chosen: c, source: :user_correction)
|
|
879
1150
|
PWN::AI::Agent::Reward.preferences(limit: 100)
|
|
880
|
-
PWN::AI::Agent::Reward.export_dpo(format: :dpo) # W1 → ~/.pwn/finetune/pwn-dpo-*.jsonl (≤40%/source)
|
|
1151
|
+
PWN::AI::Agent::Reward.export_dpo(format: :dpo) # W1 → ~/.pwn/finetune/pwn-dpo-*.jsonl (≤40%/source, scrubbed)
|
|
881
1152
|
PWN::AI::Agent::Reward.export_dpo(format: :dpo, balance: false) # raw dump (diagnostics)
|
|
1153
|
+
PWN::AI::Agent::Reward.scrub_preferences(dry_run: true) # P15 ledger hygiene report
|
|
1154
|
+
PWN::AI::Agent::Reward.scrub_preferences # P15 rewrite jsonl (backup first)
|
|
1155
|
+
PWN::AI::Agent::Reward.preference_balance(scrub: true) # P15 geometry-aware mix
|
|
882
1156
|
|
|
883
1157
|
# Tier 6 — grounded reward
|
|
884
1158
|
PWN::AI::Agent::Reward.verify_as_reward(final: text) # E3 browser-verified reward
|
|
@@ -126,3 +126,69 @@ PWN::AI::Agent::Registry.register(
|
|
|
126
126
|
)
|
|
127
127
|
}
|
|
128
128
|
)
|
|
129
|
+
|
|
130
|
+
PWN::AI::Agent::Registry.register(
|
|
131
|
+
name: 'reward_warm_sentinel',
|
|
132
|
+
toolset: 'learning',
|
|
133
|
+
schema: {
|
|
134
|
+
name: 'reward_warm_sentinel',
|
|
135
|
+
description: 'P10 — Backfill the R3 sentinel ring buffer from Learning ' \
|
|
136
|
+
'outcomes so proxy_distrust can engage on local/offline hosts ' \
|
|
137
|
+
'without waiting for live remote introspect. Only fills empty ' \
|
|
138
|
+
'slots; never flushes a warm window.',
|
|
139
|
+
parameters: {
|
|
140
|
+
type: 'object',
|
|
141
|
+
properties: {
|
|
142
|
+
limit: { type: 'integer', default: 120, description: 'Max outcomes to scan.' }
|
|
143
|
+
},
|
|
144
|
+
required: []
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
check: -> { defined?(PWN::AI::Agent::Reward) && PWN::AI::Agent::Reward.respond_to?(:warm_sentinel) },
|
|
148
|
+
handler: ->(args) { PWN::AI::Agent::Reward.warm_sentinel(limit: args[:limit] || 120) }
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
PWN::AI::Agent::Registry.register(
|
|
152
|
+
name: 'reward_scrub_preferences',
|
|
153
|
+
toolset: 'learning',
|
|
154
|
+
schema: {
|
|
155
|
+
name: 'reward_scrub_preferences',
|
|
156
|
+
description: 'P15 — One-shot W1 ledger hygiene. Drops CORRECTION:-only chosen, resolve rows without trajectory shape, and chosen≪rejected pairs. dry_run:true reports; false rewrites ~/.pwn/preferences.jsonl (backup first).',
|
|
157
|
+
parameters: {
|
|
158
|
+
type: 'object',
|
|
159
|
+
properties: {
|
|
160
|
+
dry_run: { type: 'boolean', default: true, description: 'Report only (default true).' }
|
|
161
|
+
},
|
|
162
|
+
required: []
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
check: -> { defined?(PWN::AI::Agent::Reward) && PWN::AI::Agent::Reward.respond_to?(:scrub_preferences) },
|
|
166
|
+
handler: lambda { |args|
|
|
167
|
+
dry = args.key?(:dry_run) ? args[:dry_run] : true
|
|
168
|
+
PWN::AI::Agent::Reward.scrub_preferences(dry_run: dry)
|
|
169
|
+
}
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
PWN::AI::Agent::Registry.register(
|
|
173
|
+
name: 'reward_preference_balance',
|
|
174
|
+
toolset: 'learning',
|
|
175
|
+
schema: {
|
|
176
|
+
name: 'reward_preference_balance',
|
|
177
|
+
description: 'P15/P5 — Geometry-aware W1 diversity report. scrub:true scores the post-hygiene diet (trajectory_fraction, by_shape) so promote/export gates see usable pairs only.',
|
|
178
|
+
parameters: {
|
|
179
|
+
type: 'object',
|
|
180
|
+
properties: {
|
|
181
|
+
limit: { type: 'integer', default: 10_000 },
|
|
182
|
+
scrub: { type: 'boolean', default: true }
|
|
183
|
+
},
|
|
184
|
+
required: []
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
check: -> { defined?(PWN::AI::Agent::Reward) && PWN::AI::Agent::Reward.respond_to?(:preference_balance) },
|
|
188
|
+
handler: lambda { |args|
|
|
189
|
+
PWN::AI::Agent::Reward.preference_balance(
|
|
190
|
+
limit: args[:limit] || 10_000,
|
|
191
|
+
scrub: args.key?(:scrub) ? args[:scrub] : true
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
)
|
data/lib/pwn/cron.rb
CHANGED
|
@@ -231,8 +231,11 @@ module PWN
|
|
|
231
231
|
)
|
|
232
232
|
end
|
|
233
233
|
|
|
234
|
-
# P3 — backfill ORM/PRM labels when local :failure_only introspect is on
|
|
235
|
-
|
|
234
|
+
# P3 — backfill ORM/PRM labels when local :failure_only introspect is on.
|
|
235
|
+
# P13 — treat legacy alias offline_judge_nightly as the same job so
|
|
236
|
+
# install_defaults never double-seeds the 30 3 * * * slot.
|
|
237
|
+
offline_names = %w[curriculum_offline_judge offline_judge_nightly]
|
|
238
|
+
unless names.intersect?(offline_names)
|
|
236
239
|
seeded << create(
|
|
237
240
|
name: 'curriculum_offline_judge',
|
|
238
241
|
schedule: '30 3 * * *',
|
|
@@ -241,6 +244,15 @@ module PWN
|
|
|
241
244
|
enabled: true
|
|
242
245
|
)
|
|
243
246
|
end
|
|
247
|
+
# Disable duplicate alias if both exist (idempotent cleanup).
|
|
248
|
+
if names.include?('curriculum_offline_judge') && names.include?('offline_judge_nightly')
|
|
249
|
+
begin
|
|
250
|
+
dup = jobs.values.find { |j| j[:name].to_s == 'offline_judge_nightly' }
|
|
251
|
+
disable(id: dup[:id]) if dup && dup[:enabled]
|
|
252
|
+
rescue StandardError
|
|
253
|
+
nil
|
|
254
|
+
end
|
|
255
|
+
end
|
|
244
256
|
|
|
245
257
|
# M1/M3 — nightly memory GC so the injected MEMORY block stays high-signal
|
|
246
258
|
unless names.include?('learning_consolidate_nightly')
|
data/lib/pwn/version.rb
CHANGED