pwn 0.5.685 → 0.5.688

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.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/.ruby-version +1 -1
  4. data/Gemfile +3 -3
  5. data/README.md +3 -4
  6. data/documentation/Agent-Tool-Registry.md +2 -2
  7. data/documentation/Configuration.md +5 -4
  8. data/documentation/General-PWN-Usage.md +1 -1
  9. data/documentation/Home.md +1 -1
  10. data/documentation/How-PWN-Works.md +1 -1
  11. data/documentation/Mistakes.md +1 -1
  12. data/documentation/Reinforcement-Learning.md +4 -6
  13. data/documentation/Troubleshooting.md +1 -1
  14. data/documentation/pwn-REPL.md +1 -1
  15. data/documentation/pwn-ai-Agent.md +2 -3
  16. data/lib/pwn/ai/agent/curriculum.rb +2 -2
  17. data/lib/pwn/ai/agent/dispatch.rb +1 -1
  18. data/lib/pwn/ai/agent/learning.rb +150 -32
  19. data/lib/pwn/ai/agent/loop.rb +352 -127
  20. data/lib/pwn/ai/agent/prompt_builder.rb +50 -29
  21. data/lib/pwn/ai/agent/registry.rb +2 -1
  22. data/lib/pwn/ai/agent/reward.rb +9 -0
  23. data/lib/pwn/ai/agent/swarm.rb +3 -1
  24. data/lib/pwn/ai/agent/task_summarizer.rb +23 -6
  25. data/lib/pwn/ai/agent/tool_guard.rb +109 -0
  26. data/lib/pwn/ai/agent/tools/ruby_eval.rb +44 -20
  27. data/lib/pwn/ai/agent/tools/shell.rb +17 -4
  28. data/lib/pwn/ai/agent/tools/skills.rb +63 -2
  29. data/lib/pwn/ai/anthropic.rb +25 -123
  30. data/lib/pwn/ai/gemini.rb +25 -20
  31. data/lib/pwn/ai/grok.rb +30 -171
  32. data/lib/pwn/ai/http_retry.rb +78 -0
  33. data/lib/pwn/ai/ollama.rb +25 -17
  34. data/lib/pwn/ai/open_ai.rb +26 -123
  35. data/lib/pwn/ai/open_web_ui.rb +25 -17
  36. data/lib/pwn/ai.rb +1 -86
  37. data/lib/pwn/config.rb +1 -1
  38. data/lib/pwn/plugins/log.rb +444 -0
  39. data/lib/pwn/plugins/repl.rb +75 -20
  40. data/lib/pwn/plugins/transparent_browser.rb +16 -2
  41. data/lib/pwn/sessions.rb +50 -1
  42. data/lib/pwn/version.rb +1 -1
  43. data/spec/integration/reinforced_feedback_loop_spec.rb +9 -11
  44. data/spec/lib/pwn/ai/agent/loop_spec.rb +223 -25
  45. data/spec/lib/pwn/ai/agent/prompt_builder_spec.rb +3 -0
  46. data/spec/lib/pwn/ai/agent/registry_spec.rb +1 -1
  47. data/spec/lib/pwn/ai/agent/reward_spec.rb +13 -0
  48. data/spec/lib/pwn/ai/agent/swarm_spec.rb +6 -0
  49. data/spec/lib/pwn/ai/agent/task_summarizer_spec.rb +36 -0
  50. data/spec/lib/pwn/ai/agent/tool_guard_spec.rb +46 -0
  51. data/spec/lib/pwn/ai/agent/tools/ruby_eval_spec.rb +24 -0
  52. data/spec/lib/pwn/ai/agent/tools/shell_spec.rb +10 -0
  53. data/spec/lib/pwn/ai/agent/tools/skills_spec.rb +29 -0
  54. data/spec/lib/pwn/ai/anthropic_spec.rb +2 -2
  55. data/spec/lib/pwn/ai/gemini_spec.rb +2 -2
  56. data/spec/lib/pwn/ai/grok_spec.rb +24 -2
  57. data/spec/lib/pwn/ai/http_retry_spec.rb +55 -0
  58. data/spec/lib/pwn/ai/ollama_spec.rb +2 -7
  59. data/spec/lib/pwn/ai/open_ai_spec.rb +2 -23
  60. data/spec/lib/pwn/ai/open_web_ui_spec.rb +2 -7
  61. data/spec/lib/pwn/ai_spec.rb +4 -21
  62. data/spec/lib/pwn/plugins/log_spec.rb +290 -0
  63. data/spec/lib/pwn/plugins/repl_pwn_vault_spec.rb +42 -1
  64. data/spec/lib/pwn/plugins/repl_spec.rb +4 -3
  65. data/spec/lib/pwn/plugins/transparent_browser_spec.rb +8 -0
  66. data/spec/lib/pwn/sessions_spec.rb +24 -0
  67. data/third_party/pwn_rdoc.jsonl +65 -23
  68. data/tmp_pwn_critic_skills_flaw.txt +9 -0
  69. metadata +11 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4969508ce00cf858c5dfd06506e306f37d86e3d0671f07c3d3c021fa603b49d8
4
- data.tar.gz: 6e23201a56add7514b8a12de42ee930aba424e33329bc9c608bd3dc4c2ce0473
3
+ metadata.gz: 57da0c5ba0b3110683a6cad1e71a400dc1d58ef63337016ff1d6b710bbc293e1
4
+ data.tar.gz: 125343370941538d882d38f8b3b2bec6a89d973db66dc5ad724e85ec073a2583
5
5
  SHA512:
6
- metadata.gz: 980664beb783d0a4c4aed5df5548d8f3c839e47afadd566a35f5b3b4413d6242e6c6da54319cacc91ceb7a3f8f5756f8268d29876b6277fdfda2e2f4475a251d
7
- data.tar.gz: de6ce8fac7cc8c1dda8989faef831c4c32bf88141d188b113aa16b7b0035f3062505fc4cb843ea2b7f9e179306f66060ab8fbc1e4c8a325169a274f5784ef79f
6
+ metadata.gz: eb44627c68dd94bedf56449d7a5337b1042a69ecf080e17300dce2e907643a6c14e13cf32064dfbc384885b501e43b1c0e4f6bd203549dd805e335f6948eea5f
7
+ data.tar.gz: 5031073e597b8562dd5ff6718fb37faad67d0bdab0afce0750e518762c3398d16aba22e0486859ae56e8539455121ac7c2db457a9e6348e6ee366ba3b0534a15
data/.gitignore CHANGED
@@ -64,3 +64,5 @@ packer/*.box
64
64
  third_party/burpsuite-pro.jar
65
65
  third_party/open_ai_pwn.jsonl
66
66
  ubuntu-*-cloudimg-console.log
67
+
68
+ vendor/
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 4.0.5
1
+ 4.0.6
data/Gemfile CHANGED
@@ -20,9 +20,9 @@ gem 'base32', '0.3.4'
20
20
  gem 'bitcoin-ruby', '0.0.20'
21
21
  gem 'brakeman', '8.0.6'
22
22
  gem 'bson', '5.2.0'
23
- gem 'bundler', '>=4.0.18'
23
+ gem 'bundler', '>=4.0.19'
24
24
  gem 'bundler-audit', '>=0.9.3'
25
- gem 'bunny', '3.1.0'
25
+ gem 'bunny', '3.2.0'
26
26
  gem 'colorize', '1.1.0'
27
27
  gem 'credit_card_validations', '9.0.0'
28
28
  gem 'curses', '1.6.0'
@@ -50,7 +50,7 @@ gem 'libusb', '0.8.0'
50
50
  gem 'luhn', '3.0.0'
51
51
  gem 'mail', '2.9.1'
52
52
  gem 'mcp', '1.2.0'
53
- gem 'meshtastic', '0.0.173'
53
+ gem 'meshtastic', '0.0.174'
54
54
  gem 'metasm', '1.0.6'
55
55
  gem 'mongo', '2.25.0'
56
56
  gem 'msfrpc-client', '1.1.2'
data/README.md CHANGED
@@ -121,10 +121,9 @@ message bus:
121
121
  Long-running turns also show **executive task briefs** (not raw commands) via
122
122
  `TaskSummarizer`. There is no request type. Every request gets an English
123
123
  task compass (`emit_plan!` on submit, `about_to` as `task k/n` before each
124
- tool batch). Duplicate briefs are suppressed. Budget-hot turns skip extra
125
- counterfactual forks and only strip tools on the last iteration when the
126
- original request is already satisfied. Yesterday's scars do not lower this
127
- request's `max_iters` (default 777).
124
+ tool batch). Duplicate briefs are suppressed. Loop.run does not abort
125
+ on a round cap CORE_TOOLS stay until `may_finalize?`. Yesterday's
126
+ scars do not end this request.
128
127
 
129
128
  Full pages: [How PWN Works](documentation/How-PWN-Works.md) ·
130
129
  [All data-flow diagrams](documentation/Diagrams.md)
@@ -56,7 +56,7 @@ pool to:
56
56
  ```text
57
57
  CORE_TOOLS / DEFAULT_PREFERENCE (same list):
58
58
  memory_recall · session_recall · skills_recall · pwn_eval · shell
59
- mistakes_record · mistakes_resolve · learning_note_outcome · memory_remember
59
+ mistakes_record · mistakes_resolve · learning_note_outcome · memory_remember · skills_update
60
60
  + top-K keyword-ranked matches for THIS request
61
61
  (ties break on Metrics per-engine success_rate, then
62
62
  ai.agent.tool_preference)
@@ -80,7 +80,7 @@ default order:
80
80
 
81
81
  ```text
82
82
  memory_recall · session_recall · skills_recall · pwn_eval · shell
83
- mistakes_record · mistakes_resolve · learning_note_outcome · memory_remember
83
+ mistakes_record · mistakes_resolve · learning_note_outcome · memory_remember · skills_update
84
84
  ```
85
85
 
86
86
  Set `ai.agent.tool_preference` in `~/.pwn/pwn.yaml`, or pass `order:` /
@@ -118,7 +118,7 @@ ai:
118
118
 
119
119
  agent:
120
120
  native_tools: true # Use provider-native tool_calls / function-calling. false → legacy text-parsed tool protocol.
121
- max_iters: 777 # Hard cap on tool-call rounds per user turn. Scars do not lower it.
121
+ max_iters: 777 # Advisory / Swarm. Loop.run does not abort on this cap.
122
122
  task_summary: true # Executive task briefs via TaskSummarizer (plan + about_to). false disables.
123
123
  task_summary_every: 5 # When task_summary_verbose: emit Progress every N completed tools.
124
124
  task_summary_interval_s: 8.0 # When verbose: also emit when this many seconds elapsed.
@@ -132,7 +132,7 @@ ai:
132
132
  shell_bash: false # true -> run shell via bash -lc. Default is /bin/sh.
133
133
  plan_first: ~ # Plan-then-act pre-pass. nil = auto (true when ai.active is ollama or openwebui).
134
134
  tool_router: ~ # Dynamic tool-set slimming. nil = auto (true for ollama / openwebui).
135
- tool_preference: # Same order as CORE_TOOLS: memory_recall, session_recall, skills_recall, pwn_eval, shell, mistakes_record, mistakes_resolve, learning_note_outcome, memory_remember. Current session is injected separately.
135
+ tool_preference: # Same order as CORE_TOOLS: memory_recall, session_recall, skills_recall, pwn_eval, shell, mistakes_record, mistakes_resolve, learning_note_outcome, memory_remember, skills_update. Current session is injected separately.
136
136
  - memory_recall
137
137
  - session_recall
138
138
  - skills_recall
@@ -142,6 +142,7 @@ ai:
142
142
  - mistakes_resolve
143
143
  - learning_note_outcome
144
144
  - memory_remember
145
+ - skills_update
145
146
  escalation_persona: escalator # Swarm persona for a 3-line corrective hint after enough in-turn failures. nil = disabled.
146
147
  local_introspect: failure_only # End-of-turn auto_introspect policy for local engines: always | failure_only | every_n.
147
148
  policy: true # Live tabular Q / REINFORCE. Advisory only; never replaces TaskSummarizer / plan_first. false disables.
@@ -302,7 +303,7 @@ PWN::Config.refresh_env
302
303
  | Key path | Type | Default | Consumed by | Purpose |
303
304
  |---|---|---|---|---|
304
305
  | `ai.agent.native_tools` | Boolean | `true` | `PWN::Plugins::REPL` (`pwn-ai` cmd) | Use provider-native `tool_calls` / function-calling. `false` falls back to the legacy text-parsed tool protocol. |
305
- | `ai.agent.max_iters` | Integer | `777` | `PWN::AI::Agent::Loop.run`, `PWN::AI::Agent::Swarm` | Hard cap on tool-call rounds per user turn. Budget-hot scars and overconfidence do not lower this request's runway. |
306
+ | `ai.agent.max_iters` | Integer | `777` | `PWN::AI::Agent::Swarm` | Advisory. `Loop.run` does not abort a live goal on this counter. |
306
307
  | `ai.agent.task_summary` | Boolean | `true` | `PWN::AI::Agent::TaskSummarizer`, `Loop` | Master switch for executive task briefs (`emit_plan!` / `about_to`). |
307
308
  | `ai.agent.task_summary_every` | Integer | `5` | `TaskSummarizer.every_n` | Verbose progress cadence (tools). |
308
309
  | `ai.agent.task_summary_interval_s` | Float | `8.0` | `TaskSummarizer.interval_s` | Verbose progress cadence (seconds). |
@@ -315,7 +316,7 @@ PWN::Config.refresh_env
315
316
  | `ai.agent.shell_bash` | Boolean | `false` | `PWN::AI::Agent::ToolGuard.shell_bash?` | When true, `shell` runs via `bash -lc` so bash-only syntax is allowed. Default is POSIX `/bin/sh` and bashisms are rejected with a rewrite hint. |
316
317
  | `ai.agent.plan_first` | Boolean \| `nil` | `nil` (auto: `true` when `ai.active` is `ollama` or `openwebui`) | `PWN::AI::Agent::Loop.plan_first` | Plan-then-act pre-pass: the model must emit a numbered tool plan (as an assistant message) *before* it may dispatch anything. Cheap chain-of-thought scaffolding for local models. |
317
318
  | `ai.agent.tool_router` | Boolean \| `nil` | `nil` (auto: `true` for `ollama` / `openwebui`) | `PWN::AI::Agent::Registry.definitions` | Dynamic tool-set slimming: expose only `Registry::CORE_TOOLS` + the top-K keyword-relevant schemas for *this* request. Ties break on historical `Metrics` success rate, then `ai.agent.tool_preference`. |
318
- | `ai.agent.tool_preference` | Array\<String\> | `memory_recall`, `session_recall`, `skills_recall`, `pwn_eval`, `shell`, `mistakes_record`, `mistakes_resolve`, `learning_note_outcome`, `memory_remember` | `PWN::AI::Agent::Registry.preference_order` / `.rank` / `.apply_preference`, `Policy` | Same order as `CORE_TOOLS`. Current session is injected as RECENT TURNS. Explicit empty list disables preference. |
319
+ | `ai.agent.tool_preference` | Array\<String\> | `memory_recall`, `session_recall`, `skills_recall`, `pwn_eval`, `shell`, `mistakes_record`, `mistakes_resolve`, `learning_note_outcome`, `memory_remember`, `skills_update` | `PWN::AI::Agent::Registry.preference_order` / `.rank` / `.apply_preference`, `Policy` | Same order as `CORE_TOOLS`. Current session is injected as RECENT TURNS. Explicit empty list disables preference. |
319
320
  | `ai.agent.defer_introspect` | Boolean | `true` | `PWN::AI::Agent::TurnFinalizer` | Run `Learning.auto_introspect` on a background thread after the user-visible reply. Specs and cron stay inline. |
320
321
  | `ai.agent.prompt_cache` | Boolean | `true` | `PWN::AI::Agent::PromptCache` | Engine-native prefix cache. Anthropic uses `cache_control`; OpenAI uses `prompt_cache_key`; Grok uses `x-grok-conv-id`; Gemini splits `systemInstruction`. Ollama and Open WebUI have no native prefix-cache field. |
321
322
  | `ai.agent.local_introspect` | Symbol | `failure_only` | `PWN::AI::Agent::Learning.auto_introspect` | End-of-turn introspect policy for local engines: `always` · `failure_only` · `every_n` (with `introspect_every_n`). |
@@ -45,7 +45,7 @@ history # what you've typed → copy into a driver
45
45
  | `pwn-ai-sessions` | List / view / delete transcripts |
46
46
  | `pwn-ai-cron` | List / run scheduled jobs |
47
47
  | `pwn-ai-delegate` | Hand a task to a Swarm persona |
48
- | `toggle-pwn-ai-debug` | Show/hide raw tool_calls JSON |
48
+ | `toggle-debug` | Stage log per request to `/tmp/pwn-ai-DEBUG-<SESSION_ID>-RN.log` (`ai.agent.debug_trace` for TracePoint) |
49
49
  | `toggle-pwn-ai-speaks` | TTS every final answer on/off |
50
50
  | `welcome-banner` | Redraw a random `PWN::Banner` |
51
51
  | `toggle-pager` | Pry pager on/off |
@@ -27,7 +27,7 @@
27
27
  | | |
28
28
  |---|---|
29
29
  | [The `pwn` REPL](pwn-REPL.md) | Pry shell with the whole `PWN::` namespace pre-loaded |
30
- | [`pwn-ai` Autonomous Agent](pwn-ai-Agent.md) | Natural-language TUI + `pwn --ai PROMPT` one-shot · **TaskSummarizer** briefs · iteration budget guard |
30
+ | [`pwn-ai` Autonomous Agent](pwn-ai-Agent.md) | Natural-language TUI + `pwn --ai PROMPT` one-shot · **TaskSummarizer** briefs · CORE_TOOLS until `may_finalize?` |
31
31
  | [CLI Drivers `bin/pwn_*`](CLI-Drivers.md) | 54 headless executables (`pwn` + 53 `pwn_*`) for CI/CD |
32
32
  | [Drivers (build your own)](Drivers.md) | Turn a REPL session into a shipped binary |
33
33
 
@@ -27,7 +27,7 @@ hardware).
27
27
 
28
28
  | Module | Role |
29
29
  |---|---|
30
- | `Loop` | plan → **TaskSummarizer** briefs → dispatch tool_calls → observe → repeat until final answer. Scars do not lower `max_iters` (default 777). Last-iter strips tools only when the original request is already satisfied |
30
+ | `Loop` | plan → **TaskSummarizer** briefs → dispatch tool_calls → observe → repeat until `may_finalize?`. No iteration-budget abort |
31
31
  | **`TaskSummarizer`** | Executive UX: every request gets an English task compass (`emit_plan!` · `about_to` as `task k/n`) - no statement/question/goal type |
32
32
  | `Registry` | JSON-Schema function definitions grouped into 13 **toolsets** · **87 tools** · `CORE_TOOLS` = `DEFAULT_PREFERENCE` (`memory_recall` · `session_recall` · `skills_recall` · `pwn_eval` · `shell` · `mistakes_record` · `mistakes_resolve` · `learning_note_outcome` · `memory_remember`) |
33
33
  | `Dispatch` / `Result` | execute a tool, capture stdout/value/error/duration |
@@ -50,7 +50,7 @@ Repeating shell failures with a known recipe (`command is required`, missing pat
50
50
  | Source | Trigger | What is recorded |
51
51
  |---|---|---|
52
52
  | `:tool` | any tool dispatch returns `success:false` / raises | *automatic* - `Loop.record_metrics` |
53
- | `:loop` | iteration budget exhausted with no final answer | *automatic* - `Loop.run` epilogue |
53
+ | `:loop` | (legacy) iteration budget exhausted Loop.run no longer emits this | *historical* |
54
54
  | `:user_correction` | next user message matches `CORRECTION_RX` (*"no that's wrong"*, *"still broken"*, *"try again"*, ...) | `check_user_correction` - also flips the previous `Learning` outcome to `success:false` |
55
55
  | `:model` | the model itself calls `mistakes_record` | wrong assumption, wrong file, hallucinated API - failures that are **not** dispatch errors |
56
56
  | **`:model` (proactive)** | **`extro_verify(claim:)` returns `:refuted`** | **`Mistakes.record(tool:'assumption', error:'REFUTED ...: <claim>')` - the browser caught the model being wrong about the world *before* a human did** |
@@ -115,13 +115,11 @@ When unresolved `agent_loop` / `assistant_answer` budget-exhaustion fingerprints
115
115
  dominate, the loop marks the budget path hot and tightens *side work* only:
116
116
 
117
117
  - skip counterfactual / red-team forks that would spend more tool rounds
118
- - last-iter strips tools only when the original request is already satisfied
119
- - still flush TaskSummarizer state and Learning on the exhaust path
120
- - end-of-turn critic runs text-only under the same pressure
118
+ - **do not abort** with `[pwn-ai] iteration budget exhausted`
121
119
  - **do not shrink `max_iters`** - yesterday's scars must not abort this request
122
120
 
123
121
  Practice prioritizes those scars with short-horizon "finish the task" prompts.
124
- Raising `ai.agent.max_iters` still sets the hard ceiling.
122
+ `Loop.run` has no hard ceiling — CORE_TOOLS stay until `may_finalize?`.
125
123
 
126
124
  ## Design-priority STATUS
127
125
 
@@ -141,7 +139,7 @@ This is the live control list. Track the outcomes, not source comments.
141
139
  | ops | Nightly diet close | `offline_judge` then scrub + mix + KPI so raw resolve prose does not survive the night |
142
140
  | ops | Shape backfill (`Reward.infer_shape`) | Legacy shapeless rows become `winning_trace` / `revised_answer` when the content warrants |
143
141
  | ops | Mix in prompt | `Metrics.to_context` emits `MIX:` when preference sources are unhealthy |
144
- | must | Budget exhaust | Last-iter force-final only when the original request is already satisfied; skip extra forks when the budget path is hot; **do not shrink max_iters**; still flush the session + introspect |
142
+ | must | Budget exhaust | Loop.run does **not** abort on a round cap or emit `[pwn-ai] iteration budget exhausted`. Keep CORE_TOOLS until `may_finalize?`. Skip extra CF forks when old budget scars are hot; **do not shrink max_iters** |
145
143
 
146
144
  ## Intro and extro join
147
145
 
@@ -171,7 +169,7 @@ This is the live control list. Track the outcomes, not source comments.
171
169
  :max_iters: 777 # hard cap for this request; scars do not lower it
172
170
  :defer_introspect: true # post-answer Learning after the user-visible reply
173
171
  :prompt_cache: true # engine-native prefix cache (not ollama / openwebui)
174
- :tool_preference: [memory_recall, session_recall, skills_recall, pwn_eval, shell, mistakes_record, mistakes_resolve, learning_note_outcome, memory_remember]
172
+ :tool_preference: [memory_recall, session_recall, skills_recall, pwn_eval, shell, mistakes_record, mistakes_resolve, learning_note_outcome, memory_remember, skills_update]
175
173
  ```
176
174
 
177
175
  ## Cron self-improvement
@@ -149,6 +149,6 @@ If you still see doubles, confirm you are on a build with `lib/pwn/ai/agent/task
149
149
 
150
150
  ## Agent stops early / "iteration budget exhausted"
151
151
 
152
- When unresolved budget-exhaustion mistakes dominate, the Loop does **not** shrink `max_iters` (default 777). It only trims side work: skip extra counterfactual forks, and strip tools on the last iteration only if the original request is already satisfied. Check `mistakes_list`, resolve fixed signatures, or raise `ai.agent.max_iters` if a long task still hits the cap. Exhaust paths still run Learning and the task-summary flush.
152
+ When unresolved budget-exhaustion mistakes dominate, the Loop does **not** abort the request. There is no `[pwn-ai] iteration budget exhausted` final. Loop keeps CORE_TOOLS until `may_finalize?`. Ctrl-C stops the turn. OpenGoal stays until a real final so `continue` reloads the same request. Old budget scars do not end this request.
153
153
 
154
154
  [← Home](Home.md)
@@ -27,7 +27,7 @@ commands.
27
27
  | `pwn-ai-cron` | `PWN::Cron` | List/run/toggle scheduled jobs |
28
28
  | `pwn-ai-delegate` | `Agent::Swarm` | Send one request to a persona |
29
29
  | `pwn-irc` | *(deprecated)* | Prints a pointer to `Agent::Swarm` - the IRC daemon transport is gone |
30
- | `toggle-pwn-ai-debug` | `Agent::Loop` | Show/hide raw tool_calls JSON in the TUI |
30
+ | `toggle-debug` | `Agent::Loop` / `Plugins::Log` | Stage log per operator request to `/tmp/pwn-ai-DEBUG-<SESSION_ID>-RN.log`. Set `ai.agent.debug_trace` for per-call TracePoint |
31
31
  | `toggle-pwn-ai-speaks` | `Plugins::Voice` | TTS every final answer on/off |
32
32
  | `welcome-banner` | `PWN::Banner` | Redraw a random banner |
33
33
  | `toggle-pager` | Pry | Page long output on/off |
@@ -143,7 +143,7 @@ There is no request type. Every request gets an English task compass.
143
143
  - Advancement needs a PRM +1 streak or a clear phase shift after tools on the active task (not a blind every-3-tools hop).
144
144
  - REPL contract: `on_tool.call('task', full_summary_text, '')` - result empty, no truncation.
145
145
 
146
- **Long-run pressure:** budget-hot turns skip extra counterfactual forks. The last iteration strips tools only when the original request is already satisfied. Scars and overconfidence do not lower this request's `max_iters` (default 777). Task state and Learning still flush on the exhaust path.
146
+ **Long-run pressure:** Loop.run does not abort on a round cap. Keep CORE_TOOLS until `may_finalize?`. Budget-hot turns skip extra counterfactual forks. Ctrl-C stops the turn.
147
147
 
148
148
  ![TaskSummarizer](diagrams/task-summarizer.svg)
149
149
 
@@ -163,7 +163,6 @@ max_iters: 777 # scars / overconf do not lower this request
163
163
 
164
164
  - SHIFT+ENTER = newline, ENTER = submit.
165
165
  - `back` / `exit` returns to the plain REPL.
166
- - Set `ai.agent.max_iters` in `~/.pwn/pwn.yaml` if long tasks get truncated.
167
166
  - Disable `auto_introspect` during noisy fuzz loops
168
167
  (`learning_auto_introspect_toggle(enabled: false)`), re-enable for the
169
168
  summary turn.
@@ -196,7 +195,7 @@ max_iters: 777 # scars / overconf do not lower this request
196
195
  | `reward_llm_timeout` | `12` | seconds for the cheap ORM chat (clamped 2..30) |
197
196
  | `verify_as_reward` | `nil` (auto) | browser-grounded claim sample policy |
198
197
  | `local_introspect` | `:failure_only` | ollama / openwebui end-of-turn introspect policy |
199
- | `tool_preference` | same list as CORE_TOOLS (`memory_recall`, `session_recall`, `skills_recall`, `pwn_eval`, `shell`, `mistakes_record`, `mistakes_resolve`, `learning_note_outcome`, `memory_remember`) | Rank bonus + Policy suggested-action order |
198
+ | `tool_preference` | same list as CORE_TOOLS (`memory_recall`, `session_recall`, `skills_recall`, `pwn_eval`, `shell`, `mistakes_record`, `mistakes_resolve`, `learning_note_outcome`, `memory_remember`, `skills_update`) | Rank bonus + Policy suggested-action order |
200
199
  | `defer_introspect` | `true` | Post-answer Learning on a background thread |
201
200
  | `prompt_cache` | `true` | Engine-native prefix cache (not Ollama / Open WebUI) |
202
201
 
@@ -527,7 +527,7 @@ module PWN
527
527
  ensure_persona(name: RED_TEAM_NAME, role: 'You are pwn-ai\'s adversarial plan reviewer. Given a numbered tool plan and telemetry from THIS host (tool success rates, known mistakes, environment drift), identify the ONE step most likely to fail and say why in ≤2 lines. Cite the metric/mistake/drift. If the plan is sound reply: SOUND.')
528
528
  telemetry = build_telemetry
529
529
  reply = with_curriculum_guard do
530
- ask_persona(name: RED_TEAM_NAME, request: "GOAL: #{opts[:request].to_s[0, 300]}\n\nPLAN:\n#{opts[:plan].to_s[0, 1_200]}\n\nHOST TELEMETRY:\n#{telemetry}")
530
+ ask_persona(name: RED_TEAM_NAME, request: "GOAL: #{opts[:request].to_s[0, 300]}\n\nPLAN:\n#{opts[:plan].to_s[0, 1_200]}\n\nHOST TELEMETRY:\n#{telemetry}", text_only: true)
531
531
  end
532
532
  return nil if reply.to_s.strip.upcase.start_with?('SOUND') || reply.to_s.strip.empty?
533
533
 
@@ -1086,7 +1086,7 @@ module PWN
1086
1086
  private_class_method def self.ask_persona(opts = {})
1087
1087
  return nil unless defined?(Swarm)
1088
1088
 
1089
- r = Swarm.ask(name: opts[:name], request: opts[:request])
1089
+ r = Swarm.ask(name: opts[:name], request: opts[:request], text_only: opts[:text_only])
1090
1090
  r.is_a?(Hash) ? r[:reply].to_s : r.to_s
1091
1091
  rescue StandardError
1092
1092
  nil
@@ -301,7 +301,7 @@ module PWN
301
301
  ].freeze
302
302
  STORE_TOOLS = %w[
303
303
  memory_remember mistakes_record mistakes_resolve
304
- learning_note_outcome skill_create skill_add_reference
304
+ learning_note_outcome skill_create skill_add_reference skills_update
305
305
  ].freeze
306
306
 
307
307
  public_class_method def self.effect(opts = {})
@@ -408,6 +408,65 @@ module PWN
408
408
  out.merge(saved: true)
409
409
  end
410
410
 
411
+ # Fold RL artefacts (mistakes / structured_fix / an explicit lesson)
412
+ # into an existing skill. Does not create skills (use distill_skill /
413
+ # skill_create). Does not write loop-law. Dedupes by mistake signature.
414
+ public_class_method def self.update_skill(opts = {})
415
+ dry = opts[:dry_run] ? true : false
416
+ lesson = opts[:lesson].to_s.strip
417
+ notes = rl_skill_notes(
418
+ signature: opts[:signature],
419
+ request: opts[:request] || opts[:query],
420
+ lesson: lesson
421
+ )
422
+ return { updated: false, reason: 'no rl notes' } if notes.empty?
423
+
424
+ target = locate_skill_for_update(
425
+ name: opts[:name],
426
+ query: opts[:query] || opts[:request] || notes.map { |n| n[:text] }.join(' ')
427
+ )
428
+ return { updated: false, reason: 'no matching skill', notes: notes } unless target
429
+
430
+ body = skill_body_without_frontmatter(meta: target[:meta])
431
+ added = []
432
+ notes.each do |note|
433
+ next if body.include?(note[:id])
434
+
435
+ added << note
436
+ end
437
+ return { updated: false, name: target[:name], reason: 'already folded', notes: notes } if added.empty?
438
+
439
+ block = added.map { |n| "- [#{n[:id]}] #{n[:text]}" }.join("\n")
440
+ body = if body.match?(/^\#{1,3}\s*RL feedback\s*$/i)
441
+ "#{body.rstrip}\n#{block}\n"
442
+ else
443
+ "#{body.rstrip}\n\n## RL feedback\n#{block}\n"
444
+ end
445
+ return { updated: false, dry_run: true, name: target[:name], added: added.map { |n| n[:text] } } if dry
446
+
447
+ root = skills_dir
448
+ out = PWN::Config.write_skill(
449
+ name: target[:name],
450
+ description: target[:meta][:description],
451
+ content: body,
452
+ references: target[:meta][:references],
453
+ license: (target[:meta][:frontmatter] || {})['license'],
454
+ allowed_tools: target[:meta][:allowed_tools],
455
+ metadata: (target[:meta][:frontmatter] || {})['metadata'],
456
+ pwn_skills_path: root
457
+ )
458
+ PWN::Config.load_skills(pwn_skills_path: root) if PWN::Config.respond_to?(:load_skills)
459
+ note_outcome(
460
+ task: "skills_update:#{target[:name]}",
461
+ success: true,
462
+ details: "Folded #{added.length} RL note(s)",
463
+ tags: %w[skill rl]
464
+ )
465
+ out.merge(updated: true, added: added.map { |n| n[:id] })
466
+ rescue StandardError => e
467
+ { updated: false, error: "#{e.class}: #{e.message}" }
468
+ end
469
+
411
470
  # Supported Method Parameters::
412
471
  # report = PWN::AI::Agent::Learning.reflect(
413
472
  # session_id: 'required - PWN::Sessions id to analyse',
@@ -490,28 +549,14 @@ module PWN
490
549
 
491
550
  proxy_ok = infer_success(session_id: session_id, final: opts[:final])
492
551
 
493
- # S3 critic — BEFORE reward so verdict is evidence.
494
- # P24/P0 — budget_hot or soft-cap → text_only or skip.
495
- force_critic = begin
496
- eng = (PWN::Env.dig(:ai, :active) if defined?(PWN::Env))
497
- cal = defined?(Metrics) ? Metrics.calibration(engine: eng) : { n: 0 }
498
- cal[:n].to_i >= 8 && (cal[:brier].to_f > 0.35 || cal[:overconfidence].to_f > 0.25)
499
- rescue StandardError
500
- false
501
- end
502
- # P0 — when W1 mix urgently needs :critic pairs, prefer running critic
503
- need_critic_mix = begin
504
- mix = defined?(Reward) && Reward.respond_to?(:generator_mix) ? Reward.generator_mix : {}
505
- Array(mix[:urgent]).include?('critic')
506
- rescue StandardError
507
- false
508
- end
509
-
510
552
  crit = { verdict: :pass, source: :skipped }
511
- # Single skip path (Lint/DuplicateBranch): hard-budget OR no Curriculum.
512
- if !defined?(Curriculum) || (over_hard.call && !need_critic_mix)
553
+ # Live-turn critic is always text-only. A tool-armed persona is
554
+ # another Loop.run of the same goal (opens more browsers, never
555
+ # returns the operator answer). Cron/practice still call
556
+ # Curriculum.critic without text_only.
557
+ if !defined?(Curriculum) || over_hard.call
513
558
  stages_skipped << :critic
514
- elsif budget_hot || (over_soft.call && !force_critic && !need_critic_mix)
559
+ else
515
560
  stages_run << :critic_text_only
516
561
  crit = Curriculum.critic(
517
562
  request: opts[:request],
@@ -519,18 +564,6 @@ module PWN
519
564
  session_id: session_id,
520
565
  text_only: true
521
566
  )
522
- elsif force_critic
523
- stages_run << :critic_forced
524
- prev = (PWN::Env[:ai][:agent][:critic] if defined?(PWN::Env) && PWN::Env[:ai].is_a?(Hash) && PWN::Env[:ai][:agent].is_a?(Hash))
525
- begin
526
- PWN::Env[:ai][:agent][:critic] = true if defined?(PWN::Env) && PWN::Env[:ai].is_a?(Hash) && PWN::Env[:ai][:agent].is_a?(Hash) && !PWN::Env[:ai][:agent].frozen?
527
- crit = Curriculum.critic(request: opts[:request], final: opts[:final], session_id: session_id)
528
- ensure
529
- PWN::Env[:ai][:agent][:critic] = prev if defined?(PWN::Env) && PWN::Env[:ai].is_a?(Hash) && PWN::Env[:ai][:agent].is_a?(Hash) && !PWN::Env[:ai][:agent].frozen?
530
- end
531
- else
532
- stages_run << :critic
533
- crit = Curriculum.critic(request: opts[:request], final: opts[:final], session_id: session_id)
534
567
  end
535
568
 
536
569
  # R1 judge — always attempt (heuristic is cheap; LLM gated inside)
@@ -1097,6 +1130,91 @@ module PWN
1097
1130
  !final.strip.empty?
1098
1131
  end
1099
1132
 
1133
+ private_class_method def self.rl_skill_notes(opts = {})
1134
+ notes = []
1135
+ day = Time.now.utc.strftime('%Y-%m-%d')
1136
+ lesson = opts[:lesson].to_s.strip
1137
+ notes << { id: "lesson:#{Digest::SHA256.hexdigest(lesson)[0, 8]}", text: "[#{day}] #{lesson[0, 400]}" } unless lesson.empty?
1138
+ return notes unless defined?(Mistakes)
1139
+
1140
+ rows = []
1141
+ if opts[:signature].to_s.strip.empty?
1142
+ rows = Mistakes.top(limit: 8, unresolved_only: false)
1143
+ else
1144
+ hit = Mistakes.find(signature: opts[:signature].to_s)
1145
+ rows = hit ? [hit] : []
1146
+ end
1147
+ req = opts[:request].to_s.downcase
1148
+ rows.each do |mistake|
1149
+ next unless mistake.is_a?(Hash)
1150
+
1151
+ err = mistake[:error].to_s.downcase
1152
+ next if (err.include?('budget') || err.include?('iteration')) && !req.match?(/budget|iterat|exhaust/)
1153
+
1154
+ fix = mistake[:fix].to_s
1155
+ sf = mistake[:structured_fix]
1156
+ trace = sf.is_a?(Hash) ? sf[:winning_trace].to_s : ''
1157
+ next if fix.empty? && trace.empty?
1158
+
1159
+ sig = mistake[:signature].to_s
1160
+ next if sig.empty?
1161
+
1162
+ bit = "[#{day}] [#{sig}] #{mistake[:tool]}: #{mistake[:error].to_s[0, 120]}"
1163
+ bit = "#{bit} — FIX: #{fix[0, 160]}" unless fix.empty?
1164
+ bit = "#{bit} trace: #{trace[0, 160]}" unless trace.empty?
1165
+ notes << { id: sig, text: bit }
1166
+ end
1167
+ notes.uniq { |n| n[:id] }.first(6)
1168
+ rescue StandardError
1169
+ notes
1170
+ end
1171
+
1172
+ private_class_method def self.locate_skill_for_update(opts = {})
1173
+ return nil unless defined?(PWN::Skills) && PWN::Skills.is_a?(Hash)
1174
+
1175
+ if opts[:name].to_s.strip.empty?
1176
+ query = opts[:query].to_s.downcase
1177
+ tokens = query.scan(/[a-z0-9_-]{3,}/).uniq
1178
+ return nil if tokens.empty?
1179
+
1180
+ scored = PWN::Skills.map do |name, meta|
1181
+ next unless meta.is_a?(Hash)
1182
+
1183
+ hay = "#{name} #{meta[:description]} #{meta[:content]}".downcase
1184
+ score = tokens.count { |tok| hay.include?(tok) }
1185
+ [name.to_s, meta, score]
1186
+ end.compact
1187
+ best = scored.max_by { |_, _, score| score }
1188
+ return nil unless best && best[2] >= 2
1189
+
1190
+ return { name: best[0], meta: best[1] }
1191
+ end
1192
+
1193
+ key = opts[:name].to_s
1194
+ meta = PWN::Skills[key.to_sym] || PWN::Skills[key]
1195
+ if meta.nil? && defined?(PWN::Config)
1196
+ meta = PWN::Skills[PWN::Config.sanitize_skill_name(name: key).to_sym]
1197
+ key = PWN::Config.sanitize_skill_name(name: key) if meta
1198
+ end
1199
+ return nil unless meta.is_a?(Hash)
1200
+
1201
+ { name: key.to_s, meta: meta }
1202
+ rescue StandardError
1203
+ nil
1204
+ end
1205
+
1206
+ private_class_method def self.skill_body_without_frontmatter(opts = {})
1207
+ meta = opts[:meta] || {}
1208
+ raw = meta[:content].to_s
1209
+ return raw unless defined?(PWN::Config) && PWN::Config.respond_to?(:parse_skill_frontmatter)
1210
+
1211
+ parsed = PWN::Config.parse_skill_frontmatter(content: raw)
1212
+ body = parsed[:body].to_s
1213
+ body.empty? ? raw : body
1214
+ rescue StandardError
1215
+ opts.dig(:meta, :content).to_s
1216
+ end
1217
+
1100
1218
  private_class_method def self.skills_dir
1101
1219
  if defined?(PWN::Config) && PWN::Config.respond_to?(:pwn_skills_path)
1102
1220
  PWN::Config.pwn_skills_path