pwn 0.5.682 → 0.5.683

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/documentation/Agent-Tool-Registry.md +11 -7
  3. data/documentation/Configuration.md +4 -2
  4. data/documentation/How-PWN-Works.md +1 -1
  5. data/documentation/Reinforcement-Learning.md +1 -1
  6. data/documentation/diagrams/agent-tool-registry.svg +1 -1
  7. data/documentation/diagrams/dot/agent-tool-registry.dot +1 -1
  8. data/documentation/pwn-ai-Agent.md +1 -1
  9. data/lib/pwn/ai/agent/loop.rb +40 -6
  10. data/lib/pwn/ai/agent/policy.rb +2 -3
  11. data/lib/pwn/ai/agent/prompt_builder.rb +13 -12
  12. data/lib/pwn/ai/agent/registry.rb +6 -9
  13. data/lib/pwn/ai/agent/tools/sessions.rb +32 -0
  14. data/lib/pwn/ai/agent/tools/skills.rb +56 -0
  15. data/lib/pwn/ai/anthropic.rb +0 -1
  16. data/lib/pwn/ai/gemini.rb +0 -1
  17. data/lib/pwn/ai/grok.rb +0 -1
  18. data/lib/pwn/ai/ollama.rb +0 -1
  19. data/lib/pwn/ai/open_ai.rb +0 -1
  20. data/lib/pwn/ai/open_web_ui.rb +0 -1
  21. data/lib/pwn/config.rb +1 -1
  22. data/lib/pwn/plugins/repl.rb +37 -0
  23. data/lib/pwn/plugins/tty_spinner.rb +55 -6
  24. data/lib/pwn/sessions.rb +82 -0
  25. data/lib/pwn/version.rb +1 -1
  26. data/spec/integration/prompt_builder_spec.rb +3 -3
  27. data/spec/lib/pwn/ai/agent/loop_spec.rb +63 -1
  28. data/spec/lib/pwn/ai/agent/prompt_builder_spec.rb +7 -3
  29. data/spec/lib/pwn/ai/agent/registry_spec.rb +16 -9
  30. data/spec/lib/pwn/ai/agent/tools/sessions_spec.rb +5 -0
  31. data/spec/lib/pwn/ai/agent/tools/skills_spec.rb +16 -0
  32. data/spec/lib/pwn/ai/red_team/test_case_engine_spec.rb +20 -0
  33. data/spec/lib/pwn/plugins/repl_spec.rb +9 -0
  34. data/spec/lib/pwn/plugins/tty_spinner_spec.rb +30 -0
  35. data/spec/lib/pwn/sessions_spec.rb +29 -0
  36. data/spec/spec_helper.rb +6 -0
  37. data/third_party/pwn_rdoc.jsonl +6 -0
  38. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8f46629cb3abad0974a4fe9bd5d2936bb42c90e304986261cf0106775ffb8de9
4
- data.tar.gz: 56fc226ba599b541c47e66c38f7e3a6d142c019442a00707db8f9d8d12caee74
3
+ metadata.gz: 138deb9b9916fd71329a5898d6cb49ab53751e597f408177aee7682704f33500
4
+ data.tar.gz: 77f88100898efbff17bea33bbd2efa186afecf2776c02d3e577a71b26d1b2d3b
5
5
  SHA512:
6
- metadata.gz: aadc0dad32c3ac362ee269637c0308a1f89e400c311dd5b69d9a7c2e944cc57698799c85b97c51ea3024207f18703b32735faacf394a7d2e3f91e6945efb724e
7
- data.tar.gz: 748bedcea767a060882928c3697cc96a3ffa5f316cbca209d7e4dc870bcbaef6737ebb582d86b76dcd9c5f6624cd46c6d646b94dec8f0dbcd20af9a1cc0bbdf9
6
+ metadata.gz: 265a38e4abaead447360e549c98f88dc0a3d3220358d0e30c9e9419d6c1b53dd558dac87b2ce093e968b53bbab9051db7032b405d5835194c33934acd64ecb4d
7
+ data.tar.gz: 7c0e34b2be0c96ddef980ee81493f5b2d394680ca956f0b6208cc32758fda13f212670febca563092cd5b0331ac46464bdecd0f6c88d2f7a15744f4b1cd79d48
@@ -13,8 +13,8 @@ toolsets; the JSON-Schema for each tool is what the model actually sees.
13
13
  | `terminal` | `shell` | `Open3.capture3` on the host, after `PWN::AI::Agent::ToolGuard` |
14
14
  | `pwn` | `pwn_eval` | `TOPLEVEL_BINDING.eval` in the live REPL process, after `ToolGuard` |
15
15
  | `memory` | `memory_remember` · `memory_recall` · `memory_forget` · `memory_clear` · **`memory_lean`** | `PWN::Memory` → `~/.pwn/memory.json` |
16
- | `skills` | `skill_list` · `skill_view` · `skill_create` · `skill_add_reference` · `skill_delete` · `skill_migrate_legacy` | `~/.pwn/skills/<name>/SKILL.md` (**[agentskills.io](https://agentskills.io) spec**; legacy flat `*.md` auto-migrated) |
17
- | `sessions` | `sessions_list` · `sessions_view` · `sessions_current` · `sessions_delete` · `sessions_stats` · **`sessions_lean`** | `PWN::Sessions` → `~/.pwn/sessions/` |
16
+ | `skills` | **`skills_recall`** · `skill_list` · `skill_view` · `skill_create` · `skill_add_reference` · `skill_delete` · `skill_migrate_legacy` | `~/.pwn/skills/<name>/SKILL.md` (**[agentskills.io](https://agentskills.io) spec**; legacy flat `*.md` auto-migrated) |
17
+ | `sessions` | **`session_recall`** · `sessions_list` · `sessions_view` · `sessions_current` · `sessions_delete` · `sessions_stats` · **`sessions_lean`** | `PWN::Sessions` → `~/.pwn/sessions/` |
18
18
  | `learning` | `learning_note_outcome` · `learning_reflect` · `learning_distill_skill` · `learning_stats` · `learning_outcomes` · `learning_consolidate` · `learning_reset` · `learning_auto_introspect_toggle` · **`learning_gc_stores`** · **`learning_purge_noise`** · **`mistakes_list`** · **`mistakes_record`** · **`mistakes_resolve`** · **`mistakes_reset`** · **`mistakes_lean`** · **`reward_judge`** · **`reward_prm`** · **`reward_sentinel`** · **`reward_preferences`** · **`reward_export_dpo`** · **`reward_warm_sentinel`** · **`reward_scrub_preferences`** · **`reward_preference_balance`** · **`curriculum_practice`** · **`curriculum_train`** · **`curriculum_hindsight`** · **`curriculum_offline_judge`** · **`curriculum_preference_balance`** | `PWN::AI::Agent::Learning` + `Mistakes` + `Reward` + `Curriculum` → `~/.pwn/learning.jsonl` + `~/.pwn/mistakes.json` + `~/.pwn/preferences.jsonl` + `~/.pwn/curriculum/` + `~/.pwn/finetune/` |
19
19
  | `reward` | **`reward_generator_mix`** | `PWN::AI::Agent::Reward.generator_mix` → online preference source-mix controller (`preferences.jsonl`) |
20
20
  | `curriculum` | **`curriculum_practice_kpi`** | `PWN::AI::Agent::Curriculum.practice_kpi` → `~/.pwn/curriculum_kpi.jsonl` |
@@ -56,8 +56,9 @@ user request through as `relevance:`, `Registry.definitions` shrinks the
56
56
  pool to:
57
57
 
58
58
  ```text
59
- CORE_TOOLS = shell · pwn_eval · memory_remember · memory_recall
60
- mistakes_record · mistakes_resolve · learning_note_outcome
59
+ CORE_TOOLS / DEFAULT_PREFERENCE (same list):
60
+ memory_recall · session_recall · skills_recall · pwn_eval · shell
61
+ mistakes_record · mistakes_resolve · learning_note_outcome · memory_remember
61
62
  + top-K keyword-ranked matches for THIS request
62
63
  (ties break on Metrics per-engine success_rate, then
63
64
  ai.agent.tool_preference)
@@ -80,7 +81,7 @@ When keyword fit and other rank scores tie, the registry prefers this
80
81
  default order:
81
82
 
82
83
  ```text
83
- memory_recall · pwn_eval · shell
84
+ memory_recall · session_recall · skills_recall · pwn_eval · shell
84
85
  mistakes_record · mistakes_resolve · learning_note_outcome · memory_remember
85
86
  ```
86
87
 
@@ -88,9 +89,12 @@ Set `ai.agent.tool_preference` in `~/.pwn/pwn.yaml`, or pass `order:` /
88
89
  `preference:` into `Registry.definitions`, `.rank`, or `.apply_preference`.
89
90
  An explicit empty list turns preference off (no Env / default fallback).
90
91
 
92
+ Learned facts and this session are injected (MEMORY / RECENT TURNS), not
93
+ first tools. Preference then lists `pwn_eval` before `shell`. `sessions_view`
94
+ is not a CORE tool. There is no separate ACT_PREFERENCE.
95
+
91
96
  Keyword fit stays the primary signal. Preference is a smaller bonus plus a
92
- stable sort after the router slims the pool, so `memory_recall` wins a
93
- tie against `shell` without hiding a better keyword match.
97
+ stable sort after the router slims the pool.
94
98
 
95
99
  `Policy` uses the same list when it suggests a next action in the prompt.
96
100
 
@@ -133,8 +133,10 @@ ai:
133
133
  shell_bash: false # true -> run shell via bash -lc. Default is /bin/sh.
134
134
  plan_first: ~ # Plan-then-act pre-pass. nil = auto (true when ai.active is ollama or openwebui).
135
135
  tool_router: ~ # Dynamic tool-set slimming. nil = auto (true for ollama / openwebui).
136
- tool_preference: # Operator-tunable tool order. Rank bonus + Policy suggested-action list.
136
+ tool_preference: # Same order as CORE_TOOLS. Current session is injected; then memory_recall, session_recall, skills_recall, pwn_eval, shell.
137
137
  - memory_recall
138
+ - session_recall
139
+ - skills_recall
138
140
  - pwn_eval
139
141
  - shell
140
142
  - mistakes_record
@@ -315,7 +317,7 @@ PWN::Config.refresh_env
315
317
  | `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
318
  | `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
319
  | `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`, `pwn_eval`, `shell`, `mistakes_record`, `mistakes_resolve`, `learning_note_outcome`, `memory_remember` | `PWN::AI::Agent::Registry.preference_order` / `.rank` / `.apply_preference`, `Policy` | Operator-tunable tool order. Keyword fit stays primary; act/recon kinds lead with `shell`/`pwn_eval`. Explicit empty list disables preference. |
320
+ | `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; then `memory_recall`, `session_recall`, `skills_recall`, `pwn_eval`, `shell`. Explicit empty list disables preference. |
319
321
  | `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
322
  | `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
323
  | `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`). |
@@ -29,7 +29,7 @@ hardware).
29
29
  |---|---|
30
30
  | `Loop` | plan → **TaskSummarizer** briefs → dispatch tool_calls → observe → repeat until final answer; tightens runway when recent turns exhausted the budget |
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
- | `Registry` | JSON-Schema function definitions grouped into 13 **toolsets** · **85 tools** · `CORE_TOOLS` default pool · kind-aware `tool_preference` |
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`) |
33
33
  | `Dispatch` / `Result` | execute a tool, capture stdout/value/error/duration |
34
34
  | `PromptBuilder` | inject MEMORY / SKILLS / LEARNING / **KNOWN MISTAKES + FIXES** / METRICS / **POLICY** / EXTROSPECTION / RECENT TURNS |
35
35
  | `Metrics` · `Learning` · `Reflect` · **`Policy`** | **introspection** - how well am I doing? (Policy is live Q / REINFORCE, advisory rank only) |
@@ -169,7 +169,7 @@ This table is the live control list. Track the outcomes, not source comments.
169
169
  :max_iters: 75 # hard cap; budget pressure may lower effective value
170
170
  :defer_introspect: true # post-answer Learning after the user-visible reply
171
171
  :prompt_cache: true # engine-native prefix cache (not ollama / openwebui)
172
- :tool_preference: [memory_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]
173
173
  ```
174
174
 
175
175
  ## Cron self-improvement
@@ -29,7 +29,7 @@
29
29
  <text xml:space="preserve" text-anchor="middle" x="105.12" y="-521.82" font-family="sans-Serif" font-size="12.00" fill="#0f172a">JSON&#45;Schema function defs</text>
30
30
  <text xml:space="preserve" text-anchor="middle" x="105.12" y="-507.57" font-family="sans-Serif" font-size="12.00" fill="#0f172a">rank(query) · CORE_TOOLS</text>
31
31
  <text xml:space="preserve" text-anchor="middle" x="105.12" y="-493.32" font-family="sans-Serif" font-size="12.00" fill="#0f172a">tool_router → CORE + top&#45;K</text>
32
- <text xml:space="preserve" text-anchor="middle" x="105.12" y="-479.07" font-family="sans-Serif" font-size="12.00" fill="#0f172a">tool_preference (memory_recall first)</text>
32
+ <text xml:space="preserve" text-anchor="middle" x="105.12" y="-479.07" font-family="sans-Serif" font-size="12.00" fill="#0f172a">tool_preference (pwn_eval then shell)</text>
33
33
  </g>
34
34
  <!-- terminal -->
35
35
  <g id="node2" class="node">
@@ -9,7 +9,7 @@ digraph "PWN_Agent_Tool_Registry" {
9
9
  shape=box, penwidth=1.3, color="#334155", fontcolor="#0f172a"];
10
10
  edge [color="#94a3b8", penwidth=1.1, arrowsize=0.7];
11
11
 
12
- Registry [label="Registry\nJSON-Schema function defs\nrank(query) · CORE_TOOLS\ntool_router → CORE + top-K\ntool_preference (memory_recall first)", fillcolor="#c4b5fd", fontsize=12, penwidth=2];
12
+ Registry [label="Registry\nJSON-Schema function defs\nrank(query) · CORE_TOOLS\ntool_router → CORE + top-K\ntool_preference (pwn_eval then shell)", fillcolor="#c4b5fd", fontsize=12, penwidth=2];
13
13
 
14
14
  subgraph cluster_ts {
15
15
  label="Toolsets"; fontcolor="#a7f3d0"; style=rounded;
@@ -197,7 +197,7 @@ max_iters: 75 # budget pressure may lower the effective cap (s
197
197
  | `reward_llm_timeout` | `12` | seconds for the cheap ORM chat (clamped 2..30) |
198
198
  | `verify_as_reward` | `nil` (auto) | browser-grounded claim sample policy |
199
199
  | `local_introspect` | `:failure_only` | ollama / openwebui end-of-turn introspect policy |
200
- | `tool_preference` | CORE_TOOLS list (act leads with shell) | Rank bonus + Policy suggested-action order |
200
+ | `tool_preference` | same list as CORE_TOOLS (`memory_recall`, `session_recall`, `skills_recall`, `pwn_eval`, `shell`) | Rank bonus + Policy suggested-action order |
201
201
  | `defer_introspect` | `true` | Post-answer Learning on a background thread |
202
202
  | `prompt_cache` | `true` | Engine-native prefix cache (not Ollama / Open WebUI) |
203
203
 
@@ -321,9 +321,15 @@ module PWN
321
321
  LOOKUP_REQUEST_RX = /
322
322
  \b(what\s+is\s+my|hostname|uname|cwd|whoami|status|version|how\s+many)\b
323
323
  /ix
324
- HOST_PATH_RX = %r{(?:/|\./)[\w./-]+\.\w+}
324
+ # Real filesystem paths only — not https://host.tld (that was matching //host.tld).
325
+ HOST_PATH_RX = %r{(?:(?<![.:/])/(?!/)|\./)[\w./-]+\.\w+}
326
+ BROWSER_REQUEST_RX = /
327
+ TransparentBrowser|browser_obj|\bdevtools\b|
328
+ \b(navigate|dump_links|headless_?chrome|watir)\b
329
+ /ix
330
+ BROWSER_EVIDENCE_RX = %r{https?://|dump_links|TransparentBrowser|\.close\b|browser_obj}i
325
331
 
326
- # True only when the ask needs a live host/file effect. World-knowledge
332
+ # True only when the ask needs a live host/file/browser effect. World-knowledge
327
333
  # questions ("what color is a cherry") do not.
328
334
  public_class_method def self.needs_host_work?(opts = {})
329
335
  request = opts[:request].to_s
@@ -331,6 +337,7 @@ module PWN
331
337
  return true if request.match?(ACT_REQUEST_RX)
332
338
  return true if request.match?(LOOKUP_REQUEST_RX)
333
339
  return true if request.match?(HOST_PATH_RX)
340
+ return true if request.match?(BROWSER_REQUEST_RX)
334
341
 
335
342
  false
336
343
  rescue StandardError
@@ -377,10 +384,16 @@ module PWN
377
384
 
378
385
  return false if request.match?(LOOKUP_REQUEST_RX) && tools.any? && blob.length >= 20
379
386
  return true if tools.empty?
380
- return false if blob.match?(MUTATION_EVIDENCE_RX)
381
- return false if request_path_evidenced?(request: request, blob: blob)
382
387
 
383
- true
388
+ if request.match?(ACT_REQUEST_RX) || request.match?(HOST_PATH_RX)
389
+ return false if blob.match?(MUTATION_EVIDENCE_RX)
390
+ return false if request_path_evidenced?(request: request, blob: blob)
391
+
392
+ return true
393
+ end
394
+ return false if request.match?(BROWSER_REQUEST_RX) && blob.match?(BROWSER_EVIDENCE_RX)
395
+
396
+ blob.length < 20
384
397
  rescue StandardError
385
398
  false
386
399
  end
@@ -1326,6 +1339,7 @@ module PWN
1326
1339
  last\s+thing\s+(?:i|you)\s+said
1327
1340
  )\b
1328
1341
  /ix
1342
+ LAST_SESSION_RX = /\b(?:in|from|of)\s+(?:the\s+)?(?:last|previous|prior)\s+session\b|\blast\s+session\b/i
1329
1343
 
1330
1344
  # Pure greeting / light smalltalk — never full :act tool loop.
1331
1345
  # Anchored short forms only so "hi, please scan X" stays :act/:recon_act.
@@ -1393,6 +1407,14 @@ module PWN
1393
1407
  return :recall unless doing
1394
1408
  end
1395
1409
 
1410
+ if req.match?(LAST_SESSION_RX) && !req.match?(HOWTO_RX) && !req.match?(LIVE_RECON_RX)
1411
+ doing = req.match?(
1412
+ /\b(implement|fix|patch|refactor|run|execute|scan|write|edit|
1413
+ change|deploy|install|build|compile|commit|push)\b/ix
1414
+ )
1415
+ return :recall unless doing
1416
+ end
1417
+
1396
1418
  # Live-action recon takes precedence over bare "how to" when both appear
1397
1419
  # only if the user clearly asks the agent to do the sweep here.
1398
1420
  live = req.match?(LIVE_RECON_RX) && req.match?(
@@ -1715,6 +1737,18 @@ module PWN
1715
1737
  session_id = opts[:session_id]
1716
1738
  system_role_content = opts[:system_role_content].to_s
1717
1739
  target = recall_target(request: request)
1740
+ last_session = request.match?(LAST_SESSION_RX)
1741
+ if last_session && defined?(PWN::Sessions) && PWN::Sessions.respond_to?(:previous_id)
1742
+ prev = PWN::Sessions.previous_id(exclude_session_id: session_id)
1743
+ if prev.to_s.empty?
1744
+ txt = 'I do not have a previous session transcript yet.'
1745
+ append_session(session_id: opts[:session_id], role: 'user', content: request)
1746
+ append_session(session_id: opts[:session_id], role: 'assistant', content: txt)
1747
+ return txt
1748
+ end
1749
+
1750
+ session_id = prev
1751
+ end
1718
1752
 
1719
1753
  prior_user = nil
1720
1754
  prior_asst = nil
@@ -1757,7 +1791,7 @@ module PWN
1757
1791
  # User-target / fallback: skip meta intermediate recall asks so
1758
1792
  # "what did I just say?" after a nested chain still surfaces the
1759
1793
  # original utterance when appropriate; default stays newest.
1760
- skip_meta = target == :assistant
1794
+ skip_meta = target == :assistant || last_session
1761
1795
  prior_user = PWN::Memory.prior_user_message(
1762
1796
  session_id: session_id,
1763
1797
  max_chars: 4_000,
@@ -580,10 +580,9 @@ module PWN
580
580
  end
581
581
 
582
582
  ev = evaluate(limit: opts[:limit] || 40)
583
- fallback = %w[shell pwn_eval memory_recall mistakes_record mistakes_resolve learning_note_outcome memory_remember]
583
+ fallback = PWN::AI::Agent::Registry::DEFAULT_PREFERENCE
584
584
  pref = begin
585
- intent = Thread.current[:pwn_request_intent]
586
- PWN::AI::Agent::Registry.preference_order(intent: intent)
585
+ PWN::AI::Agent::Registry.preference_order
587
586
  rescue StandardError
588
587
  []
589
588
  end
@@ -63,16 +63,14 @@ module PWN
63
63
 
64
64
  # Heredoc (not a "..." literal): an unescaped "..." inside a
65
65
  # double-quoted string is parsed as Range (begin..."...end).
66
- # Skills sit in the STATIC prefix (Hermes index) so prompt-cache
67
- # breakpoints can pin the persona + SKILLS list; MEMORY/LEARNING
68
- # remain in the dynamic tail. Mid-turn lean: request + CORE_TOOLS +
69
- # known-fix. Expand MEMORY/SKILLS/LEARNING/METRICS/POLICY/EXTRO
70
- # only when the turn is stuck or the operator asked for the full harness.
66
+ # Mid-turn: current session + memory + skills + learning + known-fix,
67
+ # then tools (memory_recall / session_recall / skills_recall / pwn_eval / shell).
68
+ # Expand METRICS/POLICY/EXTRO when stuck or expand_harness.
71
69
  expand = opts[:expand_harness] == true || opts[:stuck] == true
72
70
  harness = if expand
73
- "#{skills_block}#{memory_block(limit: b[:memory], request: request)}#{recent_turns_block(session_id: session_id, request: request, limit: b[:recent_turns])}#{learning_block(limit: b[:learning])}#{mistakes_block(limit: b[:mistakes], request: request)}#{metrics_block(limit: b[:metrics], engine: engine)}#{policy_block if b[:policy].to_i.positive?}#{extrospection_block if b[:extro]}"
71
+ "#{skills_block}#{recent_turns_block(session_id: session_id, request: request, limit: b[:recent_turns])}#{memory_block(limit: b[:memory], request: request)}#{learning_block(limit: b[:learning])}#{mistakes_block(limit: b[:mistakes], request: request)}#{metrics_block(limit: b[:metrics], engine: engine)}#{policy_block if b[:policy].to_i.positive?}#{extrospection_block if b[:extro]}"
74
72
  else
75
- "#{recent_turns_block(session_id: session_id, request: request, limit: [b[:recent_turns].to_i, 1].max)}#{mistakes_block(limit: b[:mistakes], request: request)}"
73
+ "#{recent_turns_block(session_id: session_id, request: request, limit: b[:recent_turns])}#{memory_block(limit: b[:memory], request: request)}#{skills_block}#{learning_block(limit: b[:learning])}#{mistakes_block(limit: b[:mistakes], request: request)}"
76
74
  end
77
75
  <<~PROMPT
78
76
  #{base}
@@ -92,8 +90,11 @@ module PWN
92
90
  "I will run…", "one more thing…") — that is treated as an incomplete
93
91
  reply. Emit a real tool_call instead, or a complete final answer
94
92
  with evidence. A reply with no tool_calls is your FINAL answer to the user.
95
- Prefer `pwn_eval` for anything in the PWN:: namespace and `shell`
96
- for OS commands. Save durable facts with `memory_remember`.
93
+ Prefer this order: use RECENT TURNS (current session already in
94
+ context), then `memory_recall`, then `session_recall`, then
95
+ `skills_recall`, then `pwn_eval` for PWN:: work, then `shell` for OS
96
+ commands. Save durable facts with `memory_remember`.
97
+ TransparentBrowser: open once, reuse browser_obj, close once.
97
98
 
98
99
  AUTONOMY
99
100
  Multi-step goals must be finished in one Loop.run. Keep calling
@@ -140,13 +141,13 @@ module PWN
140
141
  metrics: (b[:metrics] || (local ? 3 : 8)).to_i,
141
142
  mistakes: (b[:mistakes] || (local ? 3 : 6)).to_i,
142
143
  learning: (b[:learning] || (local ? 2 : 5)).to_i,
143
- # Always inject last user/assistant pair(s); local keeps it tiny.
144
- recent_turns: (b[:recent_turns] || (local ? 1 : 2)).to_i,
144
+ # Always inject prior user/assistant pairs from this session.
145
+ recent_turns: (b[:recent_turns] || (local ? 3 : 6)).to_i,
145
146
  policy: (b[:policy] || 1).to_i,
146
147
  extro: b[:extro].nil? ? !local : b[:extro]
147
148
  }
148
149
  rescue StandardError
149
- { memory: 25, metrics: 8, mistakes: 6, learning: 5, recent_turns: 2, policy: 1, extro: true }
150
+ { memory: 25, metrics: 8, mistakes: 6, learning: 5, recent_turns: 6, policy: 1, extro: true }
150
151
  end
151
152
 
152
153
  private_class_method def self.active_engine
@@ -40,17 +40,14 @@ module PWN
40
40
  keyword_init: true
41
41
  )
42
42
 
43
- CORE_TOOLS = %w[shell pwn_eval memory_remember memory_recall
44
- mistakes_record mistakes_resolve learning_note_outcome].freeze
43
+ CORE_TOOLS = %w[memory_recall session_recall skills_recall pwn_eval shell
44
+ mistakes_record mistakes_resolve learning_note_outcome memory_remember].freeze
45
45
 
46
- # Operator-tunable tool order. Prefer these names when keyword-fit
47
- # (or other rank features) tie. Overridable via opts[:order],
46
+ # Schema order = CORE_TOOLS. Current session is injected (RECENT TURNS).
47
+ # Then memory / prior sessions / skills, then pwn_eval before shell.
48
48
  # opts[:preference], or PWN::Env[:ai][:agent][:tool_preference].
49
49
  # Explicit nil/empty order disables preference (no Env/DEFAULT fallback).
50
- # Recall-first fallback; kind-aware preference_order leads act/recon
51
- # with shell / pwn_eval. Names stay inside CORE_TOOLS.
52
- DEFAULT_PREFERENCE = %w[memory_recall pwn_eval shell mistakes_record mistakes_resolve learning_note_outcome memory_remember].freeze
53
- ACT_PREFERENCE = %w[shell pwn_eval memory_recall mistakes_record mistakes_resolve learning_note_outcome memory_remember].freeze
50
+ DEFAULT_PREFERENCE = CORE_TOOLS
54
51
 
55
52
  @entries = {}
56
53
  @discovered = false
@@ -156,7 +153,7 @@ module PWN
156
153
  raw = PWN::Env.dig(:ai, :agent, :tool_preference) if defined?(PWN::Env) && PWN::Env.is_a?(Hash)
157
154
  return Array(raw).map(&:to_s).reject(&:empty?) unless raw.nil? || (raw.respond_to?(:empty?) && raw.empty?)
158
155
 
159
- ACT_PREFERENCE.dup
156
+ DEFAULT_PREFERENCE.dup
160
157
  rescue StandardError
161
158
  DEFAULT_PREFERENCE.dup
162
159
  end
@@ -3,6 +3,38 @@
3
3
  require 'pwn/ai/agent/registry'
4
4
  require 'pwn/sessions'
5
5
 
6
+ PWN::AI::Agent::Registry.register(
7
+ name: 'session_recall',
8
+ toolset: 'sessions',
9
+ schema: {
10
+ name: 'session_recall',
11
+ description: 'Search previous pwn-ai session transcripts (~/.pwn/sessions) ' \
12
+ 'for how a similar request was solved. Current session is ' \
13
+ 'already in RECENT TURNS — this is for older sessions. ' \
14
+ 'Call before pwn_eval / shell when prior method matters.',
15
+ parameters: {
16
+ type: 'object',
17
+ properties: {
18
+ query: { type: 'string', description: 'Keywords to match in prior user/assistant turns.' },
19
+ limit: { type: 'integer', default: 12, description: 'Max hits (newest / highest score first).' },
20
+ include_current: { type: 'boolean', default: false, description: 'Also search the active session.' }
21
+ },
22
+ required: []
23
+ }
24
+ },
25
+ check: -> { defined?(PWN::Sessions) },
26
+ handler: lambda { |args|
27
+ exclude = ''
28
+ exclude = PWN::Memory.current_session_id if defined?(PWN::Memory) && PWN::Memory.respond_to?(:current_session_id)
29
+ PWN::Sessions.recall(
30
+ query: args[:query],
31
+ limit: args[:limit] || 12,
32
+ exclude_session_id: exclude,
33
+ include_current: args[:include_current] == true
34
+ )
35
+ }
36
+ )
37
+
6
38
  # Thin wrappers around PWN::Sessions so the model can DISCOVER and INSPECT
7
39
  # the JSONL transcripts that learning_reflect / learning_distill_skill
8
40
  # consume. Without these the model has to blindly `shell("ls ~/.pwn/sessions")`
@@ -22,6 +22,62 @@ require 'pwn/ai/agent/registry'
22
22
  # and/or a "## References" markdown section — mirroring the
23
23
  # PWN::SAST::*.security_references convention.
24
24
 
25
+ PWN::AI::Agent::Registry.register(
26
+ name: 'skills_recall',
27
+ toolset: 'skills',
28
+ schema: {
29
+ name: 'skills_recall',
30
+ description: 'Search installed pwn-ai skills (name, description, body, ' \
31
+ 'references) for how to do similar work. Call before ' \
32
+ 'pwn_eval / shell when a reusable procedure may exist. ' \
33
+ 'The SKILLS index is already in the prompt — this loads ' \
34
+ 'matching full methods.',
35
+ parameters: {
36
+ type: 'object',
37
+ properties: {
38
+ query: { type: 'string', description: 'Keywords to match against skill name / description / body.' },
39
+ limit: { type: 'integer', default: 6, description: 'Max hits (highest score first).' }
40
+ },
41
+ required: []
42
+ }
43
+ },
44
+ check: -> { true },
45
+ handler: lambda { |args|
46
+ return [] unless defined?(PWN::Skills) && PWN::Skills.is_a?(Hash)
47
+
48
+ query = args[:query].to_s.downcase
49
+ tokens = query.split(/\W+/).reject { |t| t.length < 3 }
50
+ limit = (args[:limit] || 6).to_i
51
+ limit = 6 if limit <= 0
52
+ trunc = 400
53
+ hits = []
54
+ PWN::Skills.each do |name, meta|
55
+ next unless meta.is_a?(Hash)
56
+
57
+ blob = [
58
+ name.to_s,
59
+ meta[:description],
60
+ meta[:content],
61
+ Array(meta[:references]).join(' ')
62
+ ].join("\n").downcase
63
+ score = 0
64
+ score += 5 if !query.empty? && blob.include?(query)
65
+ tokens.each { |tok| score += 1 if blob.include?(tok) }
66
+ score = 1 if query.empty?
67
+ next if score <= 0
68
+
69
+ body = meta[:content].to_s
70
+ hits << {
71
+ name: name.to_s,
72
+ description: meta[:description].to_s,
73
+ score: score,
74
+ snippet: body.length > trunc ? "#{body[0, trunc]}…[truncated]" : body
75
+ }
76
+ end
77
+ hits.sort_by { |h| -h[:score].to_i }.first(limit)
78
+ }
79
+ )
80
+
25
81
  PWN::AI::Agent::Registry.register(
26
82
  name: 'skill_list',
27
83
  toolset: 'skills',
@@ -418,7 +418,6 @@ module PWN
418
418
  rest_client = browser_obj[:browser]::Request
419
419
 
420
420
  spin = PWN::Plugins::TTYSpinner.start if spinner
421
- spin.auto_spin if spinner
422
421
 
423
422
  retry_count = 0
424
423
  begin
data/lib/pwn/ai/gemini.rb CHANGED
@@ -55,7 +55,6 @@ module PWN
55
55
  rest_client = browser_obj[:browser]::Request
56
56
 
57
57
  spin = PWN::Plugins::TTYSpinner.start if spinner
58
- spin.auto_spin if spinner
59
58
 
60
59
  retry_count = 0
61
60
  begin
data/lib/pwn/ai/grok.rb CHANGED
@@ -424,7 +424,6 @@ module PWN
424
424
  rest_client = browser_obj[:browser]::Request
425
425
 
426
426
  spin = PWN::Plugins::TTYSpinner.start if spinner
427
- spin.auto_spin if spinner
428
427
 
429
428
  retry_count = 0
430
429
  begin
data/lib/pwn/ai/ollama.rb CHANGED
@@ -75,7 +75,6 @@ module PWN
75
75
  rest_client = browser_obj[:browser]::Request
76
76
 
77
77
  spin = PWN::Plugins::TTYSpinner.start if spinner
78
- spin.auto_spin if spinner
79
78
 
80
79
  retry_count = 0
81
80
  begin
@@ -460,7 +460,6 @@ module PWN
460
460
  rest_client = browser_obj[:browser]::Request
461
461
 
462
462
  spin = PWN::Plugins::TTYSpinner.start if spinner
463
- spin.auto_spin if spinner
464
463
 
465
464
  retry_count = 0
466
465
  begin
@@ -87,7 +87,6 @@ module PWN
87
87
  rest_client = browser_obj[:browser]::Request
88
88
 
89
89
  spin = PWN::Plugins::TTYSpinner.start if spinner
90
- spin.auto_spin if spinner
91
90
 
92
91
  retry_count = 0
93
92
  begin
data/lib/pwn/config.rb CHANGED
@@ -160,7 +160,7 @@ module PWN
160
160
  # LLM tangible-task decomposition for autonomous goals (default on).
161
161
  task_summary_llm: nil,
162
162
  tool_router: nil, # nil = auto (true when :active is local :ollama/:openwebui) — cuts ~11k→~3k schema tokens
163
- tool_preference: %w[memory_recall pwn_eval shell mistakes_record mistakes_resolve learning_note_outcome memory_remember],
163
+ tool_preference: %w[memory_recall session_recall skills_recall pwn_eval shell mistakes_record mistakes_resolve learning_note_outcome memory_remember],
164
164
  escalation_persona: 'escalator', # Swarm persona for frontier corrective hints when a local model is stuck
165
165
  # sample E3 verify_as_reward: true|false|nil(auto: ~10% local / always frontier when CLAIM_RX hits)
166
166
  verify_as_reward: nil,
@@ -3,6 +3,7 @@
3
3
  require 'curses'
4
4
  require 'pry'
5
5
  require 'reline'
6
+ require 'tty-cursor'
6
7
  require 'tty-prompt'
7
8
  require 'unicode/display_width'
8
9
  require 'yaml'
@@ -240,6 +241,7 @@ module PWN
240
241
  end
241
242
 
242
243
  def readline(prompt)
244
+ PWN::Plugins::REPL.ready_tty!
243
245
  # Ask the terminal to encode Shift+Enter distinctly from Enter for
244
246
  # the duration of this read. Without this, most emulators send 0x0D
245
247
  # for both and SHIFT_ENTER_SEQS can never match. Reset in `ensure`.
@@ -280,6 +282,40 @@ module PWN
280
282
  end
281
283
  end
282
284
 
285
+ # Restore the TTY after a spinner / agent turn so Pry/Reline prints
286
+ # the next PS1 immediately. hide_cursor + a background worker leave
287
+ # the cursor hidden on $stdout (Reline's stream) even after
288
+ # TTY::Spinner#stop writes show-cursor to $stderr. Reline then
289
+ # waits for a key without redrawing the prompt.
290
+ public_class_method def self.ready_tty!(opts = {})
291
+ return nil if opts[:skip]
292
+
293
+ out = opts[:io] || $stdout
294
+ return nil unless out.respond_to?(:write)
295
+
296
+ out.write("\e[0m#{TTY::Cursor.show}") if defined?(TTY::Cursor)
297
+ out.write("\e[0m\e[?25h") unless defined?(TTY::Cursor)
298
+ out.flush if out.respond_to?(:flush)
299
+ reset_reline_editor
300
+ nil
301
+ rescue StandardError
302
+ nil
303
+ end
304
+
305
+ private_class_method def self.reset_reline_editor(opts = {})
306
+ return unless opts.is_a?(Hash)
307
+ return unless defined?(Reline)
308
+ return unless Reline.respond_to?(:core)
309
+
310
+ editor = Reline.core.instance_variable_get(:@line_editor)
311
+ return unless editor
312
+
313
+ editor.instance_variable_set(:@finished, false) if editor.instance_variable_defined?(:@finished)
314
+ nil
315
+ rescue StandardError
316
+ nil
317
+ end
318
+
283
319
  # Compact token-count formatter for the pwn.ai PS1 (e.g. 0, 843, 12K, 250K, 1M).
284
320
  public_class_method def self.compact_context_tokens(opts = {})
285
321
  n = opts[:tokens].to_i
@@ -1182,6 +1218,7 @@ module PWN
1182
1218
  puts "\n\001\e[32m\002#{final}\001\e[0m\002\n\n"
1183
1219
  pp PWN::Sessions.load(session_id: sess_id) if pi.config.pwn_ai_debug && sess_id && PWN.const_defined?(:Sessions)
1184
1220
  request.replace('nil')
1221
+ PWN::Plugins::REPL.ready_tty!
1185
1222
  next
1186
1223
  rescue StandardError => e
1187
1224
  warn "[pwn-ai] native agent loop failed (#{e.class}: #{e.message}\n#{e.backtrace}); " \
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'tty-spinner'
4
+ require 'tty-cursor'
4
5
 
5
6
  module PWN
6
7
  module Plugins
@@ -10,10 +11,7 @@ module PWN
10
11
  # worker but does NOT join it. The worker is typically mid-sleep
11
12
  # on its interval, so it stays alive long enough to write another
12
13
  # frame (cursor col 1 + glyph) over the HTTP response that the
13
- # caller is about to print. Creating the spinner with clear:false
14
- # (the gem default) also reprints the last glyph + newline on
15
- # stop, so the spinner appears to keep running whenever a
16
- # response is provided.
14
+ # caller is about to print.
17
15
  #
18
16
  # #start uses clear:true + hide_cursor and owns the worker
19
17
  # (spin.start + Thread.new { spin.spin until spin.done? }).
@@ -37,7 +35,18 @@ module PWN
37
35
  }
38
36
  args[:output] = opts[:output] unless opts[:output].nil?
39
37
 
40
- TTY::Spinner.new(**args)
38
+ spin = TTY::Spinner.new(**args)
39
+ spin.start
40
+ interval = 1.0 / [spin.interval.to_f, 1.0].max
41
+ worker = Thread.new do
42
+ Thread.current.report_on_exception = false
43
+ until spin.done?
44
+ spin.spin
45
+ sleep(interval)
46
+ end
47
+ end
48
+ spin.define_singleton_method(:pwn_worker_thread) { worker }
49
+ spin
41
50
  end
42
51
 
43
52
  # Supported Method Parameters::
@@ -49,7 +58,47 @@ module PWN
49
58
  spin = opts.is_a?(Hash) ? opts[:spin] : opts
50
59
  return if spin.nil?
51
60
 
52
- spin.stop if spin.respond_to?(:stop)
61
+ worker = spin.respond_to?(:pwn_worker_thread) ? spin.pwn_worker_thread : nil
62
+ begin
63
+ spin.stop if spin.respond_to?(:stop)
64
+ rescue StandardError
65
+ spin.kill if spin.respond_to?(:kill)
66
+ end
67
+ join_thread(thread: worker)
68
+ join_spinner(spin: spin)
69
+ begin
70
+ spin.clear_line if spin.respond_to?(:clear_line)
71
+ show = defined?(TTY::Cursor) ? TTY::Cursor.show : "\e[?25h"
72
+ out = spin.output if spin.respond_to?(:output)
73
+ out.print(show) if out.respond_to?(:print)
74
+ $stdout.write(show) if $stdout.respond_to?(:write)
75
+ $stdout.flush if $stdout.respond_to?(:flush)
76
+ rescue StandardError
77
+ nil
78
+ end
79
+ nil
80
+ end
81
+
82
+ private_class_method def self.join_thread(opts = {})
83
+ thread = opts[:thread]
84
+ return unless thread.is_a?(Thread)
85
+ return unless thread.alive?
86
+
87
+ unless thread.join(JOIN_SECS)
88
+ thread.kill
89
+ thread.join(JOIN_SECS)
90
+ end
91
+ rescue StandardError
92
+ thread.kill if thread.respond_to?(:kill)
93
+ end
94
+
95
+ private_class_method def self.join_spinner(opts = {})
96
+ spin = opts[:spin]
97
+ return unless spin.respond_to?(:join)
98
+
99
+ spin.join(JOIN_SECS)
100
+ rescue TTY::Spinner::NotSpinningError, StandardError
101
+ nil
53
102
  end
54
103
 
55
104
  # Author(s):: 0day Inc. <support@0dayinc.com>
data/lib/pwn/sessions.rb CHANGED
@@ -119,6 +119,86 @@ module PWN
119
119
  File.readlines(path).map { |l| JSON.parse(l, symbolize_names: true) }
120
120
  end
121
121
 
122
+ # Newest session that is not exclude_session_id (the prior pwn-ai run).
123
+ #
124
+ # Supported Method Parameters::
125
+ # id = PWN::Sessions.previous_id(
126
+ # exclude_session_id: 'optional - current session to skip'
127
+ # )
128
+ public_class_method def self.previous_id(opts = {})
129
+ exclude = opts[:exclude_session_id].to_s
130
+ rows = list.sort_by { |s| [s[:mtime].to_s, s[:id].to_s] }.reverse
131
+ hit = rows.find { |s| s[:id].to_s != exclude }
132
+ hit && hit[:id].to_s
133
+ rescue StandardError
134
+ nil
135
+ end
136
+
137
+ # Search prior session transcripts for query terms. Current session is
138
+ # excluded by default — that tail is already in RECENT TURNS.
139
+ #
140
+ # Supported Method Parameters::
141
+ # hits = PWN::Sessions.recall(
142
+ # query: 'optional - substring / tokens to match',
143
+ # exclude_session_id: 'optional - skip this id (current session)',
144
+ # include_current: 'optional - Boolean include excluded id (default false)',
145
+ # limit: 'optional - max hits (default 12)',
146
+ # max_files: 'optional - newest files to scan (default 40)',
147
+ # truncate: 'optional - chars per hit (default 280)'
148
+ # )
149
+ public_class_method def self.recall(opts = {})
150
+ query = opts[:query].to_s.downcase
151
+ tokens = query.split(/\W+/).reject { |t| t.length < 3 }
152
+ limit = (opts[:limit] || 12).to_i
153
+ limit = 12 if limit <= 0
154
+ max_files = (opts[:max_files] || 40).to_i
155
+ max_files = 40 if max_files <= 0
156
+ trunc = (opts[:truncate] || 280).to_i
157
+ trunc = 280 if trunc <= 0
158
+ exclude = opts[:exclude_session_id].to_s
159
+ include_current = opts[:include_current] == true
160
+
161
+ hits = []
162
+ list.sort_by { |s| s[:mtime].to_s }.reverse.first(max_files).each do |sess|
163
+ sid = sess[:id].to_s
164
+ next if !include_current && !exclude.empty? && sid == exclude
165
+
166
+ load(session_id: sid).each do |row|
167
+ next unless row.is_a?(Hash)
168
+ next unless %w[user assistant].include?(row[:role].to_s)
169
+
170
+ body = row[:content].to_s
171
+ next if body.strip.empty?
172
+
173
+ score = recall_score(body: body.downcase, query: query, tokens: tokens)
174
+ next if score <= 0
175
+
176
+ hits << {
177
+ session_id: sid,
178
+ role: row[:role].to_s,
179
+ timestamp: row[:timestamp],
180
+ content: body.length > trunc ? "#{body[0, trunc]}…[truncated]" : body,
181
+ score: score
182
+ }
183
+ end
184
+ end
185
+ hits.sort_by { |h| [-h[:score].to_f, h[:timestamp].to_s] }.first(limit)
186
+ rescue StandardError
187
+ []
188
+ end
189
+
190
+ private_class_method def self.recall_score(opts = {})
191
+ body = opts[:body].to_s
192
+ query = opts[:query].to_s
193
+ tokens = Array(opts[:tokens])
194
+ return 1 if query.empty?
195
+
196
+ score = 0
197
+ score += 5 if !query.empty? && body.include?(query)
198
+ tokens.each { |tok| score += 1 if body.include?(tok) }
199
+ score
200
+ end
201
+
122
202
  # Supported Method Parameters::
123
203
  # history_for_ai = PWN::Sessions.to_response_history(session_id:)
124
204
  # (converts transcript to the response_history format used by PWN::AI::* .chat)
@@ -417,6 +497,8 @@ module PWN
417
497
  PWN::Sessions.append(session_id: sess[:id], role: 'user', content: 'Run NmapIt...')
418
498
  transcript = PWN::Sessions.load(session_id: sess[:id])
419
499
  hist = PWN::Sessions.to_response_history(session_id: sess[:id])
500
+ PWN::Sessions.recall(query: 'hping3', exclude_session_id: sess[:id])
501
+ PWN::Sessions.previous_id(exclude_session_id: sess[:id])
420
502
  PWN::Sessions.list
421
503
  PWN::Sessions.stats
422
504
  PWN::Sessions.delete(session_id: sess[:id], force: true)
data/lib/pwn/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PWN
4
- VERSION = '0.5.682'
4
+ VERSION = '0.5.683'
5
5
  end
@@ -29,14 +29,14 @@ RSpec.describe 'PWN::AI::Agent::PromptBuilder', :aggregate_failures do
29
29
  PWN::Env[:ai] = { active: :anthropic, module_reflection: false, agent: @agent_cfg }
30
30
  end
31
31
 
32
- it 'mid-turn autonomous prompt keeps request + CORE_TOOLS + known-fix, not the full harness' do
32
+ it 'mid-turn prompt injects MEMORY + RECENT TURNS + known-fix, not the parked harness' do
33
33
  prompt = builder.build(session_id: 'sess_abc', request: 'Write hello into /tmp/x and verify it')
34
- ['ENVIRONMENT', 'KNOWN MISTAKES', 'TOOL USE'].each do |hdr|
34
+ ['ENVIRONMENT', 'MEMORY', 'SKILLS', 'LEARNING', 'KNOWN MISTAKES', 'TOOL USE'].each do |hdr|
35
35
  expect(prompt).to include(hdr), "missing section: #{hdr}"
36
36
  end
37
37
  expect(prompt).to include('session_id : sess_abc')
38
38
  expect(prompt).to include(PWN::VERSION)
39
- ['MEMORY', 'SKILLS', 'LEARNING', 'TOOL EFFECTIVENESS', 'EXTROSPECTION', 'POLICY'].each do |hdr|
39
+ ['TOOL EFFECTIVENESS', 'EXTROSPECTION', 'POLICY'].each do |hdr|
40
40
  expect(prompt).not_to include(hdr), "mid-turn prompt still injects #{hdr}"
41
41
  end
42
42
  end
@@ -15,7 +15,7 @@ describe PWN::AI::Agent::Loop do # rubocop:disable Metrics/BlockLength
15
15
  expect(help_response).to respond_to :help
16
16
  end
17
17
 
18
- describe 'RL-adjacent loop contracts' do
18
+ describe 'RL-adjacent loop contracts' do # rubocop:disable Metrics/BlockLength
19
19
  it 'exhaust path still calls task_summary_flush! and auto_introspect' do
20
20
  src = File.read(described_class.method(:run).source_location.first)
21
21
  # budget exhausted terminal path
@@ -316,6 +316,44 @@ describe PWN::AI::Agent::Loop do # rubocop:disable Metrics/BlockLength
316
316
  expect(described_class.needs_host_work?(request: 'Write hello into /tmp/x.txt')).to eq true
317
317
  end
318
318
 
319
+ it 'does not treat https URLs as file paths; browser collect asks complete on URL evidence' do
320
+ req = <<~REQ.gsub(/\s+/, ' ').strip
321
+ Navigate to https://0dayinc.com using TransparentBrowser.open(browser_type: :chrome, devtools: true)
322
+ find all the blog posts ever made, and return a list of URLs.
323
+ Once complete close the browser via TransparentBrowser.close
324
+ REQ
325
+ expect(req.scan(described_class::HOST_PATH_RX)).to eq([])
326
+ expect(described_class.needs_host_work?(request: req)).to eq true
327
+ expect(described_class.world_knowledge?(request: req)).to eq false
328
+ expect(
329
+ described_class.send(
330
+ :request_unsatisfied?,
331
+ request: req,
332
+ messages: [{ role: 'assistant', content: 'opening chrome', tool_calls: [] }],
333
+ last_iter: false
334
+ )
335
+ ).to eq true
336
+ collected = [
337
+ {
338
+ role: 'assistant',
339
+ tool_calls: [{ function: { name: 'pwn_eval', arguments: '{"code":"browser.goto"}' } }]
340
+ },
341
+ {
342
+ role: 'tool',
343
+ name: 'pwn_eval',
344
+ content: '{"success":true,"result":["https://0dayinc.com/blog/one","https://0dayinc.com/blog/two"]}'
345
+ }
346
+ ]
347
+ expect(
348
+ described_class.send(
349
+ :request_unsatisfied?,
350
+ request: req,
351
+ messages: collected,
352
+ last_iter: false
353
+ )
354
+ ).to eq false
355
+ end
356
+
319
357
  it 'last-iter strips tools only on the true last slot; leftover English tasks do not force required' do
320
358
  src = File.read(described_class.method(:run).source_location.first)
321
359
  expect(src).to match(/text_only_iters = 1/)
@@ -507,6 +545,30 @@ describe PWN::AI::Agent::Loop do # rubocop:disable Metrics/BlockLength
507
545
  FileUtils.rm_rf(sess_tmp) if sess_tmp
508
546
  end
509
547
 
548
+ it 'answer_recall looks up the previous session file for last-session asks' do
549
+ sess_tmp = Dir.mktmpdir('pwn-sess-last')
550
+ stub_const('PWN::Sessions::SESSIONS_DIR', sess_tmp)
551
+ old = PWN::Sessions.create(id: '20200101_000000_oldone', title: 'old')
552
+ PWN::Sessions.append(session_id: old[:id], role: 'user', content: 'SHIP MARKER LAST SESSION')
553
+ PWN::Sessions.append(session_id: old[:id], role: 'assistant', content: 'ack old')
554
+ cur = PWN::Sessions.create(id: '20260101_000000_curone', title: 'current')
555
+ PWN::Sessions.append(session_id: cur[:id], role: 'user', content: 'what did I just say?')
556
+ PWN::Sessions.append(session_id: cur[:id], role: 'assistant', content: "You just said:\n\nwhat did I just say?")
557
+ allow(described_class).to receive(:should_auto_introspect?).and_return(false)
558
+ req = 'what did I just say in the last session?'
559
+ expect(described_class.request_intent(request: req)).to eq(:recall)
560
+ out = described_class.send(
561
+ :answer_recall,
562
+ request: req,
563
+ session_id: cur[:id],
564
+ system_role_content: 'test'
565
+ )
566
+ expect(out).to include('SHIP MARKER LAST SESSION')
567
+ expect(out).not_to match(/You just said:\s*\n\s*what did I just say\?/i)
568
+ ensure
569
+ FileUtils.rm_rf(sess_tmp) if sess_tmp
570
+ end
571
+
510
572
  it 'answer_recall returns prior assistant text for how-did-you-respond without tools' do
511
573
  sess_tmp = Dir.mktmpdir('pwn-sess-recall-asst')
512
574
  stub_const('PWN::Sessions::SESSIONS_DIR', sess_tmp)
@@ -64,11 +64,15 @@ describe PWN::AI::Agent::PromptBuilder do
64
64
  end
65
65
  end
66
66
 
67
- describe 'mid-turn lean prompt' do
68
- it 'does not inject MEMORY/SKILLS/LEARNING/POLICY/EXTRO on a live goal' do
67
+ describe 'mid-turn prompt' do
68
+ it 'injects current session + MEMORY + SKILLS + LEARNING + known-fix before tools' do
69
69
  src = File.read(described_class.method(:build).source_location.first)
70
+ expect(src).to match(/recent_turns_block/)
71
+ expect(src).to match(/memory_block/)
72
+ expect(src).to match(/skills_block/)
73
+ expect(src).to match(/learning_block/)
70
74
  expect(src).to match(/KNOWN MISTAKES|mistakes_block/)
71
- expect(src).to match(/stuck|expand_harness|lean/)
75
+ expect(src).to match(/skills_recall/)
72
76
  end
73
77
  end
74
78
  end
@@ -13,17 +13,24 @@ describe PWN::AI::Agent::Registry do
13
13
  expect(help_response).to respond_to :help
14
14
  end
15
15
 
16
- it 'DEFAULT_PREFERENCE names only CORE_TOOLS in a recall-first fallback' do
17
- expect(described_class::DEFAULT_PREFERENCE).to eq(
18
- %w[memory_recall pwn_eval shell mistakes_record mistakes_resolve learning_note_outcome memory_remember]
16
+ it 'CORE_TOOLS is recall-then-act including skills_recall before pwn_eval' do
17
+ expect(described_class.const_defined?(:ACT_PREFERENCE)).to eq false
18
+ expect(described_class::CORE_TOOLS).to eq(
19
+ %w[memory_recall session_recall skills_recall pwn_eval shell mistakes_record mistakes_resolve learning_note_outcome memory_remember]
19
20
  )
20
- expect(described_class::DEFAULT_PREFERENCE - described_class::CORE_TOOLS).to be_empty
21
+ expect(described_class::DEFAULT_PREFERENCE).to eq(described_class::CORE_TOOLS)
21
22
  expect(described_class::DEFAULT_PREFERENCE).not_to include('sessions_view')
23
+ expect(described_class::CORE_TOOLS.index('memory_recall')).to be < described_class::CORE_TOOLS.index('session_recall')
24
+ expect(described_class::CORE_TOOLS.index('session_recall')).to be < described_class::CORE_TOOLS.index('skills_recall')
25
+ expect(described_class::CORE_TOOLS.index('skills_recall')).to be < described_class::CORE_TOOLS.index('pwn_eval')
26
+ expect(described_class::CORE_TOOLS.index('pwn_eval')).to be < described_class::CORE_TOOLS.index('shell')
22
27
  end
23
28
 
24
- it 'preference_order defaults to shell / pwn_eval there is no request type' do
25
- expect(described_class.preference_order.first(2)).to eq(%w[shell pwn_eval])
26
- expect(described_class.preference_order(kind: :question).first(2)).to eq(%w[shell pwn_eval])
29
+ it 'preference_order is DEFAULT_PREFERENCE kind/intent do not change it' do
30
+ expect(described_class.preference_order).to eq(described_class::DEFAULT_PREFERENCE)
31
+ expect(described_class.preference_order.first(5)).to eq(%w[memory_recall session_recall skills_recall pwn_eval shell])
32
+ expect(described_class.preference_order(kind: :question)).to eq(described_class::DEFAULT_PREFERENCE)
33
+ expect(described_class.preference_order(intent: :recall)).to eq(described_class::DEFAULT_PREFERENCE)
27
34
  end
28
35
 
29
36
  it 'preference_order still honors explicit empty list and Env override' do
@@ -34,8 +41,8 @@ describe PWN::AI::Agent::Registry do
34
41
  it 'definitions(core_only: true) ships CORE_TOOLS, not the full ~85 schema set' do
35
42
  described_class.discover
36
43
  names = described_class.definitions(core_only: true).map { |t| t.dig(:function, :name) }
37
- expect(names).to match_array(described_class::CORE_TOOLS)
44
+ expect(names).to eq(described_class::CORE_TOOLS)
38
45
  expect(names).not_to include('sessions_view')
39
- expect(names.length).to be <= described_class::CORE_TOOLS.length
46
+ expect(names.length).to eq(described_class::CORE_TOOLS.length)
40
47
  end
41
48
  end
@@ -28,6 +28,11 @@ describe 'PWN::AI::Agent::Tools sessions' do
28
28
  expect(PWN::AI::Agent::Registry.lookup(name: 'sessions_current')).not_to be_nil
29
29
  end
30
30
 
31
+ it 'registers the session_recall tool' do
32
+ PWN::AI::Agent::Registry.discover(force: true)
33
+ expect(PWN::AI::Agent::Registry.lookup(name: 'session_recall')).not_to be_nil
34
+ end
35
+
31
36
  it 'exposes the sessions toolset in the registry' do
32
37
  PWN::AI::Agent::Registry.discover(force: true)
33
38
  expect(PWN::AI::Agent::Registry.toolsets).to include('sessions')
@@ -10,6 +10,10 @@ describe 'PWN::AI::Agent::Tools skills' do
10
10
  expect(PWN::AI::Agent::Registry.lookup(name: 'skill_list')).not_to be_nil
11
11
  end
12
12
 
13
+ it 'registers the skills_recall tool' do
14
+ expect(PWN::AI::Agent::Registry.lookup(name: 'skills_recall')).not_to be_nil
15
+ end
16
+
13
17
  it 'registers the skill_migrate_legacy tool' do
14
18
  expect(PWN::AI::Agent::Registry.lookup(name: 'skill_migrate_legacy')).not_to be_nil
15
19
  end
@@ -32,6 +36,18 @@ describe 'PWN::AI::Agent::Tools skills' do
32
36
  let(:delete) { PWN::AI::Agent::Registry.lookup(name: 'skill_delete')[:handler] }
33
37
  let(:list) { PWN::AI::Agent::Registry.lookup(name: 'skill_list')[:handler] }
34
38
  let(:view) { PWN::AI::Agent::Registry.lookup(name: 'skill_view')[:handler] }
39
+ let(:recall) { PWN::AI::Agent::Registry.lookup(name: 'skills_recall')[:handler] }
40
+
41
+ it 'skills_recall ranks matching skills by query and skips non-matches' do
42
+ create.call(name: 'browser-dump', description: 'Dump links via TransparentBrowser.', content: "open once, dump_links, close once\n")
43
+ create.call(name: 'unrelated', description: 'Something else entirely.', content: "no overlap here\n")
44
+ hits = recall.call(query: 'TransparentBrowser dump_links', limit: 8)
45
+ expect(hits).to be_an(Array)
46
+ expect(hits).not_to be_empty
47
+ names = hits.map { |h| h[:name].to_s }
48
+ expect(names).to include('browser-dump')
49
+ expect(names).not_to include('unrelated')
50
+ end
35
51
 
36
52
  it 'skill_create writes <name>/SKILL.md with required name+description frontmatter' do
37
53
  out = create.call(name: 'Recon Quick_Scan', description: 'Fast host triage.', content: "# Recon\nnmap -T4 -F {t}\n", references: ['T1046'])
@@ -59,8 +59,13 @@ describe PWN::AI::RedTeam::TestCaseEngine do
59
59
  described_class.define_singleton_method(:judge) { |_opts| 'EPSS 5%' }
60
60
  end
61
61
 
62
+ before do
63
+ described_class.class_variable_set(:@@logger, Logger.new(File::NULL))
64
+ end
65
+
62
66
  after do
63
67
  load File.expand_path('../../../../../lib/pwn/ai/red_team/test_case_engine.rb', __dir__)
68
+ described_class.class_variable_set(:@@logger, Logger.new(File::NULL))
64
69
  end
65
70
 
66
71
  it 'generates payload_count LLM payloads from strategies' do
@@ -85,6 +90,21 @@ describe PWN::AI::RedTeam::TestCaseEngine do
85
90
  )
86
91
  end
87
92
 
93
+ it 'does not print [INFO] completion banners to $stdout' do
94
+ apply_env_stub
95
+ apply_engine_stub({})
96
+ expect do
97
+ described_class.execute(
98
+ strategies: strategies,
99
+ payload_count: 1,
100
+ security_references: refs,
101
+ target_engine: :grok,
102
+ attacker_engine: :grok,
103
+ max_adaptive_rounds: 0
104
+ )
105
+ end.to output('').to_stdout
106
+ end
107
+
88
108
  it 'defaults to ten generated payloads when payload_count is omitted' do
89
109
  tracker = {}
90
110
  apply_env_stub
@@ -24,4 +24,13 @@ describe PWN::Plugins::REPL do
24
24
  expect(described_class.compact_context_tokens(tokens: 26_000)).to eq('26K')
25
25
  expect(described_class.compact_context_tokens(tokens: 500_000)).to eq('500K')
26
26
  end
27
+
28
+ it 'ready_tty! exists and the pwn-ai path resets the TTY before the next PS1' do
29
+ expect(described_class).to respond_to :ready_tty!
30
+ hook = File.read(described_class.method(:add_hooks).source_location.first)
31
+ expect(hook).to match(/ready_tty!/)
32
+ expect(hook).to match(/request\.replace\('nil'\)/)
33
+ reader = File.read(described_class.const_get(:PWNMultiLineInput).instance_method(:readline).source_location.first)
34
+ expect(reader).to match(/ready_tty!/)
35
+ end
27
36
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'spec_helper'
4
+ require 'stringio'
4
5
 
5
6
  describe PWN::Plugins::TTYSpinner do
6
7
  it 'should display information for authors' do
@@ -10,4 +11,33 @@ describe PWN::Plugins::TTYSpinner do
10
11
  it 'should display information for existing help method' do
11
12
  expect(described_class).to respond_to :help
12
13
  end
14
+
15
+ it 'stop joins the auto_spin worker so no frames are written after the response' do
16
+ io = StringIO.new
17
+ spin = described_class.start(output: io, format: :dots)
18
+ spin.auto_spin
19
+ worker = spin.instance_variable_get(:@thread)
20
+ expect(worker).to be_a(Thread)
21
+ sleep 0.12
22
+ described_class.stop(spin: spin)
23
+ expect(worker.alive?).to eq false
24
+ after_stop = io.string.dup
25
+ sleep 0.2
26
+ expect(io.string).to eq(after_stop)
27
+ expect(spin.done?).to eq true
28
+ end
29
+
30
+ it 'stop is a no-op for nil and is idempotent' do
31
+ expect { described_class.stop(spin: nil) }.not_to raise_error
32
+ io = StringIO.new
33
+ spin = described_class.start(output: io)
34
+ described_class.stop(spin: spin)
35
+ expect { described_class.stop(spin: spin) }.not_to raise_error
36
+ end
37
+
38
+ it 'stop shows the cursor on $stdout so the next PS1 is visible' do
39
+ src = File.read(described_class.method(:stop).source_location.first)
40
+ expect(src).to match(/Cursor\.show/)
41
+ expect(src).to match(/\$stdout/)
42
+ end
13
43
  end
@@ -61,4 +61,33 @@ describe PWN::Sessions do
61
61
  ensure
62
62
  FileUtils.rm_rf(tmp) if tmp
63
63
  end
64
+
65
+ it 'recall searches prior session transcripts and skips the current session' do
66
+ tmp = Dir.mktmpdir
67
+ stub_const('PWN::Sessions::SESSIONS_DIR', tmp)
68
+ old = PWN::Sessions.create(id: '20200101_000000_oldone', title: 'old')
69
+ PWN::Sessions.append(session_id: old[:id], role: 'user', content: 'use hping3 for a ping sweep')
70
+ PWN::Sessions.append(session_id: old[:id], role: 'assistant', content: 'Prefer pwn_eval then shell for that')
71
+ cur = PWN::Sessions.create(id: '20260101_000000_curone', title: 'current')
72
+ PWN::Sessions.append(session_id: cur[:id], role: 'user', content: 'unrelated current chatter')
73
+ hits = PWN::Sessions.recall(query: 'hping3', exclude_session_id: cur[:id], limit: 8)
74
+ expect(hits).to be_an(Array)
75
+ expect(hits).not_to be_empty
76
+ expect(hits.all? { |h| h[:session_id] != cur[:id] }).to eq true
77
+ expect(hits.any? { |h| h[:content].to_s.include?('hping3') }).to eq true
78
+ ensure
79
+ FileUtils.rm_rf(tmp) if tmp
80
+ end
81
+
82
+ it 'previous_id returns the newest session that is not the current one' do
83
+ tmp = Dir.mktmpdir
84
+ stub_const('PWN::Sessions::SESSIONS_DIR', tmp)
85
+ old = PWN::Sessions.create(id: '20200101_000000_oldses', title: 'old')
86
+ PWN::Sessions.append(session_id: old[:id], role: 'user', content: 'SHIP MARKER LAST SESSION')
87
+ cur = PWN::Sessions.create(id: '20260101_000000_curses', title: 'current')
88
+ PWN::Sessions.append(session_id: cur[:id], role: 'user', content: 'what did I just say?')
89
+ expect(PWN::Sessions.previous_id(exclude_session_id: cur[:id])).to eq(old[:id])
90
+ ensure
91
+ FileUtils.rm_rf(tmp) if tmp
92
+ end
64
93
  end
data/spec/spec_helper.rb CHANGED
@@ -38,3 +38,9 @@ RSpec.configure do |config|
38
38
  $stderr = original_stderr
39
39
  end
40
40
  end
41
+
42
+ # Logger.new($stdout) captures the IO at class load — the same stream RSpec
43
+ # uses for progress dots. Reassigning $stdout in before(:each) does not
44
+ # retarget that logger. Point the red-team engine at /dev/null so execute
45
+ # banners cannot leak into rake.
46
+ PWN::AI::RedTeam::TestCaseEngine.class_variable_set(:@@logger, Logger.new(File::NULL)) if defined?(PWN::AI::RedTeam::TestCaseEngine)
@@ -2043,7 +2043,9 @@
2043
2043
  {"messages":[{"role":"user","content":"PWN::Plugins::REPL.compact_context_tokens Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL.compact_context_tokens`: "}]}
2044
2044
  {"messages":[{"role":"user","content":"PWN::Plugins::REPL.enable_autocomplete Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL.enable_autocomplete`: Supported Method Parameters\n\nPWN::Plugins::REPL.enable_autocomplete(\n\nenabled: 'optional - Boolean (default true). false reverts to single-line cycling.'\n\n)\n\nIRB-style suggest-as-you-type for the pwn REPL.\n\nReplaces Pry’s default input (rb-readline — single-candidate TAB cycling) with Reline and turns on Reline.autocompletion, which renders a live dropdown of candidates below the cursor as you type (the same widget IRB uses). Pry already wires Reline.completion_proc → @pry.complete (Pry::InputCompleter) when input == Reline, so the menu is fed by the full Ruby/PWN object graph: constants (PWN::Plugins::Nm<TAB>), instance methods, local/global variables, and Pry slash-commands.\n\nNavigate with ↑/↓ or TAB, accept with → or ENTER, dismiss with ESC.\n\nScope: this drives the MAIN pwn REPL (Ruby). pwn-ai / pwn-asm swap to PWNMultiLineInput, which bypasses Pry’s Reline path on purpose (natural-language / opcode input — Ruby completion isn’t useful there); SHIFT+ENTER multi-line continues to work in those modes.\n"}]}
2045
2045
  {"messages":[{"role":"user","content":"PWN::Plugins::REPL.help Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL.help`: "}]}
2046
+ {"messages":[{"role":"user","content":"PWN::Plugins::REPL.ready_tty! Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL.ready_tty!`: "}]}
2046
2047
  {"messages":[{"role":"user","content":"PWN::Plugins::REPL.refresh_ps1_proc Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL.refresh_ps1_proc`: "}]}
2048
+ {"messages":[{"role":"user","content":"PWN::Plugins::REPL.reset_reline_editor Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL.reset_reline_editor`: "}]}
2047
2049
  {"messages":[{"role":"user","content":"PWN::Plugins::REPL.start Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL.start`: Supported Method Parameters\n\nPWN::Plugins::REPL.start\n"}]}
2048
2050
  {"messages":[{"role":"user","content":"PWN::Plugins::REPL.process Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL.process`: "}]}
2049
2051
  {"messages":[{"role":"user","content":"PWN::Plugins::REPL::PWNMultiLineInput.new Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::PWNMultiLineInput.new`: "}]}
@@ -2115,6 +2117,8 @@
2115
2117
  {"messages":[{"role":"user","content":"PWN::Plugins::Spider.help Usage"},{"role":"assistant","content":"`PWN::Plugins::Spider.help`: "}]}
2116
2118
  {"messages":[{"role":"user","content":"PWN::Plugins::TTYSpinner.authors Usage"},{"role":"assistant","content":"`PWN::Plugins::TTYSpinner.authors`: Author(s)\n\n0day Inc. <support@0dayinc.com>\n"}]}
2117
2119
  {"messages":[{"role":"user","content":"PWN::Plugins::TTYSpinner.help Usage"},{"role":"assistant","content":"`PWN::Plugins::TTYSpinner.help`: "}]}
2120
+ {"messages":[{"role":"user","content":"PWN::Plugins::TTYSpinner.join_spinner Usage"},{"role":"assistant","content":"`PWN::Plugins::TTYSpinner.join_spinner`: "}]}
2121
+ {"messages":[{"role":"user","content":"PWN::Plugins::TTYSpinner.join_thread Usage"},{"role":"assistant","content":"`PWN::Plugins::TTYSpinner.join_thread`: "}]}
2118
2122
  {"messages":[{"role":"user","content":"PWN::Plugins::TTYSpinner.start Usage"},{"role":"assistant","content":"`PWN::Plugins::TTYSpinner.start`: Supported Method Parameters\n\nspin = PWN::Plugins::TTYSpinner.start(\n\nformat: 'optional - TTY::Spinner format (defaults to :dots)',\noutput: 'optional - IO to draw on (defaults to $stderr)'\n\n)\n"}]}
2119
2123
  {"messages":[{"role":"user","content":"PWN::Plugins::TTYSpinner.stop Usage"},{"role":"assistant","content":"`PWN::Plugins::TTYSpinner.stop`: Supported Method Parameters\n\nPWN::Plugins::TTYSpinner.stop(\n\nspin: 'optional - TTY::Spinner from #start (no-op if nil)'\n\n)\n"}]}
2120
2124
  {"messages":[{"role":"user","content":"PWN::Plugins::ThreadPool.authors Usage"},{"role":"assistant","content":"`PWN::Plugins::ThreadPool.authors`: Author(s)\n\n0day Inc. <support@0dayinc.com>\n"}]}
@@ -2700,6 +2704,8 @@
2700
2704
  {"messages":[{"role":"user","content":"PWN::Sessions.list Usage"},{"role":"assistant","content":"`PWN::Sessions.list`: Supported Method Parameters\n\nsessions = PWN::Sessions.list\n"}]}
2701
2705
  {"messages":[{"role":"user","content":"PWN::Sessions.load Usage"},{"role":"assistant","content":"`PWN::Sessions.load`: Supported Method Parameters\n\ntranscript = PWN::Sessions.load(session_id: ‘required’)\n"}]}
2702
2706
  {"messages":[{"role":"user","content":"PWN::Sessions.protected_session_ids Usage"},{"role":"assistant","content":"`PWN::Sessions.protected_session_ids`: "}]}
2707
+ {"messages":[{"role":"user","content":"PWN::Sessions.recall Usage"},{"role":"assistant","content":"`PWN::Sessions.recall`: Supported Method Parameters\n\nhits = PWN::Sessions.recall(\n\nquery: 'optional - substring / tokens to match',\nexclude_session_id: 'optional - skip this id (current session)',\ninclude_current: 'optional - Boolean include excluded id (default false)',\nlimit: 'optional - max hits (default 12)',\nmax_files: 'optional - newest files to scan (default 40)',\ntruncate: 'optional - chars per hit (default 280)'\n\n)\n"}]}
2708
+ {"messages":[{"role":"user","content":"PWN::Sessions.recall_score Usage"},{"role":"assistant","content":"`PWN::Sessions.recall_score`: "}]}
2703
2709
  {"messages":[{"role":"user","content":"PWN::Sessions.sessions_dir Usage"},{"role":"assistant","content":"`PWN::Sessions.sessions_dir`: Supported Method Parameters\n\ndir = PWN::Sessions.sessions_dir\n"}]}
2704
2710
  {"messages":[{"role":"user","content":"PWN::Sessions.stats Usage"},{"role":"assistant","content":"`PWN::Sessions.stats`: Supported Method Parameters\n\nstats = PWN::Sessions.stats\n"}]}
2705
2711
  {"messages":[{"role":"user","content":"PWN::Sessions.stub_session? Usage"},{"role":"assistant","content":"`PWN::Sessions.stub_session?`: "}]}
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pwn
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.682
4
+ version: 0.5.683
5
5
  platform: ruby
6
6
  authors:
7
7
  - 0day Inc.