pwn 0.5.762 → 0.5.766
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/Gemfile +9 -9
- data/documentation/Banner.md +18 -0
- data/documentation/Installation.md +104 -0
- data/documentation/Reinforcement-Learning.md +80 -0
- data/documentation/pwn-REPL.md +74 -0
- data/etc/default_cron/session_cleanup.rb +255 -0
- data/etc/default_skills/pwn/ai/agent/learning/SKILL.md +9 -2
- data/etc/default_skills/pwn/plugins/repl/SKILL.md +8 -0
- data/etc/default_skills/pwn/sessions/SKILL.md +10 -0
- data/lib/pwn/ai/agent/curriculum.rb +9 -2
- data/lib/pwn/ai/agent/learning.rb +365 -0
- data/lib/pwn/ai/agent/loop.rb +4 -0
- data/lib/pwn/ai/agent/reward.rb +12 -1
- data/lib/pwn/ai/agent/swarm.rb +10 -3
- data/lib/pwn/ai/agent/tools/learning.rb +20 -3
- data/lib/pwn/ai/agent/tools/sessions.rb +2 -1
- data/lib/pwn/ai/anthropic.rb +1 -1
- data/lib/pwn/ai/cli.rb +5 -2
- data/lib/pwn/ai/grok.rb +1 -1
- data/lib/pwn/ai/open_ai.rb +1 -1
- data/lib/pwn/ai/router.rb +56 -12
- data/lib/pwn/cron.rb +35 -1
- data/lib/pwn/migrate.rb +5 -1
- data/lib/pwn/plugins/repl/ai.rb +176 -31
- data/lib/pwn/plugins/repl/ai_console.rb +173 -16
- data/lib/pwn/plugins/repl/mesh.rb +73 -10
- data/lib/pwn/plugins/repl.rb +9 -1
- data/lib/pwn/sessions.rb +567 -22
- data/lib/pwn/version.rb +1 -1
- data/spec/lib/pwn/ai/agent/learning_spec.rb +229 -0
- data/spec/lib/pwn/ai/agent/swarm_spec.rb +10 -1
- data/spec/lib/pwn/ai/agent/tools/learning_spec.rb +14 -0
- data/spec/lib/pwn/ai/cli_spec.rb +15 -1
- data/spec/lib/pwn/ai/router_spec.rb +53 -0
- data/spec/lib/pwn/banner_spec.rb +1 -1
- data/spec/lib/pwn/cron_spec.rb +4 -3
- data/spec/lib/pwn/migrate_spec.rb +4 -4
- data/spec/lib/pwn/plugins/repl/ai_console_commands_spec.rb +9 -0
- data/spec/lib/pwn/plugins/repl/ai_console_spec.rb +400 -7
- data/spec/lib/pwn/plugins/repl/ai_result_chronology_spec.rb +210 -0
- data/spec/lib/pwn/plugins/repl/ai_spec.rb +209 -0
- data/spec/lib/pwn/plugins/repl_mesh_transport_label_spec.rb +236 -0
- data/spec/lib/pwn/plugins/repl_spec.rb +2 -0
- data/spec/lib/pwn/redaction_spec.rb +3 -2
- data/spec/lib/pwn/session_cleanup_default_spec.rb +72 -0
- data/spec/lib/pwn/session_cleanup_payload_spec.rb +593 -0
- data/spec/lib/pwn/sessions_spec.rb +344 -0
- data/third_party/pwn_rdoc.jsonl +52 -1
- metadata +24 -19
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 48f9dad90543bf3140d52f14058c9b59c0e011daa42b8729ff4614fb01b34d60
|
|
4
|
+
data.tar.gz: e04116b2e14a9f728a1a583762ad9395f4858dcad190211c05988e8e5d60830b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e10732fcb06f209fd72fa0518241143fb9dccd448fdbf75c0d78d0da155ef0206f2bd068906e4be0b8605e1a15d73a7f1844f1a10b9f756f0cc34ff3b788495d
|
|
7
|
+
data.tar.gz: 734bef29b1fb747424b676a0277095175b37816b6b7ca272dd3ff720ec36872ab69a7700afc10f74ba6542c7855282a97666ae6127e1bf8ccc66c78b8a5c3b1c
|
data/Gemfile
CHANGED
|
@@ -50,19 +50,19 @@ gem 'libusb', '0.8.0'
|
|
|
50
50
|
gem 'luhn', '3.0.0'
|
|
51
51
|
gem 'mail', '2.9.1'
|
|
52
52
|
gem 'mcp', '1.7.0'
|
|
53
|
-
gem 'meshtastic', '0.0.
|
|
53
|
+
gem 'meshtastic', '0.0.187'
|
|
54
54
|
gem 'metasm', '1.0.6'
|
|
55
55
|
gem 'mongo', '2.26.0'
|
|
56
56
|
gem 'msfrpc-client', '1.1.2'
|
|
57
57
|
gem 'netaddr', '2.0.6'
|
|
58
|
-
gem 'net-ldap', '0.
|
|
58
|
+
gem 'net-ldap', '0.21.0'
|
|
59
59
|
gem 'net-openvpn', '0.8.7'
|
|
60
60
|
gem 'net-smtp', '0.5.2'
|
|
61
61
|
gem 'nexpose', '7.3.0'
|
|
62
62
|
gem 'nokogiri', '1.19.4'
|
|
63
63
|
gem 'nokogiri-diff', '0.3.0'
|
|
64
64
|
gem 'oily_png', '1.2.1'
|
|
65
|
-
gem 'open3', '0.
|
|
65
|
+
gem 'open3', '0.3.0'
|
|
66
66
|
gem 'os', '1.1.4'
|
|
67
67
|
gem 'ostruct', '0.6.3'
|
|
68
68
|
gem 'packetfu', '2.0.0'
|
|
@@ -73,15 +73,15 @@ gem 'pry', '0.16.0'
|
|
|
73
73
|
gem 'pry-doc', '1.7.0'
|
|
74
74
|
gem 'rake', '13.4.2'
|
|
75
75
|
gem 'rb-readline', '0.5.5'
|
|
76
|
-
gem 'rbvmomi2', '3.
|
|
76
|
+
gem 'rbvmomi2', '3.11.0'
|
|
77
77
|
gem 'rdoc', '7.0.4'
|
|
78
78
|
gem 'rest-client', '2.1.0'
|
|
79
79
|
gem 'rex', '2.0.13'
|
|
80
|
-
gem 'rmagick', '7.
|
|
80
|
+
gem 'rmagick', '7.2.0'
|
|
81
81
|
gem 'rqrcode', '3.2.0'
|
|
82
82
|
gem 'rspec', '3.13.2'
|
|
83
83
|
gem 'rtesseract', '3.1.4'
|
|
84
|
-
gem 'rubocop', '1.
|
|
84
|
+
gem 'rubocop', '1.92.0'
|
|
85
85
|
gem 'rubocop-rake', '0.7.1'
|
|
86
86
|
gem 'rubocop-rspec', '3.10.2'
|
|
87
87
|
gem 'ruby-audio', '1.6.1'
|
|
@@ -89,9 +89,9 @@ gem 'ruby-nmap', '1.0.3'
|
|
|
89
89
|
gem 'ruby-saml', '1.18.1'
|
|
90
90
|
gem 'rvm', '1.11.3.9'
|
|
91
91
|
gem 'savon', '2.17.5'
|
|
92
|
-
gem 'selenium-devtools', '0.
|
|
93
|
-
gem 'selenium-webdriver', '4.
|
|
94
|
-
gem 'slack-ruby-client', '3.
|
|
92
|
+
gem 'selenium-devtools', '0.155.0'
|
|
93
|
+
gem 'selenium-webdriver', '4.51.0'
|
|
94
|
+
gem 'slack-ruby-client', '3.3.0'
|
|
95
95
|
gem 'socksify', '1.8.1'
|
|
96
96
|
gem 'spreadsheet', '1.3.5'
|
|
97
97
|
gem 'sqlite3', '2.9.6'
|
data/documentation/Banner.md
CHANGED
|
@@ -14,4 +14,22 @@ welcome-banner # REPL command: redraw a random one
|
|
|
14
14
|
|
|
15
15
|
Add your own: drop a module in `lib/pwn/banner/` implementing `self.get`.
|
|
16
16
|
|
|
17
|
+
## pwn-ai startup pane
|
|
18
|
+
|
|
19
|
+
The curses console starts with a white miniature derived directly from
|
|
20
|
+
`PWN::Banner::WhiteRabbit.get`, without its dedication or wordmark. The full
|
|
21
|
+
banner remains unchanged. The rabbit is centered and aspect-contained in the
|
|
22
|
+
current pane: small panes use area-filtered 3×6 character strokes packed into
|
|
23
|
+
ordinary 2×4 Braille text dots; when the original ASCII fits, it is displayed
|
|
24
|
+
verbatim, centered without enlargement. This assumes typical 1:2 terminal cells.
|
|
25
|
+
Small panes lose punctuation detail, and Braille dot spacing depends on the font;
|
|
26
|
+
physically tiny panes cannot retain a recognizable rabbit. No image protocol,
|
|
27
|
+
image file, graphics probe or runtime image library is used.
|
|
28
|
+
|
|
29
|
+
No games or replays are computed until the animation pane first receives focus
|
|
30
|
+
via Ctrl+T/Ctrl+X. That enables games for the console lifetime. Ctrl+G cycles
|
|
31
|
+
games while focused; leaving the pane keeps the existing automatic animation,
|
|
32
|
+
not the startup rabbit. Resizing, modal input priority and the mission draft
|
|
33
|
+
retain their existing behavior. Monochrome terminals keep the same geometry.
|
|
34
|
+
|
|
17
35
|
[← Home](Home.md)
|
|
@@ -202,6 +202,110 @@ without a per-job crontab line. Live Policy files (`policy.json`,
|
|
|
202
202
|
`policy_traj.jsonl`) are created on first agent turn when `ai.agent.policy`
|
|
203
203
|
is on.
|
|
204
204
|
|
|
205
|
+
Schema `v8` seeds the enabled `session_cleanup` hygiene job, daily at 03:45
|
|
206
|
+
in the scheduler's local timezone. First launch also seeds it through
|
|
207
|
+
`PWN::Cron.install_defaults`. The bundled `etc/default_cron/session_cleanup.rb`
|
|
208
|
+
is installed to `~/.pwn/cron/session_cleanup.rb`; managed copies refresh on
|
|
209
|
+
subsequent launches/migrations, while unmarked custom helpers are preserved.
|
|
210
|
+
An existing job named `session_cleanup` keeps its ID, schedule, enabled state,
|
|
211
|
+
payload and native scheduler drop-ins. New jobs defer cleanup until their first
|
|
212
|
+
scheduled time, rather than deleting during initial catch-up.
|
|
213
|
+
|
|
214
|
+
Cleanup calls a session named only when its persisted metadata has
|
|
215
|
+
`title_source: summary`, a nonblank title and summary, and a positive integer
|
|
216
|
+
`summary_turns` cursor: the fields published after validated LLM title generation.
|
|
217
|
+
Provisional first-request titles, manual titles, persona labels, aliases and
|
|
218
|
+
historical embedded titles are not evidence of LLM summarization. Legacy records
|
|
219
|
+
without provenance are unsummarized with `missing_summary_provenance`; invalid or
|
|
220
|
+
incomplete metadata is unknown, never named. `/sessions list` and cleanup share
|
|
221
|
+
`title_source`, `is_summarized`, `classification` and `title_reason`. Classification
|
|
222
|
+
does not establish deletion safety: malformed records, symlinks and uncertain pin
|
|
223
|
+
stores remain protected. Age alone does not preserve an unsummarized session.
|
|
224
|
+
Version 5 backs up unreferenced unsummarized sessions before a guarded `Sessions.delete`
|
|
225
|
+
call that bypasses the ordinary age pin. Current sessions and every known
|
|
226
|
+
learning/mistake/memory reference remain protected. `/sessions list` exposes
|
|
227
|
+
`dependencies` as an array of files **referencing this session** (empty when
|
|
228
|
+
none), not files this session depends on. `dependency_errors` lists unknown,
|
|
229
|
+
malformed or unsafe stores; cleanup fails closed when it is nonempty.
|
|
230
|
+
The shared index follows defined structural fields, not ID-looking text.
|
|
231
|
+
Dependency-backed sessions remain visible and resumable, not archived away.
|
|
232
|
+
Vault/config/credential payloads and historical backups are not scanned.
|
|
233
|
+
Same-user interactive PWN
|
|
234
|
+
processes defer deletion because existing writers have no lifetime leases; this is a
|
|
235
|
+
conservative guard, not a cross-process session lease. Inspect without deleting:
|
|
236
|
+
`ruby ~/.pwn/cron/session_cleanup.rb --dry-run`.
|
|
237
|
+
|
|
238
|
+
`--apply` backs up eligible transcript/metadata bytes under
|
|
239
|
+
`~/.pwn/session_cleanup_archive/<run>/` (directories 0700, files 0600), outside
|
|
240
|
+
`/sessions list`. Atomic, fsynced backups are SHA-256 verified before deletion;
|
|
241
|
+
file identities/hashes, title classification, reference stores and process state
|
|
242
|
+
are rechecked under per-ID/transcript locks. `ledger.json` records exact IDs,
|
|
243
|
+
hashes, skips and completed deletions; dry-run JSON contains metadata only and
|
|
244
|
+
creates no archive. Backups are for recovery, not an automatic reference
|
|
245
|
+
resolver: referenced placeholders remain in the active list until a separately
|
|
246
|
+
verified archive-aware lookup policy exists. No memory, learning or mistake
|
|
247
|
+
records are rewritten. The process checks and advisory locks do not claim a
|
|
248
|
+
transactional guarantee against arbitrary non-cooperating external writers.
|
|
249
|
+
|
|
250
|
+
The cleanup-specific pins additionally preserve every session ID referenced in
|
|
251
|
+
retained learning, mistake and durable memory stores, including non-gold learning
|
|
252
|
+
and old resolved mistakes. References are schema-aware:
|
|
253
|
+
|
|
254
|
+
| Store / writer | Session reference |
|
|
255
|
+
| --- | --- |
|
|
256
|
+
| `learning.jsonl` / Learning; `policy_traj.jsonl` / Policy; `open_goal.json` / OpenGoal | Top-level `session_id` |
|
|
257
|
+
| `mistakes.json` / Mistakes | Each signature record's `sessions[]` and `verified_sessions[]`; signature keys are not session IDs |
|
|
258
|
+
| `memory.json`, `memory.archive.json` / Learning.reflect, Memory | Exact `reflect_<session ID>_<8-hex lesson digest>` keys; arbitrary memory keys, values and source strings are not links |
|
|
259
|
+
| `jobs/<12-hex job ID>.json` / Jobs | Top-level `session_id`; command, environment and job ID are not links |
|
|
260
|
+
| `swarm/*/personas.json` / Swarm | Persona-to-session map values, not persona names or swarm IDs |
|
|
261
|
+
| `curriculum/log.jsonl` / Curriculum | `practice` event's `data[].runs[].session_id`; other events' prose is not traversed |
|
|
262
|
+
| `extrospection.json` / Extrospection | `auto_extrospect` observations' second tag in `['drift', session ID]`; manual/other tags and observation data are not links |
|
|
263
|
+
| `engagements/*/facts.jsonl` / Swarm | Top-level `source_session` |
|
|
264
|
+
| `findings.jsonl` / Findings | Top-level `session_id`; exact transcript/sidecar paths in `evidence_paths[]` and `poc_artifacts[]` |
|
|
265
|
+
| Engagement evidence and recon ledgers / Findings, Recon | Exact transcript/sidecar paths in `path` or `evidence_path`; includes the legacy root `engagements/evidence.jsonl` |
|
|
266
|
+
| `artifacts/manifest.jsonl` / ArtifactRegistry | Top-level `session` and exact transcript/sidecar `source_path`; `kind: swarm-bus` uses `session` for a swarm ID, not session provenance |
|
|
267
|
+
| `artifacts/<session ID>/index.jsonl`, `engagement-memory.json` | Session ownership from that exact directory; index `src` may also reference a transcript/sidecar |
|
|
268
|
+
| `embeddings/<session ID>.db`, `sessions/<session ID>/trace.jsonl` | Ownership from that exact filename/directory, not database contents or trace tool payloads |
|
|
269
|
+
|
|
270
|
+
IDs must be complete canonical values, not substrings or whitespace-normalized
|
|
271
|
+
tokens. Only known evidence/source path fields pointing directly into the
|
|
272
|
+
configured sessions directory are path references. There is currently no
|
|
273
|
+
cross-session link field in transcript or title metadata schemas: listing,
|
|
274
|
+
recalling, quoting JSON, or mentioning a session never pins it, including when
|
|
275
|
+
`sessions_list` output is saved repeatedly. Self listings do not create pins.
|
|
276
|
+
|
|
277
|
+
The index also validates the JSON containers in policy, preference, sentinel,
|
|
278
|
+
skill-review and KPI stores, run checkpoints, mission ledgers, swarm bus/solve
|
|
279
|
+
records, curriculum model/cooldown maps, engagement descriptors/hosts/scans and
|
|
280
|
+
binary facts. These writers do **not** persist session ownership links; their
|
|
281
|
+
arbitrary payloads, prompts, run/job IDs and user-selected labels must not be
|
|
282
|
+
interpreted as such. Unknown JSON stores within the indexed directories are
|
|
283
|
+
reported as uncertainty rather than silently ignored. Adding a new reference
|
|
284
|
+
schema requires auditing its writer/callers and adding positive and negative
|
|
285
|
+
fixtures, not a recursive key-name or text search. Vaults, loot payloads, arbitrary
|
|
286
|
+
artifact content and backups remain outside this index.
|
|
287
|
+
|
|
288
|
+
Native scalar tool `step_reward` fields are recognized;
|
|
289
|
+
unknown transcript fields remain protected.
|
|
290
|
+
|
|
291
|
+
`/sessions list` reads all transcript files afresh, not just cleanup candidates.
|
|
292
|
+
In cleanup output, `unnamed` includes protected sessions; `candidate_ids` excludes
|
|
293
|
+
them. Active sessions hold shared OS locks on permanent per-ID cleanup lock
|
|
294
|
+
files; cleanup takes an exclusive nonblocking lock on the same inode before
|
|
295
|
+
rechecking, backing up and deleting. An active A does not retain inactive B.
|
|
296
|
+
CLI/REPL activation and resume transfer ownership, exit releases it, nested
|
|
297
|
+
Loop requests and title writers hold independent leases, and crashes release
|
|
298
|
+
descriptors automatically. Stale lock contents or reused PIDs confer no ownership.
|
|
299
|
+
Never unlink lock files: that would split the synchronization inode.
|
|
300
|
+
|
|
301
|
+
Old installed processes cannot be upgraded by refreshing the cron helper alone.
|
|
302
|
+
`legacy_owners` reports launchers without the held PID/start-identity capability
|
|
303
|
+
marker; `unknown_legacy_ownership` means their in-memory session IDs are unknown,
|
|
304
|
+
not that every session is active. Ask the operator to identify/quiesce those
|
|
305
|
+
writers and restart using the updated source or deployed version before deleting
|
|
306
|
+
ambiguous candidates. Do not infer active IDs from mtimes, titles or process-name
|
|
307
|
+
matches, and do not bypass reference/summary guards. No config migration is needed.
|
|
308
|
+
|
|
205
309
|
From a checkout:
|
|
206
310
|
|
|
207
311
|
```bash
|
|
@@ -1,5 +1,85 @@
|
|
|
1
1
|
# Reinforcement Learning in pwn-ai
|
|
2
2
|
|
|
3
|
+
## Retained learning evidence and session cleanup
|
|
4
|
+
|
|
5
|
+
Learning outcomes can retain a versioned `evidence_ref` (`version`, `id`,
|
|
6
|
+
`sha256`) pointing to `~/.pwn/learning/evidence/<sha256>.json`. The existing
|
|
7
|
+
`session_id` remains provenance. It ceases to pin a source transcript only
|
|
8
|
+
after the artifact is readable, hash-valid, schema-valid, and its complete
|
|
9
|
+
redacted transcript matches the unchanged source. A marker alone is not enough.
|
|
10
|
+
Missing, modified, malformed or conflicting evidence does not authorize cleanup.
|
|
11
|
+
Other stores, including mistakes, reflection memory and disputed learning,
|
|
12
|
+
continue to pin their source sessions. Evidence files are never session-cleanup
|
|
13
|
+
candidates; the dependency index exposes their referenced-by edges separately.
|
|
14
|
+
|
|
15
|
+
No reliable turn-to-outcome boundary exists in legacy learning rows. Migration
|
|
16
|
+
therefore preserves **every persisted transcript row**, including requests,
|
|
17
|
+
tool actions, observations, results, timestamps and extra fields, with source
|
|
18
|
+
session provenance and a SHA-256 of the original bytes. It uses the existing
|
|
19
|
+
credential-pattern redactor, not a model or Vault, and does not summarize.
|
|
20
|
+
Already compacted source content cannot be reconstructed; external artifact
|
|
21
|
+
references stay references, and those artifacts must still be retained.
|
|
22
|
+
|
|
23
|
+
Use the existing `learning_gc_stores` tool with
|
|
24
|
+
`{"operation":"evidence_plan","dry_run":true}` for a read-only count/size
|
|
25
|
+
report. This operation does not invoke store GC. The equivalent Ruby API in
|
|
26
|
+
the existing `pwn` console is:
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
PWN::AI::Agent::Learning.migrate_evidence(dry_run: true)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
After reviewing the plan and explicitly approving writes, stop old installed
|
|
33
|
+
PWN processes, scheduled maintenance and external editors first. Deploy the
|
|
34
|
+
matching source before restarting: legacy installed writers do not participate
|
|
35
|
+
in the new lock. Then use `learning_gc_stores` with
|
|
36
|
+
`{"operation":"evidence_apply","dry_run":false}`, or
|
|
37
|
+
`PWN::AI::Agent::Learning.migrate_evidence(dry_run: false)`.
|
|
38
|
+
**Neither operation deletes sessions.** Review a separate session-cleanup
|
|
39
|
+
dry run afterward; named/active/uncertain sessions remain protected by that
|
|
40
|
+
cleanup policy. Do not infer session-count reduction from an evidence migration.
|
|
41
|
+
|
|
42
|
+
Artifacts deduplicate by content hash. Files use 0600 and new directories 0700.
|
|
43
|
+
Atomic, fsynced writes/readback precede the learning-index replacement. Original
|
|
44
|
+
learning bytes and prepared/committed ledgers live under
|
|
45
|
+
`~/.pwn/learning/migrations/`, outside legacy backup pruning. Core learning
|
|
46
|
+
writers share a reentrant advisory lock; optimistic byte checks additionally
|
|
47
|
+
reject observed source/index changes. These locks cannot coordinate arbitrary
|
|
48
|
+
external editors or old installed versions: apply requires a quiescent store.
|
|
49
|
+
Interrupted precommit runs leave original references intact and can be retried;
|
|
50
|
+
an interruption after index replacement can leave a prepared ledger with a
|
|
51
|
+
fully usable verified index. No partial artifact or staging file releases a pin.
|
|
52
|
+
|
|
53
|
+
Before any source deletion, an unchanged migration can be rolled back with
|
|
54
|
+
`PWN::AI::Agent::Learning.rollback_evidence(migration: '<ledger SHA-256 ID>')`,
|
|
55
|
+
or tool operation `evidence_rollback` with the same `migration` field. Rollback
|
|
56
|
+
verifies backup/index hashes and refuses later edits or absent/changed sources;
|
|
57
|
+
it never overwrites concurrent learning. Keep recovery files private. They
|
|
58
|
+
retain the exact original learning bytes, which can include legacy sensitive
|
|
59
|
+
data. This is not an evidence garbage collector or a credential-store migration.
|
|
60
|
+
|
|
61
|
+
Consumer audit: `Sessions.load` supplies verified evidence only when the legacy
|
|
62
|
+
source is absent, keeping all existing ID-only consumers compatible. This covers
|
|
63
|
+
`Learning` export_finetune, reflect, fold_judge_into_metrics,
|
|
64
|
+
recover_predicted_from_session, infer_success, compress_exemplar,
|
|
65
|
+
build_skill_from_session and requeue_conflicted; `Reward` run_verification,
|
|
66
|
+
record_verification, request_verification and load_trace; `Curriculum`
|
|
67
|
+
offline_judge and self_play; `Mistakes` previous_user/previous_assistant;
|
|
68
|
+
`Memory.session_turns`; `tools/sessions.rb`; and REPL session-goal lookup.
|
|
69
|
+
`Sessions.export` now uses that resolver too. `Curriculum.offline_judge` also
|
|
70
|
+
discovers retained evidence outcomes after directory cleanup (outcome timestamp
|
|
71
|
+
is its archived-session age). Invalid evidence raises at the resolver rather
|
|
72
|
+
than masquerading as an empty transcript; existing consumer error/drop reporting
|
|
73
|
+
continues to apply, with a metadata-only diagnostic when evidence cannot resolve.
|
|
74
|
+
Reward's PRM annotation writer uses `Learning.annotate_evidence` when the source
|
|
75
|
+
is absent: integer step rewards create a new content-hashed snapshot and replace
|
|
76
|
+
references under the same backup/ledger/lock protocol. Supporting content and the
|
|
77
|
+
old artifact remain unchanged; no session is recreated. Annotation rollback can
|
|
78
|
+
restore verified prior evidence references even after original-session cleanup.
|
|
79
|
+
DPO export already consumes self-contained preference
|
|
80
|
+
pairs, not session files. New module/autoload/gemspec entries are unnecessary:
|
|
81
|
+
the implementation extends existing paired modules.
|
|
82
|
+
|
|
3
83
|
pwn-ai learns while you work. Most of that learning stays **in context**: the
|
|
4
84
|
agent writes what happened to disk and puts the useful bits back into the next
|
|
5
85
|
prompt. On a host **with a trainer and a GPU**, the same data can also train a
|
data/documentation/pwn-REPL.md
CHANGED
|
@@ -16,6 +16,46 @@ commands.
|
|
|
16
16
|
|
|
17
17
|
## Custom commands
|
|
18
18
|
|
|
19
|
+
### pwn-ai session titles
|
|
20
|
+
|
|
21
|
+
`/sessions list` shows stable IDs and titles of at most seven
|
|
22
|
+
whitespace-separated words, matching the SESSION pane. The first request seeds
|
|
23
|
+
a provisional title; the currently active LLM provider and selected model produce a concise conversation title
|
|
24
|
+
after completed turns, with a deterministic seven-word fallback cap. The limit
|
|
25
|
+
also applies to explicit titles. It does not shorten requests or apply to the
|
|
26
|
+
rolling conversation summary.
|
|
27
|
+
|
|
28
|
+
Title generation runs asynchronously after completed turns, never during drawing
|
|
29
|
+
or keystrokes. It uses the previous overall summary plus new conversation turns,
|
|
30
|
+
without changing the active provider or falling back to a different model. Missing
|
|
31
|
+
credentials or failed/invalid responses retain the prior or provisional title;
|
|
32
|
+
background title work never starts interactive authentication. Artifact summaries
|
|
33
|
+
retain their separate local-only routing policy.
|
|
34
|
+
|
|
35
|
+
Existing overlong titles are normalized when read, without rewriting transcripts
|
|
36
|
+
or metadata merely to list them. `/sessions resume <title-or-id>`
|
|
37
|
+
accepts the exact displayed title, optionally quoted. Internal quotes
|
|
38
|
+
and Unicode whitespace are supported. If shortened titles collide, use the
|
|
39
|
+
stable ID shown in the ambiguity message; IDs and transcript filenames do not
|
|
40
|
+
change.
|
|
41
|
+
Previously displayed titles remain resume aliases when a background summary
|
|
42
|
+
renames the session, so a name copied from the list does not expire while typing.
|
|
43
|
+
Aliases shared by multiple sessions are ambiguous too; use a stable ID.
|
|
44
|
+
|
|
45
|
+
`dependencies` means **referenced by**: files containing defined structural
|
|
46
|
+
references to that session. Transcript prose, prompts, tool results and quoted
|
|
47
|
+
JSON—including previous `/sessions list` output—are not dependencies. An empty
|
|
48
|
+
array means no known structural references; `dependency_errors` separately
|
|
49
|
+
reports uncertainty and blocks scheduled cleanup. Dependency-backed sessions
|
|
50
|
+
stay visible and resumable. See Installation's session-cleanup schema table.
|
|
51
|
+
`title_source`, `is_summarized`, `classification` and `title_reason` expose the
|
|
52
|
+
same strict provenance used by cleanup. Only persisted `summary` provenance with
|
|
53
|
+
a nonblank summary/title and positive integer summary cursor counts as named.
|
|
54
|
+
Provisional, manual, persona, historical and alias titles do not prove LLM work;
|
|
55
|
+
legacy records explicitly report missing provenance. Unknown/malformed metadata
|
|
56
|
+
is protected without being called summarized. Neither listing nor cleanup calls
|
|
57
|
+
the LLM to manufacture provenance.
|
|
58
|
+
|
|
19
59
|
| Command | Implemented in | Purpose |
|
|
20
60
|
|---|---|---|
|
|
21
61
|
| `pwn-ai` | `Agent::Loop` | Enter the AI agent TUI |
|
|
@@ -60,6 +100,40 @@ History includes messages and slash commands, skips blank lines and consecutive
|
|
|
60
100
|
duplicates, and keeps the latest 100 entries in memory for the current pwn-mesh
|
|
61
101
|
session only; it is not written to disk.
|
|
62
102
|
|
|
103
|
+
### Message transport labels
|
|
104
|
+
|
|
105
|
+
Each CONVERSATION header ends with its direction and the transport used for
|
|
106
|
+
that individual message: `· RX via MQTT`, `· TX via Serial`, `· RX via Bluetooth`,
|
|
107
|
+
or `· TX via TCP`. In `auto` mode this is the resolved connection, not the
|
|
108
|
+
configured `auto` value. Received messages retain the subscribing transport's
|
|
109
|
+
origin through queued rendering, even if the current transport changes.
|
|
110
|
+
Missing origin metadata is shown as `Unknown`, not guessed from the current pin.
|
|
111
|
+
|
|
112
|
+
Headers use `timestamp encryption · SHORT NAME · source path · destination path · direction via transport`,
|
|
113
|
+
with exactly one space on either side of each `·`. RX uses the sender's
|
|
114
|
+
`User.short_name` (`shortName` in JSON); TX uses the sending local node's User,
|
|
115
|
+
never the destination's name. Device connections use their NodeInfo/NODEINFO
|
|
116
|
+
inventory. MQTT learns NODEINFO announcements in a bounded, connection-local
|
|
117
|
+
inventory (up to 1000 users), including the sending identity if advertised.
|
|
118
|
+
Names are captured with each message, so later inventory or binding changes do
|
|
119
|
+
not rename queued messages. Missing or blank short names omit that field and
|
|
120
|
+
its separator; long names, node-ID fragments and gateway names are not substitutes.
|
|
121
|
+
ANSI CSI sequences are removed, control/newline characters and literal `·` in
|
|
122
|
+
names become spaces, and surrounding Unicode whitespace is trimmed. Other
|
|
123
|
+
Unicode text and internal spaces are retained. Names are labels, not authenticated identities.
|
|
124
|
+
|
|
125
|
+
For example (placeholder node IDs):
|
|
126
|
+
|
|
127
|
+
```text
|
|
128
|
+
2026-01-02 03:04:05+0000 🔍 · PEER · US/CA/2/e/LongFast/!aabbccdd · US/CA/2/e/LongFast/# · RX via MQTT
|
|
129
|
+
2026-01-02 03:04:06+0000 🔍 · SELF · US/CA/2/e/LongFast/!bbccddee (ME) · US/CA/2/e/LongFast/# · TX via Serial
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The timestamp, encryption indicator, source/destination paths, channel and
|
|
133
|
+
`(ME)` marker keep their existing meaning. The suffix identifies the local
|
|
134
|
+
client connection, not every RF or broker hop. A TX header means local transport
|
|
135
|
+
submission; it does not confirm over-the-air delivery or peer receipt.
|
|
136
|
+
|
|
63
137
|
### Encrypted DMs and key discovery
|
|
64
138
|
|
|
65
139
|
On serial, Bluetooth, or TCP, a DM checks the destination's public key in the
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# PWN managed default: session_cleanup v6
|
|
4
|
+
# No provider calls or broad store GC. Unsummarized candidates are backed up first.
|
|
5
|
+
require 'pwn'
|
|
6
|
+
require 'json'
|
|
7
|
+
require 'fileutils'
|
|
8
|
+
require 'time'
|
|
9
|
+
require 'digest'
|
|
10
|
+
require 'securerandom'
|
|
11
|
+
|
|
12
|
+
module PWN
|
|
13
|
+
# Provenance-based session cleanup, shared by seeded cron payloads.
|
|
14
|
+
module SessionCleanup
|
|
15
|
+
VERSION = 6
|
|
16
|
+
|
|
17
|
+
def self.classify(path, id)
|
|
18
|
+
return :unknown unless path == File.join(PWN::Sessions.sessions_dir, "#{id}.jsonl")
|
|
19
|
+
|
|
20
|
+
PWN::Sessions.title_status(session_id: id)[:classification]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Compatibility discovery only: upgraded launchers never block unrelated
|
|
24
|
+
# sessions. Legacy in-memory IDs cannot safely be inferred from recent mtimes.
|
|
25
|
+
def self.legacy_owners(proc_root)
|
|
26
|
+
return [{ error: 'process_inventory_unavailable' }] unless File.directory?(proc_root)
|
|
27
|
+
|
|
28
|
+
Dir.glob(File.join(proc_root, '[0-9]*')).filter_map do |dir|
|
|
29
|
+
next if File.basename(dir).to_i == Process.pid
|
|
30
|
+
next unless File.stat(dir).uid == Process.uid
|
|
31
|
+
|
|
32
|
+
argv = File.binread(File.join(dir, 'cmdline')).split("\0")
|
|
33
|
+
next unless pwn_launcher?(argv, dir)
|
|
34
|
+
next if tracked_owner?(dir)
|
|
35
|
+
|
|
36
|
+
{ pid: File.basename(dir).to_i, reason: 'legacy_session_ownership_unknown' }
|
|
37
|
+
rescue Errno::ENOENT, Errno::ESRCH
|
|
38
|
+
nil
|
|
39
|
+
rescue IOError, SystemCallError
|
|
40
|
+
{ pid: File.basename(dir).to_i, reason: 'ownership_unreadable' }
|
|
41
|
+
end
|
|
42
|
+
rescue IOError, SystemCallError
|
|
43
|
+
[{ error: 'process_inventory_unreadable' }]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.pwn_launcher?(argv, dir)
|
|
47
|
+
executable = %r{\A(?:[^\s]*/)?pwn(?:-[a-zA-Z0-9_-]+)?\z}
|
|
48
|
+
return true if argv.first.to_s.match?(executable)
|
|
49
|
+
return false unless argv.first.to_s.match?(%r{\A(?:[^\s]*/)?ruby[\d.]*(?:\s|\z)})
|
|
50
|
+
return false unless File.basename(File.readlink(File.join(dir, 'exe'))).match?(/\Aruby[\d.]*\z/)
|
|
51
|
+
return true if argv.first.to_s.match?(%r{\A\S*ruby[\d.]*\s+(?:[^\s]*/)?pwn(?:-[a-zA-Z0-9_-]+)?\z})
|
|
52
|
+
|
|
53
|
+
args = argv.drop(1)
|
|
54
|
+
while args.first&.start_with?('-')
|
|
55
|
+
option = args.shift
|
|
56
|
+
break if option == '--'
|
|
57
|
+
break args.clear if option.start_with?('-e')
|
|
58
|
+
|
|
59
|
+
args.shift if %w[-I -r -C -E -F].include?(option)
|
|
60
|
+
end
|
|
61
|
+
args.first.to_s.match?(executable)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def self.tracked_owner?(dir)
|
|
65
|
+
return false unless File.file?(File.join(dir, 'stat'))
|
|
66
|
+
|
|
67
|
+
start = File.read(File.join(dir, 'stat')).split(') ').last.split[19]
|
|
68
|
+
# fdinfo proves this process holds the locked open-file description, not
|
|
69
|
+
# merely an unlocked descriptor to another process's marker. Forked tool
|
|
70
|
+
# workers inherit that capability and the precise session leases; they
|
|
71
|
+
# must not be mistaken for untracked legacy launchers. A reused PID alone
|
|
72
|
+
# has neither descriptor. Marker filenames bind the original PID/start.
|
|
73
|
+
tracked = Dir.glob(File.join(dir, 'fd', '*')).any? do |fd|
|
|
74
|
+
marker = File.readlink(fd)
|
|
75
|
+
next false unless File.basename(marker).match?(/\A\.activity-owner-\d+-\d+\.lock\z/)
|
|
76
|
+
|
|
77
|
+
info = File.read(File.join(dir, 'fdinfo', File.basename(fd)))
|
|
78
|
+
info.match?(/^lock:\s+\d+:\s+FLOCK\s+ADVISORY\s+WRITE\s/)
|
|
79
|
+
rescue Errno::ENOENT
|
|
80
|
+
false
|
|
81
|
+
end
|
|
82
|
+
tracked && File.read(File.join(dir, 'stat')).split(') ').last.split[19] == start
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def self.pins_known?
|
|
86
|
+
referenced_session_ids
|
|
87
|
+
true
|
|
88
|
+
rescue StandardError
|
|
89
|
+
false
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Cleanup preserves evidence referenced by all retained learning, mistake
|
|
93
|
+
# and durable memory records, not just the ordinary gold/open/hot pins.
|
|
94
|
+
def self.referenced_session_ids
|
|
95
|
+
references = PWN::Sessions.dependency_index
|
|
96
|
+
raise 'Uncertain dependency stores' unless references[:errors].empty?
|
|
97
|
+
|
|
98
|
+
references[:dependencies].keys
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Fingerprints include inode identity and nanosecond timestamps, not bodies.
|
|
102
|
+
def self.snapshot(path)
|
|
103
|
+
[path, path.sub(/\.jsonl\z/, '.meta.json')].to_h do |file|
|
|
104
|
+
raise 'Refusing symlink' if File.symlink?(file)
|
|
105
|
+
next [File.basename(file), nil] unless File.exist?(file)
|
|
106
|
+
|
|
107
|
+
stat = File.stat(file)
|
|
108
|
+
raise 'Not a regular file' unless stat.file?
|
|
109
|
+
|
|
110
|
+
[File.basename(file), { sha256: Digest::SHA256.file(file).hexdigest, size: stat.size, inode: stat.ino, device: stat.dev, mtime: stat.mtime.to_r.to_s, ctime: stat.ctime.to_r.to_s }]
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def self.publish(path, bytes)
|
|
115
|
+
temporary = "#{path}.#{SecureRandom.hex(8)}.tmp"
|
|
116
|
+
File.open(temporary, File::WRONLY | File::CREAT | File::EXCL | File::NOFOLLOW, 0o600) do |file|
|
|
117
|
+
file.write(bytes)
|
|
118
|
+
file.flush
|
|
119
|
+
file.fsync
|
|
120
|
+
end
|
|
121
|
+
File.rename(temporary, path)
|
|
122
|
+
File.open(File.dirname(path), File::RDONLY, &:fsync)
|
|
123
|
+
ensure
|
|
124
|
+
File.unlink(temporary) if temporary && File.exist?(temporary)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def self.backup(path, expected, result)
|
|
128
|
+
unless result[:archive_dir]
|
|
129
|
+
base = File.join(Dir.home, '.pwn', 'session_cleanup_archive')
|
|
130
|
+
raise 'Unsafe archive directory' if File.symlink?(base)
|
|
131
|
+
|
|
132
|
+
FileUtils.mkdir_p(base, mode: 0o700)
|
|
133
|
+
raise 'Unsafe archive permissions' unless File.stat(base).uid == Process.uid && (File.stat(base).mode & 0o777) == 0o700
|
|
134
|
+
|
|
135
|
+
result[:archive_dir] = File.join(base, "#{Time.now.utc.strftime('%Y%m%dT%H%M%S')}-#{SecureRandom.hex(8)}")
|
|
136
|
+
Dir.mkdir(result[:archive_dir], 0o700)
|
|
137
|
+
end
|
|
138
|
+
expected.each do |name, record|
|
|
139
|
+
next unless record
|
|
140
|
+
|
|
141
|
+
source = File.join(File.dirname(path), name)
|
|
142
|
+
bytes = File.binread(source)
|
|
143
|
+
raise 'Changed before backup' unless Digest::SHA256.hexdigest(bytes) == record[:sha256]
|
|
144
|
+
|
|
145
|
+
target = File.join(result[:archive_dir], name)
|
|
146
|
+
publish(target, bytes)
|
|
147
|
+
raise 'Backup verification failed' unless Digest::SHA256.file(target).hexdigest == record[:sha256]
|
|
148
|
+
end
|
|
149
|
+
publish(File.join(result[:archive_dir], 'ledger.json'), JSON.pretty_generate(result))
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def self.run(dry_run: true, current_session_id: nil, proc_root: '/proc')
|
|
153
|
+
raise 'Session cleanup requires the updated Sessions activity API; load the updated source or deploy it before running this helper' unless PWN::Sessions.respond_to?(:activate)
|
|
154
|
+
|
|
155
|
+
root = PWN::Sessions.sessions_dir
|
|
156
|
+
raise 'Refusing symlinked session directory' if File.symlink?(root)
|
|
157
|
+
|
|
158
|
+
current = current_session_id || (PWN::Env.dig(:ai, :session_id) if defined?(PWN::Env))
|
|
159
|
+
legacy = legacy_owners(proc_root)
|
|
160
|
+
result = { version: VERSION, dry_run: dry_run, scanned: 0, unnamed: 0, candidate_ids: [], deleted_ids: [], records: [], skipped: Hash.new(0), legacy_owners: legacy }
|
|
161
|
+
unless pins_known?
|
|
162
|
+
result[:skipped][:unknown_pins] = 1
|
|
163
|
+
result[:dependency_errors] = PWN::Sessions.dependency_index[:errors]
|
|
164
|
+
return result
|
|
165
|
+
end
|
|
166
|
+
dependencies = PWN::Sessions.dependency_index[:dependencies]
|
|
167
|
+
pins = referenced_session_ids
|
|
168
|
+
Dir.glob(File.join(root, '*.jsonl')).each do |path|
|
|
169
|
+
id = File.basename(path, '.jsonl')
|
|
170
|
+
result[:scanned] += 1
|
|
171
|
+
state = classify(path, id)
|
|
172
|
+
record = PWN::Sessions.title_status(session_id: id).merge(id: id, classification: state, dependencies: dependencies.fetch(id, []))
|
|
173
|
+
result[:records] << record
|
|
174
|
+
unless state == :unnamed
|
|
175
|
+
result[:skipped][state] += 1
|
|
176
|
+
next
|
|
177
|
+
end
|
|
178
|
+
expected = snapshot(path)
|
|
179
|
+
record[:files] = expected
|
|
180
|
+
result[:unnamed] += 1
|
|
181
|
+
if PWN::Sessions.active?(session_id: id)
|
|
182
|
+
record[:skip] = :active
|
|
183
|
+
result[:skipped][:active] += 1
|
|
184
|
+
next
|
|
185
|
+
end
|
|
186
|
+
if id == current || pins.include?(id)
|
|
187
|
+
reason = id == current ? :current : :referenced
|
|
188
|
+
record[:skip] = reason
|
|
189
|
+
result[:skipped][reason] += 1
|
|
190
|
+
next
|
|
191
|
+
end
|
|
192
|
+
result[:candidate_ids] << id
|
|
193
|
+
record[:apply_blocked_by] = :unknown_legacy_ownership unless legacy.empty?
|
|
194
|
+
next if dry_run
|
|
195
|
+
|
|
196
|
+
unless legacy.empty? && legacy_owners(proc_root).empty?
|
|
197
|
+
record[:skip] = :unknown_legacy_ownership
|
|
198
|
+
result[:skipped][:unknown_legacy_ownership] += 1
|
|
199
|
+
next
|
|
200
|
+
end
|
|
201
|
+
# Never unlink lock files: waiters must keep sharing the same inode.
|
|
202
|
+
lock_path = File.join(root, ".#{id}.cleanup.lock")
|
|
203
|
+
raise 'Refusing symlinked cleanup lock' if File.symlink?(lock_path)
|
|
204
|
+
|
|
205
|
+
File.open(lock_path, File::RDWR | File::CREAT | File::NOFOLLOW, 0o600) do |lock|
|
|
206
|
+
unless lock.flock(File::LOCK_EX | File::LOCK_NB)
|
|
207
|
+
result[:skipped][:locked] += 1
|
|
208
|
+
record[:skip] = :locked
|
|
209
|
+
next
|
|
210
|
+
end
|
|
211
|
+
File.open(path, File::RDONLY | File::NOFOLLOW) do |transcript|
|
|
212
|
+
unless transcript.flock(File::LOCK_EX | File::LOCK_NB)
|
|
213
|
+
result[:skipped][:locked] += 1
|
|
214
|
+
record[:skip] = :locked
|
|
215
|
+
next
|
|
216
|
+
end
|
|
217
|
+
# Activation and deletion serialize on the same per-session inode.
|
|
218
|
+
# Legacy ownership uncertainty still requires operator migration.
|
|
219
|
+
unless File.identical?(transcript, path) && classify(path, id) == :unnamed && snapshot(path) == expected && legacy_owners(proc_root).empty? && pins_known? && !referenced_session_ids.include?(id)
|
|
220
|
+
result[:skipped][:changed] += 1
|
|
221
|
+
record[:skip] = :changed
|
|
222
|
+
next
|
|
223
|
+
end
|
|
224
|
+
backup(path, expected, result)
|
|
225
|
+
unless snapshot(path) == expected && classify(path, id) == :unnamed && legacy_owners(proc_root).empty? && pins_known? && !referenced_session_ids.include?(id)
|
|
226
|
+
result[:skipped][:changed] += 1
|
|
227
|
+
record[:skip] = :changed
|
|
228
|
+
next
|
|
229
|
+
end
|
|
230
|
+
# Only verified, archived, unreferenced unsummarized sessions;
|
|
231
|
+
# current and all known reference pins checked above.
|
|
232
|
+
# The normal delete API's age pin is deliberately bypassed here.
|
|
233
|
+
PWN::Sessions.delete(session_id: id, current_session_id: current, force: true, cleanup_lock: lock)
|
|
234
|
+
raise 'Deletion did not remove both session records' if File.exist?(path) || File.exist?(path.sub(/\.jsonl\z/, '.meta.json'))
|
|
235
|
+
|
|
236
|
+
result[:deleted_ids] << id
|
|
237
|
+
record[:deleted] = true
|
|
238
|
+
publish(File.join(result[:archive_dir], 'ledger.json'), JSON.pretty_generate(result))
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
rescue StandardError => e
|
|
242
|
+
result[:skipped][:error] += 1
|
|
243
|
+
record[:error] = e.class.name if record
|
|
244
|
+
end
|
|
245
|
+
publish(File.join(result[:archive_dir], 'ledger.json'), JSON.pretty_generate(result)) if result[:archive_dir]
|
|
246
|
+
result
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
if $PROGRAM_NAME == __FILE__
|
|
252
|
+
raise 'Use --dry-run or --apply' unless ARGV.length == 1 && %w[--dry-run --apply].include?(ARGV.first)
|
|
253
|
+
|
|
254
|
+
puts JSON.generate(PWN::SessionCleanup.run(dry_run: ARGV.first == '--dry-run'))
|
|
255
|
+
end
|
|
@@ -28,11 +28,17 @@ Class methods take `(opts = {})` and read `opts`.
|
|
|
28
28
|
|
|
29
29
|
```ruby
|
|
30
30
|
PWN::AI::Agent::Learning.help
|
|
31
|
-
PWN::AI::Agent::Learning.
|
|
31
|
+
PWN::AI::Agent::Learning.evidence_transcript(opts)
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
## Public methods
|
|
35
35
|
|
|
36
|
+
- `evidence_transcript`
|
|
37
|
+
- `session_evidence`
|
|
38
|
+
- `evidence_complete`
|
|
39
|
+
- `migrate_evidence`
|
|
40
|
+
- `annotate_evidence`
|
|
41
|
+
- `rollback_evidence`
|
|
36
42
|
- `note_outcome`
|
|
37
43
|
- `consistency_check`
|
|
38
44
|
- `disputed_save`
|
|
@@ -63,6 +69,7 @@ PWN::AI::Agent::Learning.note_outcome(opts)
|
|
|
63
69
|
- `authors`
|
|
64
70
|
- `help`
|
|
65
71
|
- `compact!`
|
|
72
|
+
- `evidence_complete?`
|
|
66
73
|
- `gc_stores!`
|
|
67
74
|
- `lean!`
|
|
68
75
|
- `prune_outcomes!`
|
|
@@ -74,5 +81,5 @@ PWN::AI::Agent::Learning.note_outcome(opts)
|
|
|
74
81
|
|
|
75
82
|
## Verification
|
|
76
83
|
|
|
77
|
-
`PWN::AI::Agent::Learning.respond_to?(:
|
|
84
|
+
`PWN::AI::Agent::Learning.respond_to?(:evidence_transcript)` after the
|
|
78
85
|
module is loaded. Read the source for parameter names.
|