pwn 0.5.686 → 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.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/.ruby-version +1 -1
- data/Gemfile +3 -3
- data/README.md +3 -4
- data/documentation/Agent-Tool-Registry.md +2 -2
- data/documentation/Configuration.md +5 -4
- data/documentation/General-PWN-Usage.md +1 -1
- data/documentation/Home.md +1 -1
- data/documentation/How-PWN-Works.md +1 -1
- data/documentation/Mistakes.md +1 -1
- data/documentation/Reinforcement-Learning.md +4 -6
- data/documentation/Troubleshooting.md +1 -1
- data/documentation/pwn-REPL.md +1 -1
- data/documentation/pwn-ai-Agent.md +2 -3
- data/lib/pwn/ai/agent/curriculum.rb +2 -2
- data/lib/pwn/ai/agent/dispatch.rb +1 -1
- data/lib/pwn/ai/agent/learning.rb +150 -32
- data/lib/pwn/ai/agent/loop.rb +291 -129
- data/lib/pwn/ai/agent/prompt_builder.rb +47 -29
- data/lib/pwn/ai/agent/registry.rb +2 -1
- data/lib/pwn/ai/agent/reward.rb +9 -0
- data/lib/pwn/ai/agent/swarm.rb +3 -1
- data/lib/pwn/ai/agent/task_summarizer.rb +23 -6
- data/lib/pwn/ai/agent/tool_guard.rb +109 -0
- data/lib/pwn/ai/agent/tools/ruby_eval.rb +44 -20
- data/lib/pwn/ai/agent/tools/shell.rb +17 -4
- data/lib/pwn/ai/agent/tools/skills.rb +63 -2
- data/lib/pwn/ai/anthropic.rb +25 -5
- data/lib/pwn/ai/gemini.rb +25 -5
- data/lib/pwn/ai/grok.rb +30 -13
- data/lib/pwn/ai/http_retry.rb +78 -0
- data/lib/pwn/ai/ollama.rb +25 -5
- data/lib/pwn/ai/open_ai.rb +26 -13
- data/lib/pwn/ai/open_web_ui.rb +25 -5
- data/lib/pwn/ai.rb +1 -0
- data/lib/pwn/config.rb +1 -1
- data/lib/pwn/plugins/log.rb +183 -41
- data/lib/pwn/plugins/repl.rb +50 -15
- data/lib/pwn/plugins/transparent_browser.rb +16 -2
- data/lib/pwn/sessions.rb +50 -1
- data/lib/pwn/version.rb +1 -1
- data/spec/integration/reinforced_feedback_loop_spec.rb +9 -11
- data/spec/lib/pwn/ai/agent/loop_spec.rb +180 -20
- data/spec/lib/pwn/ai/agent/prompt_builder_spec.rb +3 -0
- data/spec/lib/pwn/ai/agent/registry_spec.rb +1 -1
- data/spec/lib/pwn/ai/agent/reward_spec.rb +13 -0
- data/spec/lib/pwn/ai/agent/swarm_spec.rb +6 -0
- data/spec/lib/pwn/ai/agent/task_summarizer_spec.rb +36 -0
- data/spec/lib/pwn/ai/agent/tool_guard_spec.rb +46 -0
- data/spec/lib/pwn/ai/agent/tools/ruby_eval_spec.rb +24 -0
- data/spec/lib/pwn/ai/agent/tools/shell_spec.rb +10 -0
- data/spec/lib/pwn/ai/agent/tools/skills_spec.rb +29 -0
- data/spec/lib/pwn/ai/grok_spec.rb +22 -0
- data/spec/lib/pwn/ai/http_retry_spec.rb +55 -0
- data/spec/lib/pwn/plugins/log_spec.rb +170 -19
- data/spec/lib/pwn/plugins/repl_pwn_vault_spec.rb +14 -4
- data/spec/lib/pwn/plugins/transparent_browser_spec.rb +8 -0
- data/spec/lib/pwn/sessions_spec.rb +24 -0
- data/third_party/pwn_rdoc.jsonl +41 -5
- data/tmp_pwn_critic_skills_flaw.txt +9 -0
- metadata +11 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 57da0c5ba0b3110683a6cad1e71a400dc1d58ef63337016ff1d6b710bbc293e1
|
|
4
|
+
data.tar.gz: 125343370941538d882d38f8b3b2bec6a89d973db66dc5ad724e85ec073a2583
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: eb44627c68dd94bedf56449d7a5337b1042a69ecf080e17300dce2e907643a6c14e13cf32064dfbc384885b501e43b1c0e4f6bd203549dd805e335f6948eea5f
|
|
7
|
+
data.tar.gz: 5031073e597b8562dd5ff6718fb37faad67d0bdab0afce0750e518762c3398d16aba22e0486859ae56e8539455121ac7c2db457a9e6348e6ee366ba3b0534a15
|
data/.gitignore
CHANGED
data/.ruby-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
4.0.
|
|
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.
|
|
23
|
+
gem 'bundler', '>=4.0.19'
|
|
24
24
|
gem 'bundler-audit', '>=0.9.3'
|
|
25
|
-
gem 'bunny', '3.
|
|
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.
|
|
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.
|
|
125
|
-
|
|
126
|
-
|
|
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 #
|
|
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::
|
|
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-
|
|
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 |
|
data/documentation/Home.md
CHANGED
|
@@ -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 ·
|
|
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
|
|
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 |
|
data/documentation/Mistakes.md
CHANGED
|
@@ -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
|
|
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
|
-
-
|
|
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
|
-
|
|
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 |
|
|
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**
|
|
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)
|
data/documentation/pwn-REPL.md
CHANGED
|
@@ -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-
|
|
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:**
|
|
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
|

|
|
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
|
-
#
|
|
512
|
-
|
|
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
|
-
|
|
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
|