pwn 0.5.763 → 0.5.771

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 (145) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +11 -11
  3. data/documentation/Configuration.md +2 -0
  4. data/documentation/General-PWN-Usage.md +68 -0
  5. data/documentation/Harness-Evaluation.md +312 -0
  6. data/documentation/Installation.md +104 -0
  7. data/documentation/Reinforcement-Learning.md +80 -0
  8. data/documentation/pwn-REPL.md +74 -0
  9. data/documentation/pwn-ai-Agent.md +122 -0
  10. data/etc/default_cron/session_cleanup.rb +260 -0
  11. data/etc/default_skills/pwn/ai/agent/dispatch/SKILL.md +1 -0
  12. data/etc/default_skills/pwn/ai/agent/learning/SKILL.md +9 -2
  13. data/etc/default_skills/pwn/ai/agent/open_goal/SKILL.md +1 -0
  14. data/etc/default_skills/pwn/ai/agent/skill_review/SKILL.md +2 -0
  15. data/etc/default_skills/pwn/ai/agent/solve/SKILL.md +4 -2
  16. data/etc/default_skills/pwn/ai/context_ingestion/SKILL.md +1 -1
  17. data/etc/default_skills/pwn/ai/http_retry/SKILL.md +1 -0
  18. data/etc/default_skills/pwn/plugins/repl/SKILL.md +8 -0
  19. data/etc/default_skills/pwn/plugins/repl/ai_console/SKILL.md +1 -1
  20. data/etc/default_skills/pwn/plugins/repl/mesh/SKILL.md +4 -0
  21. data/etc/default_skills/pwn/plugins/repl/mesh/references/urls.md +3 -0
  22. data/etc/default_skills/pwn/redaction/SKILL.md +1 -1
  23. data/etc/default_skills/pwn/runtime_fingerprint/SKILL.md +47 -0
  24. data/etc/default_skills/pwn/sessions/SKILL.md +10 -0
  25. data/lib/pwn/ai/agent/curriculum.rb +9 -2
  26. data/lib/pwn/ai/agent/dispatch.rb +50 -6
  27. data/lib/pwn/ai/agent/learning.rb +365 -0
  28. data/lib/pwn/ai/agent/loop.rb +195 -114
  29. data/lib/pwn/ai/agent/open_goal.rb +43 -1
  30. data/lib/pwn/ai/agent/registry.rb +6 -1
  31. data/lib/pwn/ai/agent/request_runtime.rb +508 -17
  32. data/lib/pwn/ai/agent/result.rb +23 -6
  33. data/lib/pwn/ai/agent/reward.rb +16 -2
  34. data/lib/pwn/ai/agent/skill_review.rb +148 -1
  35. data/lib/pwn/ai/agent/solve.rb +420 -39
  36. data/lib/pwn/ai/agent/swarm.rb +21 -5
  37. data/lib/pwn/ai/agent/tools/learning.rb +20 -3
  38. data/lib/pwn/ai/agent/tools/sessions.rb +2 -1
  39. data/lib/pwn/ai/agent/tools/shell.rb +23 -0
  40. data/lib/pwn/ai/agent/turn_finalizer.rb +37 -6
  41. data/lib/pwn/ai/agent/verification.rb +42 -5
  42. data/lib/pwn/ai/anthropic.rb +17 -6
  43. data/lib/pwn/ai/cli.rb +52 -5
  44. data/lib/pwn/ai/context.rb +1 -1
  45. data/lib/pwn/ai/context_ingestion.rb +16 -1
  46. data/lib/pwn/ai/gemini.rb +9 -3
  47. data/lib/pwn/ai/grok.rb +11 -6
  48. data/lib/pwn/ai/http_retry.rb +14 -0
  49. data/lib/pwn/ai/mcp/combo_nation.rb +5 -2
  50. data/lib/pwn/ai/mcp.rb +5 -2
  51. data/lib/pwn/ai/ollama.rb +7 -3
  52. data/lib/pwn/ai/open_ai.rb +12 -12
  53. data/lib/pwn/ai/open_web_ui.rb +7 -3
  54. data/lib/pwn/ai/router.rb +56 -12
  55. data/lib/pwn/config.rb +6 -2
  56. data/lib/pwn/cron.rb +35 -1
  57. data/lib/pwn/migrate.rb +9 -1
  58. data/lib/pwn/plugins/repl/ai.rb +191 -32
  59. data/lib/pwn/plugins/repl/ai_console.rb +915 -34
  60. data/lib/pwn/plugins/repl/ai_console_commands.rb +23 -5
  61. data/lib/pwn/plugins/repl/ai_swarm.rb +65 -5
  62. data/lib/pwn/plugins/repl/mesh.rb +955 -81
  63. data/lib/pwn/plugins/repl.rb +11 -1
  64. data/lib/pwn/redaction.rb +29 -5
  65. data/lib/pwn/runtime_fingerprint.rb +84 -0
  66. data/lib/pwn/sessions.rb +567 -22
  67. data/lib/pwn/version.rb +1 -1
  68. data/lib/pwn.rb +1 -0
  69. data/spec/fixtures/harness/adapters.json +62 -0
  70. data/spec/fixtures/harness/adapters.json.sha256 +1 -0
  71. data/spec/fixtures/harness/experiment_gold.json +9 -0
  72. data/spec/fixtures/harness/experiment_gold.json.sha256 +1 -0
  73. data/spec/fixtures/harness/experiment_tasks.json +12 -0
  74. data/spec/fixtures/harness/experiment_tasks.json.sha256 +1 -0
  75. data/spec/fixtures/harness/live_comparison.json +21 -0
  76. data/spec/fixtures/harness/live_comparison.json.sha256 +1 -0
  77. data/spec/fixtures/harness/manifest.json +290 -0
  78. data/spec/fixtures/harness/manifest.json.sha256 +1 -0
  79. data/spec/integration/harness_evaluation_spec.rb +171 -0
  80. data/spec/integration/harness_layout_spec.rb +15 -0
  81. data/spec/integration/harness_live_spec.rb +26 -0
  82. data/spec/integration/harness_package_spec.rb +38 -0
  83. data/spec/integration/native_ruby_helpers_spec.rb +4 -2
  84. data/spec/integration/reinforced_feedback_loop_spec.rb +2 -2
  85. data/spec/lib/pwn/ai/agent/dispatch_spec.rb +47 -3
  86. data/spec/lib/pwn/ai/agent/learning_spec.rb +229 -0
  87. data/spec/lib/pwn/ai/agent/loop_spec.rb +479 -17
  88. data/spec/lib/pwn/ai/agent/open_goal_spec.rb +17 -0
  89. data/spec/lib/pwn/ai/agent/registry_spec.rb +10 -0
  90. data/spec/lib/pwn/ai/agent/request_runtime_spec.rb +232 -0
  91. data/spec/lib/pwn/ai/agent/result_spec.rb +33 -0
  92. data/spec/lib/pwn/ai/agent/signal_hygiene_spec.rb +1 -1
  93. data/spec/lib/pwn/ai/agent/skill_review_spec.rb +17 -5
  94. data/spec/lib/pwn/ai/agent/solve_spec.rb +390 -7
  95. data/spec/lib/pwn/ai/agent/swarm_spec.rb +29 -1
  96. data/spec/lib/pwn/ai/agent/tools/learning_spec.rb +14 -0
  97. data/spec/lib/pwn/ai/agent/tools/ruby_eval_spec.rb +2 -2
  98. data/spec/lib/pwn/ai/agent/turn_finalizer_spec.rb +20 -0
  99. data/spec/lib/pwn/ai/agent/verification_spec.rb +12 -0
  100. data/spec/lib/pwn/ai/anthropic_spec.rb +10 -0
  101. data/spec/lib/pwn/ai/cli_spec.rb +55 -1
  102. data/spec/lib/pwn/ai/context_ingestion_spec.rb +12 -0
  103. data/spec/lib/pwn/ai/mcp/combo_nation_spec.rb +10 -0
  104. data/spec/lib/pwn/ai/mcp_spec.rb +10 -0
  105. data/spec/lib/pwn/ai/open_ai_spec.rb +13 -0
  106. data/spec/lib/pwn/ai/router_spec.rb +53 -0
  107. data/spec/lib/pwn/banner_spec.rb +1 -1
  108. data/spec/lib/pwn/cron_spec.rb +4 -3
  109. data/spec/lib/pwn/migrate_spec.rb +37 -4
  110. data/spec/lib/pwn/plugins/repl/ai_console_commands_spec.rb +9 -0
  111. data/spec/lib/pwn/plugins/repl/ai_console_completion_wrap_spec.rb +110 -0
  112. data/spec/lib/pwn/plugins/repl/ai_console_spec.rb +558 -8
  113. data/spec/lib/pwn/plugins/repl/ai_result_chronology_spec.rb +210 -0
  114. data/spec/lib/pwn/plugins/repl/ai_spec.rb +209 -0
  115. data/spec/lib/pwn/plugins/repl/ai_swarm_spec.rb +29 -0
  116. data/spec/lib/pwn/plugins/repl/ai_vault_edit_spec.rb +221 -0
  117. data/spec/lib/pwn/plugins/repl_mesh_channel_identity_spec.rb +134 -0
  118. data/spec/lib/pwn/plugins/repl_mesh_dispatch_spec.rb +2 -0
  119. data/spec/lib/pwn/plugins/repl_mesh_dm_transport_spec.rb +2 -1
  120. data/spec/lib/pwn/plugins/repl_mesh_multi_rx_spec.rb +315 -0
  121. data/spec/lib/pwn/plugins/repl_mesh_origin_gps_spec.rb +120 -0
  122. data/spec/lib/pwn/plugins/repl_mesh_rx_errors_spec.rb +325 -0
  123. data/spec/lib/pwn/plugins/repl_mesh_scroll_spec.rb +49 -0
  124. data/spec/lib/pwn/plugins/repl_mesh_transport_label_spec.rb +264 -0
  125. data/spec/lib/pwn/plugins/repl_spec.rb +8 -5
  126. data/spec/lib/pwn/redaction_spec.rb +20 -2
  127. data/spec/lib/pwn/runtime_fingerprint_spec.rb +53 -0
  128. data/spec/lib/pwn/session_cleanup_default_spec.rb +72 -0
  129. data/spec/lib/pwn/session_cleanup_payload_spec.rb +593 -0
  130. data/spec/lib/pwn/sessions_spec.rb +344 -0
  131. data/spec/spec_helper.rb +3 -1
  132. data/spec/support/harness/cli_faults.rb +107 -0
  133. data/spec/support/harness/evaluate.rb +176 -0
  134. data/spec/support/harness/experiment_preload.rb +20 -0
  135. data/spec/support/harness/experiments.rb +137 -0
  136. data/spec/support/harness/fake_provider.rb +80 -0
  137. data/spec/support/harness/fault_preload.rb +44 -0
  138. data/spec/support/harness/package_acceptance.rb +77 -0
  139. data/spec/support/harness/record_training.rb +18 -0
  140. data/spec/support/harness/reference_adapter.py +128 -0
  141. data/spec/support/harness/runtime_faults.rb +97 -0
  142. data/spec/support/harness/session_acceptance.rb +64 -0
  143. data/spec/support/harness/swarm_acceptance.rb +61 -0
  144. data/third_party/pwn_rdoc.jsonl +169 -2
  145. metadata +67 -24
@@ -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
@@ -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
@@ -9,6 +9,75 @@ so it doesn't repeat it**.
9
9
 
10
10
  ## Two ways to run it
11
11
 
12
+ ### Local operator workspace
13
+
14
+ The curses console exposes these local views through the command menu:
15
+
16
+ * `/sessions` (or `/sessions picker`) opens a searchable picker. Type to filter
17
+ titles or stable IDs, use Up/Down to select, Enter to inspect local activity,
18
+ held activity lease, summary provenance, referenced-by paths and any recorded
19
+ unfinished goal, then `r` to resume that exact ID. Duplicate titles are never
20
+ guessed. Tab refreshes while preserving the selected ID. Esc restores the draft.
21
+ * `/readiness` previews the configured four-role routes, credential **presence**
22
+ (never credential values), local required tool paths, workspace and limits.
23
+ `w` toggles optional Git-worktree execution, `+`/`-` sets 1–10 repair rounds,
24
+ `t` cycles the bounded per-check timeout and `r` cycles the per-role timeout.
25
+ These console-lifetime options
26
+ feed the next mission without modifying the objective or encrypted config.
27
+ Navigation never logs in, installs dependencies or calls a provider. Readiness
28
+ is not a connectivity test; task-specific dependencies are not inferred.
29
+ * `/evidence` lists retained mission records and current console mission snapshots.
30
+ Enter inspects the original requirements, candidate revision, artifacts,
31
+ executed checks, observations, blockers and recorded outcome. Tab reloads
32
+ persisted records. A canceled mission retains its evidence. On a worktree
33
+ result, `d` opens the checksum-verified changed/deleted-file patch; `a` then
34
+ requires typing `APPLY <tested-revision>`. The backend rechecks source,
35
+ candidate, patch and tested revision before applying; it never stages or commits.
36
+ `x` requests removal of an owned checkout after typing `DISCARD <revision>`
37
+ (or its displayed ownership token before a candidate exists). A changed checkout
38
+ is refused with recovery guidance; patch and evidence are retained.
39
+ Running phase checkpoints are atomically persisted before publication, including
40
+ actual check records, uncovered requirements and repair reasons.
41
+ Worktrees isolate edits, **not** arbitrary code execution or network access.
42
+ * `/sessions cleanup` previews the same provenance/dependency policy as scheduled
43
+ cleanup. Inspect the exact IDs, paths, fingerprints, retention reasons and
44
+ uncertainty. `a`, then Enter confirms only the displayed candidate set.
45
+ Newly discovered or changed records are excluded. Apply rechecks activity,
46
+ references and source bytes under locks, backs up candidate bytes, and reports
47
+ actual deletions and skipped records. No deletion happens during preview.
48
+ * `/learning inbox` lists learning outcomes and retained skill proposals.
49
+ Enter inspects the exact procedure, source fields and fingerprints; `e` inspects
50
+ retained execution evidence. `a` approves, `x` rejects and `u` rolls back, each
51
+ requiring Enter confirmation. For skills, approval appends the proposed procedure
52
+ to an existing non-generated skill; rollback restores its backup only if the
53
+ approved file is unchanged. Source changes, absent or failing independent proof,
54
+ conflicted outcomes, generated skills and legacy proposals without procedure
55
+ payload remain blocked. Creation of a new skill is not enabled by this approval
56
+ API. Outcome decisions remain audit records, never fabricated training labels.
57
+ SkillReview evidence references use `learning_id` and `learning_sha256` for an
58
+ immutable learning row with retained full-session evidence and passing host
59
+ Verification checks. A model's `fixture_passed` assertion alone is insufficient,
60
+ including in `auto-safe` mode. No provider is called during review.
61
+ * `/guide` is a short dismissible guide. Its numbered examples insert editable
62
+ draft text without executing it; existing drafts are retained. `x` records
63
+ dismissal in a private local preference marker; Esc closes without writing it.
64
+
65
+ All these overlays support scrollback with PgUp/PgDn/Home/End and preserve the
66
+ mission draft. Requested cancellation and pause are not reported as completion:
67
+ pause becomes observed only at a coordinator boundary. Local list/readiness scans
68
+ run on a coalesced event worker; stale results cannot replace a newly opened view.
69
+ Ordinary curses and legacy steerable requests and missions share canonical-workspace
70
+ admission locks; a second writer receives
71
+ an actionable busy error. Other applications and unupgraded clients do not honor
72
+ these advisory locks.
73
+
74
+ For Ruby callers, `PWN::AI::Agent::Swarm.solve(request: objective, workspace: root,
75
+ worktree: true, rounds: 3, verification_timeout: 60, role_timeout: 300)` uses the
76
+ same mission backend. Timeouts are 1–600 seconds; rounds are 1–10. Worktree mode
77
+ requires a clean Git root (including ignored and untracked files). It does not
78
+ stage, commit or automatically apply the candidate. Preserve `request` verbatim;
79
+ options are separate from the objective.
80
+
12
81
  ```text
13
82
  # 1. Interactive curses console (inside the pwn REPL)
14
83
  pwn[CURRENT_VERSION]:001 >>> pwn-ai
@@ -164,6 +233,7 @@ changes only default values, not the configuration schema; no migration is neede
164
233
  | PgUp/PgDn | Scroll the timeline; new output does not pull a scrolled viewport to the bottom |
165
234
  | `/model`, `/sessions resume ID` | Show model settings / change the next request's model or session while idle |
166
235
  | `/system-role` | Open the active engine's multiline SYSTEM ROLE CONTENT editor while idle; Ctrl+S saves, Esc cancels |
236
+ | `/vault edit [key ...]` | Privately edit encrypted configuration or one nested mapping scope; Ctrl+S saves, Esc/Ctrl+C cancels |
167
237
  | `/steer INSTRUCTION` | Redirect the active request at a safe boundary |
168
238
  | `/input TEXT` | Send one line to an ordinary tool stdin prompt; not retained in input recall |
169
239
  | Ctrl+C | Cancel the active request cooperatively, or clear an idle draft |
@@ -197,6 +267,58 @@ the new role immediately, and a new session loads it from the vault. Request or
197
267
  swarm activity blocks role changes. No schema migration is required. The legacy
198
268
  non-curses interface reports that this editor requires an interactive terminal.
199
269
 
270
+ ### Private vault editor
271
+
272
+ `/vault edit` opens the complete **PRIVATE YAML** document; `/vault edit ai
273
+ anthropic` opens that nested mapping, and `/vault edit ai anthropic model`
274
+ opens a scalar. Tokens identify one nested key path, never filenames or multiple
275
+ disjoint scopes. The target and decryptor are the same `driver_opts.pwn_env_path`
276
+ and `driver_opts.pwn_dec_path` used by `pwn-vault`, defaulting to
277
+ `~/.pwn/pwn.yaml` and its existing `.decryptor` sidecar. Unknown paths are rejected; edit the parent mapping
278
+ to add a new key. Arrays are edited as a whole, not by numeric path indices.
279
+ Existing non-null types must be retained; an existing null may remain null or
280
+ be replaced by a YAML value. The root must retain an `ai` mapping, a supported
281
+ `ai.active`, and its engine mapping. Mapping keys may be added or removed within
282
+ the selected scope. Duplicate keys, aliases, object tags and multiple documents
283
+ are rejected. Nesting is limited to 100 mappings/sequences, including the merged
284
+ full document and decryptor parsing, before recursive YAML conversion. Invalid
285
+ input leaves the encrypted file intact. YAML formatting/comments are normalized on save.
286
+
287
+ Enter inserts a newline, arrows move, Home/End move to the document ends and
288
+ Ctrl+U clears. **Ctrl+S** explicitly saves encrypted; **Esc/Ctrl+C** discards
289
+ changes, and Ctrl+D leaves without saving. Save errors remain redacted inside
290
+ the modal so the text can be corrected. No editor contents enter SESSION,
291
+ history, completion suggestions, logs or provider requests. Completion suggests
292
+ only shipped schema key names, never reads/decrypts the vault, and never offers
293
+ configuration values; existing custom keys can be typed explicitly.
294
+
295
+ Plaintext is decrypted/edited in memory. Only encrypted bytes enter the private
296
+ temporary file, which is atomically renamed over the vault using the same
297
+ AES-256-CBC format/key/IV as `pwn-vault`; the decryptor is not regenerated.
298
+ Save re-reads the current document, preserves changes outside the selected scope,
299
+ and refuses a changed selected scope (the whole document for unscoped edits).
300
+ Editor instances serialize saves with a directory lock; a final ciphertext check
301
+ also detects intervening changes from other writers. Configuration is updated
302
+ locally without running authentication or `Config.refresh_env`. Editing is
303
+ blocked while a request/swarm is active. The legacy non-curses interface explains
304
+ that an interactive curses console is required and makes no changes.
305
+
306
+ ### Bounded nested data
307
+
308
+ JSON-bound tool results, schema copies/normalization, MCP arguments, Swarm key
309
+ conversion and persistence redaction reject cyclic or over-deep containers with
310
+ `JSON::NestingError`, using the serializer's 100-container default. Credential
311
+ collection for result redaction walks the environment by identity without Ruby
312
+ recursion, so repeated or cyclic mappings do not prevent credential replacement.
313
+ Redaction validates even sensitive-field containers before inspecting them;
314
+ invalid input fails closed rather than returning a raw value. Valid shared
315
+ branches remain independent output copies and inputs are not modified.
316
+
317
+ Local XML ingestion preflights element depth (maximum 100) before recursive
318
+ REXML XPath/formatting, and rejects excessive nesting with `ArgumentError`
319
+ before database or embedding work. These bounds cover the named preprocessing
320
+ paths, not arbitrary Ruby/plugin callbacks or all possible VM stack failures.
321
+
200
322
  ### Model and reasoning selection
201
323
 
202
324
  `/model` (also `show` or `status`) displays the current selection. Existing
@@ -0,0 +1,260 @@
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', approved_candidates: nil)
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
+ if approved_candidates && approved_candidates[id] != expected
181
+ record[:skip] = :not_approved_or_changed
182
+ result[:skipped][:not_approved_or_changed] += 1
183
+ next
184
+ end
185
+ result[:unnamed] += 1
186
+ if PWN::Sessions.active?(session_id: id)
187
+ record[:skip] = :active
188
+ result[:skipped][:active] += 1
189
+ next
190
+ end
191
+ if id == current || pins.include?(id)
192
+ reason = id == current ? :current : :referenced
193
+ record[:skip] = reason
194
+ result[:skipped][reason] += 1
195
+ next
196
+ end
197
+ result[:candidate_ids] << id
198
+ record[:apply_blocked_by] = :unknown_legacy_ownership unless legacy.empty?
199
+ next if dry_run
200
+
201
+ unless legacy.empty? && legacy_owners(proc_root).empty?
202
+ record[:skip] = :unknown_legacy_ownership
203
+ result[:skipped][:unknown_legacy_ownership] += 1
204
+ next
205
+ end
206
+ # Never unlink lock files: waiters must keep sharing the same inode.
207
+ lock_path = File.join(root, ".#{id}.cleanup.lock")
208
+ raise 'Refusing symlinked cleanup lock' if File.symlink?(lock_path)
209
+
210
+ File.open(lock_path, File::RDWR | File::CREAT | File::NOFOLLOW, 0o600) do |lock|
211
+ unless lock.flock(File::LOCK_EX | File::LOCK_NB)
212
+ result[:skipped][:locked] += 1
213
+ record[:skip] = :locked
214
+ next
215
+ end
216
+ File.open(path, File::RDONLY | File::NOFOLLOW) do |transcript|
217
+ unless transcript.flock(File::LOCK_EX | File::LOCK_NB)
218
+ result[:skipped][:locked] += 1
219
+ record[:skip] = :locked
220
+ next
221
+ end
222
+ # Activation and deletion serialize on the same per-session inode.
223
+ # Legacy ownership uncertainty still requires operator migration.
224
+ unless File.identical?(transcript, path) && classify(path, id) == :unnamed && snapshot(path) == expected && legacy_owners(proc_root).empty? && pins_known? && !referenced_session_ids.include?(id)
225
+ result[:skipped][:changed] += 1
226
+ record[:skip] = :changed
227
+ next
228
+ end
229
+ backup(path, expected, result)
230
+ unless snapshot(path) == expected && classify(path, id) == :unnamed && legacy_owners(proc_root).empty? && pins_known? && !referenced_session_ids.include?(id)
231
+ result[:skipped][:changed] += 1
232
+ record[:skip] = :changed
233
+ next
234
+ end
235
+ # Only verified, archived, unreferenced unsummarized sessions;
236
+ # current and all known reference pins checked above.
237
+ # The normal delete API's age pin is deliberately bypassed here.
238
+ PWN::Sessions.delete(session_id: id, current_session_id: current, force: true, cleanup_lock: lock)
239
+ raise 'Deletion did not remove both session records' if File.exist?(path) || File.exist?(path.sub(/\.jsonl\z/, '.meta.json'))
240
+
241
+ result[:deleted_ids] << id
242
+ record[:deleted] = true
243
+ publish(File.join(result[:archive_dir], 'ledger.json'), JSON.pretty_generate(result))
244
+ end
245
+ end
246
+ rescue StandardError => e
247
+ result[:skipped][:error] += 1
248
+ record[:error] = e.class.name if record
249
+ end
250
+ publish(File.join(result[:archive_dir], 'ledger.json'), JSON.pretty_generate(result)) if result[:archive_dir]
251
+ result
252
+ end
253
+ end
254
+ end
255
+
256
+ if $PROGRAM_NAME == __FILE__
257
+ raise 'Use --dry-run or --apply' unless ARGV.length == 1 && %w[--dry-run --apply].include?(ARGV.first)
258
+
259
+ puts JSON.generate(PWN::SessionCleanup.run(dry_run: ARGV.first == '--dry-run'))
260
+ end
@@ -36,6 +36,7 @@ PWN::AI::Agent::Dispatch.call(opts)
36
36
  - `call`
37
37
  - `repair_name`
38
38
  - `tool_calls_from_text`
39
+ - `scheduling`
39
40
  - `effect`
40
41
  - `authors`
41
42
  - `help`
@@ -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.note_outcome(opts)
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?(:note_outcome)` after the
84
+ `PWN::AI::Agent::Learning.respond_to?(:evidence_transcript)` after the
78
85
  module is loaded. Read the source for parameter names.
@@ -35,6 +35,7 @@ PWN::AI::Agent::OpenGoal.goal_file(opts)
35
35
 
36
36
  - `goal_file`
37
37
  - `current`
38
+ - `for_session`
38
39
  - `begin`
39
40
  - `clear`
40
41
  - `resume`
@@ -36,6 +36,8 @@ PWN::AI::Agent::SkillReview.review(opts)
36
36
  - `review`
37
37
  - `review_turn`
38
38
  - `note_reuse`
39
+ - `decide`
40
+ - `evidence_transcript`
39
41
  - `authors`
40
42
  - `help`
41
43
 
@@ -28,11 +28,13 @@ Class methods take `(opts = {})` and read `opts`.
28
28
 
29
29
  ```ruby
30
30
  PWN::AI::Agent::Solve.help
31
- PWN::AI::Agent::Solve.help(opts)
31
+ PWN::AI::Agent::Solve.apply_patch(opts)
32
32
  ```
33
33
 
34
34
  ## Public methods
35
35
 
36
+ - `apply_patch`
37
+ - `cleanup_worktree`
36
38
  - `authors`
37
39
  - `help`
38
40
 
@@ -42,5 +44,5 @@ PWN::AI::Agent::Solve.help(opts)
42
44
 
43
45
  ## Verification
44
46
 
45
- `PWN::AI::Agent::Solve.respond_to?(:authors)` after the
47
+ `PWN::AI::Agent::Solve.respond_to?(:apply_patch)` after the
46
48
  module is loaded. Read the source for parameter names.
@@ -12,7 +12,7 @@ metadata:
12
12
 
13
13
  # PWN::AI::Context
14
14
 
15
- Local evidence ingestion; unavailable embeddings never become synthetic vectors.
15
+ Local evidence ingestion; XML is bounded to 100 element levels before XPath. Unavailable embeddings never become synthetic vectors.
16
16
 
17
17
  ## When to use
18
18