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.
- checksums.yaml +4 -4
- data/Gemfile +11 -11
- data/documentation/Configuration.md +2 -0
- data/documentation/General-PWN-Usage.md +68 -0
- data/documentation/Harness-Evaluation.md +312 -0
- data/documentation/Installation.md +104 -0
- data/documentation/Reinforcement-Learning.md +80 -0
- data/documentation/pwn-REPL.md +74 -0
- data/documentation/pwn-ai-Agent.md +122 -0
- data/etc/default_cron/session_cleanup.rb +260 -0
- data/etc/default_skills/pwn/ai/agent/dispatch/SKILL.md +1 -0
- data/etc/default_skills/pwn/ai/agent/learning/SKILL.md +9 -2
- data/etc/default_skills/pwn/ai/agent/open_goal/SKILL.md +1 -0
- data/etc/default_skills/pwn/ai/agent/skill_review/SKILL.md +2 -0
- data/etc/default_skills/pwn/ai/agent/solve/SKILL.md +4 -2
- data/etc/default_skills/pwn/ai/context_ingestion/SKILL.md +1 -1
- data/etc/default_skills/pwn/ai/http_retry/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/repl/SKILL.md +8 -0
- data/etc/default_skills/pwn/plugins/repl/ai_console/SKILL.md +1 -1
- data/etc/default_skills/pwn/plugins/repl/mesh/SKILL.md +4 -0
- data/etc/default_skills/pwn/plugins/repl/mesh/references/urls.md +3 -0
- data/etc/default_skills/pwn/redaction/SKILL.md +1 -1
- data/etc/default_skills/pwn/runtime_fingerprint/SKILL.md +47 -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/dispatch.rb +50 -6
- data/lib/pwn/ai/agent/learning.rb +365 -0
- data/lib/pwn/ai/agent/loop.rb +195 -114
- data/lib/pwn/ai/agent/open_goal.rb +43 -1
- data/lib/pwn/ai/agent/registry.rb +6 -1
- data/lib/pwn/ai/agent/request_runtime.rb +508 -17
- data/lib/pwn/ai/agent/result.rb +23 -6
- data/lib/pwn/ai/agent/reward.rb +16 -2
- data/lib/pwn/ai/agent/skill_review.rb +148 -1
- data/lib/pwn/ai/agent/solve.rb +420 -39
- data/lib/pwn/ai/agent/swarm.rb +21 -5
- data/lib/pwn/ai/agent/tools/learning.rb +20 -3
- data/lib/pwn/ai/agent/tools/sessions.rb +2 -1
- data/lib/pwn/ai/agent/tools/shell.rb +23 -0
- data/lib/pwn/ai/agent/turn_finalizer.rb +37 -6
- data/lib/pwn/ai/agent/verification.rb +42 -5
- data/lib/pwn/ai/anthropic.rb +17 -6
- data/lib/pwn/ai/cli.rb +52 -5
- data/lib/pwn/ai/context.rb +1 -1
- data/lib/pwn/ai/context_ingestion.rb +16 -1
- data/lib/pwn/ai/gemini.rb +9 -3
- data/lib/pwn/ai/grok.rb +11 -6
- data/lib/pwn/ai/http_retry.rb +14 -0
- data/lib/pwn/ai/mcp/combo_nation.rb +5 -2
- data/lib/pwn/ai/mcp.rb +5 -2
- data/lib/pwn/ai/ollama.rb +7 -3
- data/lib/pwn/ai/open_ai.rb +12 -12
- data/lib/pwn/ai/open_web_ui.rb +7 -3
- data/lib/pwn/ai/router.rb +56 -12
- data/lib/pwn/config.rb +6 -2
- data/lib/pwn/cron.rb +35 -1
- data/lib/pwn/migrate.rb +9 -1
- data/lib/pwn/plugins/repl/ai.rb +191 -32
- data/lib/pwn/plugins/repl/ai_console.rb +915 -34
- data/lib/pwn/plugins/repl/ai_console_commands.rb +23 -5
- data/lib/pwn/plugins/repl/ai_swarm.rb +65 -5
- data/lib/pwn/plugins/repl/mesh.rb +955 -81
- data/lib/pwn/plugins/repl.rb +11 -1
- data/lib/pwn/redaction.rb +29 -5
- data/lib/pwn/runtime_fingerprint.rb +84 -0
- data/lib/pwn/sessions.rb +567 -22
- data/lib/pwn/version.rb +1 -1
- data/lib/pwn.rb +1 -0
- data/spec/fixtures/harness/adapters.json +62 -0
- data/spec/fixtures/harness/adapters.json.sha256 +1 -0
- data/spec/fixtures/harness/experiment_gold.json +9 -0
- data/spec/fixtures/harness/experiment_gold.json.sha256 +1 -0
- data/spec/fixtures/harness/experiment_tasks.json +12 -0
- data/spec/fixtures/harness/experiment_tasks.json.sha256 +1 -0
- data/spec/fixtures/harness/live_comparison.json +21 -0
- data/spec/fixtures/harness/live_comparison.json.sha256 +1 -0
- data/spec/fixtures/harness/manifest.json +290 -0
- data/spec/fixtures/harness/manifest.json.sha256 +1 -0
- data/spec/integration/harness_evaluation_spec.rb +171 -0
- data/spec/integration/harness_layout_spec.rb +15 -0
- data/spec/integration/harness_live_spec.rb +26 -0
- data/spec/integration/harness_package_spec.rb +38 -0
- data/spec/integration/native_ruby_helpers_spec.rb +4 -2
- data/spec/integration/reinforced_feedback_loop_spec.rb +2 -2
- data/spec/lib/pwn/ai/agent/dispatch_spec.rb +47 -3
- data/spec/lib/pwn/ai/agent/learning_spec.rb +229 -0
- data/spec/lib/pwn/ai/agent/loop_spec.rb +479 -17
- data/spec/lib/pwn/ai/agent/open_goal_spec.rb +17 -0
- data/spec/lib/pwn/ai/agent/registry_spec.rb +10 -0
- data/spec/lib/pwn/ai/agent/request_runtime_spec.rb +232 -0
- data/spec/lib/pwn/ai/agent/result_spec.rb +33 -0
- data/spec/lib/pwn/ai/agent/signal_hygiene_spec.rb +1 -1
- data/spec/lib/pwn/ai/agent/skill_review_spec.rb +17 -5
- data/spec/lib/pwn/ai/agent/solve_spec.rb +390 -7
- data/spec/lib/pwn/ai/agent/swarm_spec.rb +29 -1
- data/spec/lib/pwn/ai/agent/tools/learning_spec.rb +14 -0
- data/spec/lib/pwn/ai/agent/tools/ruby_eval_spec.rb +2 -2
- data/spec/lib/pwn/ai/agent/turn_finalizer_spec.rb +20 -0
- data/spec/lib/pwn/ai/agent/verification_spec.rb +12 -0
- data/spec/lib/pwn/ai/anthropic_spec.rb +10 -0
- data/spec/lib/pwn/ai/cli_spec.rb +55 -1
- data/spec/lib/pwn/ai/context_ingestion_spec.rb +12 -0
- data/spec/lib/pwn/ai/mcp/combo_nation_spec.rb +10 -0
- data/spec/lib/pwn/ai/mcp_spec.rb +10 -0
- data/spec/lib/pwn/ai/open_ai_spec.rb +13 -0
- 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 +37 -4
- data/spec/lib/pwn/plugins/repl/ai_console_commands_spec.rb +9 -0
- data/spec/lib/pwn/plugins/repl/ai_console_completion_wrap_spec.rb +110 -0
- data/spec/lib/pwn/plugins/repl/ai_console_spec.rb +558 -8
- 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/ai_swarm_spec.rb +29 -0
- data/spec/lib/pwn/plugins/repl/ai_vault_edit_spec.rb +221 -0
- data/spec/lib/pwn/plugins/repl_mesh_channel_identity_spec.rb +134 -0
- data/spec/lib/pwn/plugins/repl_mesh_dispatch_spec.rb +2 -0
- data/spec/lib/pwn/plugins/repl_mesh_dm_transport_spec.rb +2 -1
- data/spec/lib/pwn/plugins/repl_mesh_multi_rx_spec.rb +315 -0
- data/spec/lib/pwn/plugins/repl_mesh_origin_gps_spec.rb +120 -0
- data/spec/lib/pwn/plugins/repl_mesh_rx_errors_spec.rb +325 -0
- data/spec/lib/pwn/plugins/repl_mesh_scroll_spec.rb +49 -0
- data/spec/lib/pwn/plugins/repl_mesh_transport_label_spec.rb +264 -0
- data/spec/lib/pwn/plugins/repl_spec.rb +8 -5
- data/spec/lib/pwn/redaction_spec.rb +20 -2
- data/spec/lib/pwn/runtime_fingerprint_spec.rb +53 -0
- 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/spec/spec_helper.rb +3 -1
- data/spec/support/harness/cli_faults.rb +107 -0
- data/spec/support/harness/evaluate.rb +176 -0
- data/spec/support/harness/experiment_preload.rb +20 -0
- data/spec/support/harness/experiments.rb +137 -0
- data/spec/support/harness/fake_provider.rb +80 -0
- data/spec/support/harness/fault_preload.rb +44 -0
- data/spec/support/harness/package_acceptance.rb +77 -0
- data/spec/support/harness/record_training.rb +18 -0
- data/spec/support/harness/reference_adapter.py +128 -0
- data/spec/support/harness/runtime_faults.rb +97 -0
- data/spec/support/harness/session_acceptance.rb +64 -0
- data/spec/support/harness/swarm_acceptance.rb +61 -0
- data/third_party/pwn_rdoc.jsonl +169 -2
- 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
|
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
|
|
@@ -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
|
|
@@ -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.
|
|
@@ -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.
|
|
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?(:
|
|
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;
|
|
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
|
|