robot_lab-am 0.0.1

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.
@@ -0,0 +1,118 @@
1
+ # CLI Reference
2
+
3
+ ```text
4
+ Usage: am COMMAND [options]
5
+ ```
6
+
7
+ ## Commands
8
+
9
+ ### `am snapshot`
10
+
11
+ One-shot pipeline: collect new activity, infer the current goal, write and
12
+ print `.robot_lab_am/current_intent.md`.
13
+
14
+ ```bash
15
+ am snapshot
16
+ am snapshot --repo ~/src/other_project
17
+ ```
18
+
19
+ Prints how many *new* events were collected (dedupe means a quiet repo
20
+ reports `0` but still re-infers over the accumulated recent window).
21
+
22
+ ### `am start`
23
+
24
+ Start the daemon for the repo. Forks and detaches by default; output goes to
25
+ `.robot_lab_am/daemon.log`. Fails with exit `1` if a daemon is already
26
+ running for the repo.
27
+
28
+ ```bash
29
+ am start
30
+ am start --foreground # stay attached (what launchd runs)
31
+ am start --interval 5 --debounce 60 # tune the cadence
32
+ ```
33
+
34
+ ### `am stop`
35
+
36
+ Send SIGTERM to the running daemon and wait (up to 10s) for a clean exit.
37
+ Detects a stale pid file (crash leftover), removes it, and reports it.
38
+
39
+ ### `am status`
40
+
41
+ Report liveness — pid checked against the real process table, plus heartbeat
42
+ detail when available. Exits `0` if running, `1` if not, so it's scriptable:
43
+
44
+ ```bash
45
+ am status && echo "monitoring" || am start
46
+ ```
47
+
48
+ ### `am install` / `am uninstall`
49
+
50
+ Write (or remove) the per-repo launchd agent plist under
51
+ `~/Library/LaunchAgents/`. Only touches the file; prints the `launchctl
52
+ bootstrap` / `bootout` commands for you to run. See
53
+ [launchd supervision](daemon.md#launchd-supervision).
54
+
55
+ ## Options
56
+
57
+ | Option | Applies to | Default | Meaning |
58
+ |--------|-----------|---------|---------|
59
+ | `--repo PATH` | all commands | current directory | The repo to watch. Everything else derives from this one path. |
60
+ | `--interval N` | `start`, `install` | `15` | Seconds between watcher polls |
61
+ | `--debounce N` | `start`, `install` | `300` | Minimum seconds between inference runs |
62
+ | `--foreground` | `start` | off | Run attached to the terminal instead of detaching |
63
+ | `-h`, `--help` | — | — | Show usage |
64
+ | `-v`, `--version` | — | — | Print version and exit |
65
+
66
+ With `install`, `--interval`/`--debounce` are baked into the plist's program
67
+ arguments.
68
+
69
+ `am --help` shows the *currently effective* interval/debounce — i.e. what your
70
+ config file and environment resolve to, not the bundled defaults.
71
+
72
+ ## Configuration
73
+
74
+ Settings cascade through [myway_config](https://github.com/MadBomber/myway_config)
75
+ (the same pattern as `robot_lab-to`), lowest to highest precedence:
76
+
77
+ 1. Bundled defaults (`lib/robot_lab/am/config/defaults.yml`)
78
+ 2. User config file — `~/.config/robot_lab_am/robot_lab_am.yml`
79
+ (or `$XDG_CONFIG_HOME/robot_lab_am/robot_lab_am.yml`)
80
+ 3. `RLAM_*` environment variables
81
+ 4. CLI flags / constructor keywords
82
+
83
+ The user config file uses flat keys:
84
+
85
+ ```yaml
86
+ # ~/.config/robot_lab_am/robot_lab_am.yml
87
+ interval: 30
88
+ debounce: 600
89
+ model: qwen/qwen3.8-27b
90
+ ```
91
+
92
+ | Setting | Default | Env var | Meaning |
93
+ |---------|---------|---------|---------|
94
+ | `provider` | `openai` | `RLAM_PROVIDER` | RubyLLM provider for inference |
95
+ | `model` | `qwen/qwen3.8-27b` | `RLAM_MODEL` | Model name |
96
+ | `api_base` | `http://localhost:1234/v1` | `RLAM_API_BASE` | OpenAI-compatible server URL (applied to the `openai` provider) |
97
+ | `api_key` | *(none)* | `RLAM_API_KEY` | API key — see [API keys](#api-keys) below |
98
+ | `interval` | `15` | `RLAM_INTERVAL` | Seconds between daemon polls |
99
+ | `debounce` | `300` | `RLAM_DEBOUNCE` | Minimum seconds between inference runs |
100
+ | `inference_window` | `100` | `RLAM_INFERENCE_WINDOW` | Most recent events sent to the model |
101
+ | `terminal_log` | `~/.activity_monitor/terminal_activity.log` | `RLAM_TERMINAL_LOG` | Preexec command log path |
102
+
103
+ ## Exit codes
104
+
105
+ | Code | Meaning |
106
+ |------|---------|
107
+ | `0` | Success (for `status`: daemon is running) |
108
+ | `1` | Error — unknown command/option, daemon not running, already running, start/stop timeout |
109
+ | `130` | Interrupted (Ctrl-C) |
110
+
111
+ ## API keys
112
+
113
+ None are needed for the default local LM Studio setup. The key passed to
114
+ RubyLLM cascades: `api_key` config / `RLAM_API_KEY` if set → the
115
+ chosen provider's conventional env var (`OPENAI_API_KEY`,
116
+ `ANTHROPIC_API_KEY`, …) → a placeholder value, which keyless local servers
117
+ accept. Inference runs in a scoped RubyLLM context, so a host application's
118
+ global RubyLLM configuration is inherited but never mutated.
data/docs/daemon.md ADDED
@@ -0,0 +1,104 @@
1
+ # The Daemon
2
+
3
+ `am start` runs the activity monitor continuously for one repo: polling the
4
+ watchers, accumulating events, and re-inferring the current intent whenever
5
+ activity arrives. This is what lets it notice *drift* in direction across
6
+ sessions, not just seed a single takeover run.
7
+
8
+ ## Lifecycle
9
+
10
+ ```bash
11
+ am start # fork + detach; logs to .robot_lab_am/daemon.log
12
+ am start --foreground # stay attached to the terminal (what launchd runs)
13
+ am status # liveness + heartbeat detail
14
+ am stop # SIGTERM, waits up to 10s for a clean exit
15
+ ```
16
+
17
+ `am status` when running:
18
+
19
+ ```text
20
+ daemon running for /Users/you/src/my_project (pid 41820)
21
+ heartbeat: 2s ago
22
+ events logged since start: 14
23
+ last inference: 2026-09-01T23:22:38Z
24
+ ```
25
+
26
+ Exit codes follow convention: `status` exits `0` when the daemon is running
27
+ and `1` when it isn't, so it works in scripts.
28
+
29
+ ## Inference cadence — debounced, not per-event
30
+
31
+ Re-running inference on every keystroke-equivalent event would be wasteful and
32
+ noisy. Instead:
33
+
34
+ - The daemon polls all three watchers every **`--interval`** seconds
35
+ (default **15**).
36
+ - New events set a *dirty* flag; inference runs only when the flag is set
37
+ **and** at least **`--debounce`** seconds (default **300**) have passed
38
+ since the last inference.
39
+ - The first activity after startup infers immediately.
40
+
41
+ ```bash
42
+ am start --interval 5 --debounce 60 # snappier, chattier
43
+ ```
44
+
45
+ Prefer a persistent setting? Put it in `~/.config/robot_lab_am/robot_lab_am.yml`
46
+ or `RLAM_INTERVAL`/`RLAM_DEBOUNCE` — flags always win over
47
+ both (see [Configuration](cli-reference.md#configuration)).
48
+
49
+ A failed inference (LM Studio not running, malformed response) is logged to
50
+ `daemon.log`, keeps the dirty flag set, and retries once the next debounce
51
+ window opens. It never kills the daemon.
52
+
53
+ ## Heartbeat
54
+
55
+ Every tick, the daemon rewrites `.robot_lab_am/heartbeat.json`:
56
+
57
+ ```json
58
+ {"pid": 41820, "updated_at": "2026-09-01T23:23:08Z",
59
+ "events_total": 14, "last_inference_at": "2026-09-01T23:22:38Z"}
60
+ ```
61
+
62
+ The heartbeat also beats **before** each inference call — a local model can
63
+ take 25+ seconds to respond, and a live daemon in the middle of a slow
64
+ inference must not look dead. `am status` combines the heartbeat with a real
65
+ process-table check (not just the pid file's existence), and detects, reports,
66
+ and cleans up stale pid files left by a crash.
67
+
68
+ ## State files
69
+
70
+ Everything lives under `.robot_lab_am/` in the watched repo:
71
+
72
+ | File | Purpose |
73
+ |------|---------|
74
+ | `events.jsonl` | Append-only event log — one JSON line per event |
75
+ | `current_intent.md` | The inferred intent artifact ([format](index.md#what-it-produces)) |
76
+ | `daemon.pid` | Pid of the running daemon |
77
+ | `heartbeat.json` | Rewritten every tick |
78
+ | `daemon.log` | Stdout/stderr of the detached daemon |
79
+
80
+ ## launchd supervision
81
+
82
+ To have macOS start the daemon at login and restart it if it crashes:
83
+
84
+ ```bash
85
+ am install
86
+ ```
87
+
88
+ This writes `~/Library/LaunchAgents/com.madbomber.robot-lab-am.<repo>-<hash>.plist`
89
+ (one agent per watched repo) configured with `RunAtLoad` and `KeepAlive`,
90
+ running `am start --foreground` under launchd's supervision. Installing only
91
+ writes the file — the command prints the `launchctl` invocations to actually
92
+ load it:
93
+
94
+ ```bash
95
+ launchctl bootstrap gui/$UID ~/Library/LaunchAgents/com.madbomber.robot-lab-am.<repo>-<hash>.plist
96
+ ```
97
+
98
+ `am uninstall` removes the plist (and prints the matching `launchctl bootout`
99
+ command if the agent was loaded).
100
+
101
+ !!! tip "One repo per daemon"
102
+ Watching several repos is "run N instances" — one `am start` (or one
103
+ `am install`) per repo — not a built-in registry. Each instance derives
104
+ everything it needs from its single `--repo` root.
@@ -0,0 +1,112 @@
1
+ # Getting Started
2
+
3
+ ## Installation
4
+
5
+ Add to your Gemfile:
6
+
7
+ ```ruby
8
+ gem "robot_lab-am"
9
+ ```
10
+
11
+ Despite the family name, the gem is standalone: its only dependencies are
12
+ [RubyLLM](https://rubyllm.com) (for the one-shot inference call) and
13
+ myway_config. No robot framework required.
14
+
15
+ Or install directly:
16
+
17
+ ```bash
18
+ gem install robot_lab-am
19
+ ```
20
+
21
+ This provides the `am` executable.
22
+
23
+ ## Prerequisite: a local LLM
24
+
25
+ Inference runs against LM Studio's OpenAI-compatible server by default, so
26
+ your activity log is summarized entirely on your own machine:
27
+
28
+ ```bash
29
+ lms server start # serves http://localhost:1234/v1
30
+ ```
31
+
32
+ The default model is `qwen/qwen3.8-27b`. To use a different local server or
33
+ model, set it once in your config file:
34
+
35
+ ```yaml
36
+ # ~/.config/robot_lab_am/robot_lab_am.yml
37
+ model: llama-3.3-70b
38
+ api_base: http://localhost:8080/v1
39
+ ```
40
+
41
+ or per-invocation via `RLAM_MODEL` / `RLAM_API_BASE`
42
+ (see [Configuration](cli-reference.md#configuration)). Any RubyLLM
43
+ provider/model can be substituted — but the default is deliberately not a
44
+ hosted provider. See [Privacy & Redaction](privacy.md).
45
+
46
+ ## Optional prerequisite: terminal capture
47
+
48
+ Commands you run in a terminal are read from
49
+ `~/.activity_monitor/terminal_activity.log`, written by a bash `preexec` hook
50
+ in your dotfiles (`~/.bashrc__activity_monitor`, registered via
51
+ [rcaloras/bash-preexec](https://github.com/rcaloras/bash-preexec)). Each line
52
+ is tab-delimited: `epoch.microseconds<TAB>cwd<TAB>command`.
53
+
54
+ Without the hook, the terminal source is simply empty — git and Claude Code
55
+ signals still work, and they are the richer sources anyway.
56
+
57
+ !!! note "Why a custom hook instead of bash history?"
58
+ `~/.bash_history` is global across every terminal and repo, with no
59
+ timestamps and no working directory — a raw `bundle exec rake test` line
60
+ can't be attributed to a repo or a point in time. The `preexec` hook
61
+ records both. It writes plain tab-delimited text (not JSON) because it
62
+ runs *before* every command executes and must not fork a subprocess.
63
+
64
+ ## Your first snapshot
65
+
66
+ From inside any git repo you've been working in:
67
+
68
+ ```bash
69
+ am snapshot
70
+ ```
71
+
72
+ This runs the whole pipeline once:
73
+
74
+ 1. Collects recent git commits + uncommitted changes, your own messages from
75
+ the repo's most recent Claude Code session, and terminal commands run
76
+ inside the repo
77
+ 2. Redacts credential-shaped values and appends the new events to
78
+ `.robot_lab_am/events.jsonl`
79
+ 3. Asks the local model to infer your current goal
80
+ 4. Writes `.robot_lab_am/current_intent.md` and prints it
81
+
82
+ Repeated snapshots are safe: events are deduplicated against the log, so
83
+ nothing is double-counted.
84
+
85
+ ## Running continuously
86
+
87
+ ```bash
88
+ am start # detaches; writes .robot_lab_am/daemon.log
89
+ am status # pid, heartbeat age, event count, last inference
90
+ am stop
91
+ ```
92
+
93
+ See [The Daemon](daemon.md) for lifecycle details, tuning, and launchd
94
+ supervision.
95
+
96
+ ## Ignore the state directory
97
+
98
+ Add the gem's state directory to the watched repo's `.gitignore`:
99
+
100
+ ```gitignore
101
+ /.robot_lab_am/
102
+ ```
103
+
104
+ (The daemon already excludes its own state directory from the git activity it
105
+ reports, but there's no reason to commit event logs.)
106
+
107
+ ## Handing off to robot_lab-to
108
+
109
+ `robot_lab-am` produces a file in a format `robot_lab-to` already knows how to
110
+ consume — front matter + prose, the same shape as its decision files. Invoke
111
+ `robot-to` with no objective to let the inferred intent seed the run, or with
112
+ an explicit objective to have the intent injected as extra context.
@@ -0,0 +1,90 @@
1
+ # How It Works
2
+
3
+ ## Signal sources
4
+
5
+ Three watchers read what's already on disk — no instrumentation of your tools:
6
+
7
+ | Source | What it yields | Read from |
8
+ |--------|----------------|-----------|
9
+ | **Git** | Recent commits, plus current uncommitted changes as a WIP summary | `git log` / `git status` in the watched repo |
10
+ | **Claude Code** | Your own plain-language messages from the repo's most recent session — stated intent, not just commands | Transcripts under `~/.claude/projects/<slug>/*.jsonl` |
11
+ | **Terminal** | Commands run inside the repo (tests, greps, ad hoc scripts) | The global `preexec` log, filtered to lines whose `cwd` is inside the repo |
12
+
13
+ The Claude Code transcripts are the richest, lowest-effort source: they
14
+ contain natural-language intent. Git is second-richest. The terminal source is
15
+ optional — see [Getting Started](getting-started.md#optional-prerequisite-terminal-capture).
16
+
17
+ Two deliberate exclusions: the `ClaudeWatcher` skips synthetic user turns that
18
+ are really tool-result payloads (only things *you* typed count), and the
19
+ `GitWatcher` filters the gem's own `.robot_lab_am/` state directory out of the
20
+ WIP summary — the monitor must not observe its own footprint.
21
+
22
+ ## Events
23
+
24
+ Every watcher emits one normalized shape:
25
+
26
+ ```json
27
+ {"timestamp": "2026-09-01T23:22:38Z",
28
+ "repo": "/Users/you/src/my_project",
29
+ "source": "terminal",
30
+ "kind": "command",
31
+ "summary": "asgard quality"}
32
+ ```
33
+
34
+ `source`/`kind` pairs: `git`/`commit`, `git`/`wip`, `claude_code`/
35
+ `user_message`, `terminal`/`command`.
36
+
37
+ ## Collection: redact, dedupe, append
38
+
39
+ Both `am snapshot` and every daemon tick run the same collection pass:
40
+
41
+ 1. Ask all three watchers for their recent window of events
42
+ 2. **Redact** credential-shaped values in each summary
43
+ ([details](privacy.md#3-redaction))
44
+ 3. **Dedupe** against everything already in `events.jsonl`, by fingerprint
45
+ 4. Append only what's genuinely new
46
+
47
+ Fingerprints make collection idempotent: repeated snapshots, daemon restarts,
48
+ and overlapping watcher windows never duplicate a line. Most events
49
+ fingerprint on `timestamp + source + kind + summary`; WIP events drop the
50
+ timestamp (it's collection time, not activity time), so an unchanged dirty
51
+ tree is recorded once, and again only when it actually changes.
52
+
53
+ ## Storage: JSONL, not SQLite
54
+
55
+ `events.jsonl` is an append-only file, one JSON object per line. The only
56
+ consumer is an LLM summarization pass over a bounded recent window — not ad
57
+ hoc queries — so a database wasn't worth the dependency. (Its structural
58
+ sibling, [`robot_lab-audit`](https://github.com/MadBomber/robot_lab-audit),
59
+ made the opposite call because audit logs *are* queried.)
60
+
61
+ ## Inference
62
+
63
+ The `Inferrer` takes the last 100 events, renders them oldest-to-newest as a
64
+ timestamped activity log, and asks the model — a one-shot
65
+ [RubyLLM](https://rubyllm.com) chat in a scoped context, no agent framework —
66
+ for exactly this YAML:
67
+
68
+ ```yaml
69
+ goal: <one or two sentences on what they're currently working toward>
70
+ confidence: <low|medium|high>
71
+ evidence:
72
+ - <short reference to a specific event that supports the goal>
73
+ open_questions:
74
+ - <anything ambiguous or unresolved>
75
+ ```
76
+
77
+ The response is parsed defensively — fenced YAML is extracted, and a
78
+ malformed reply degrades to using the raw text as the goal with
79
+ `confidence: unknown` rather than failing.
80
+
81
+ A repo with no recent activity short-circuits to a "No recent activity
82
+ detected" intent without calling the model at all.
83
+
84
+ ## The intent artifact
85
+
86
+ `IntentWriter` renders the result to `.robot_lab_am/current_intent.md` —
87
+ YAML front matter (confidence, timestamp, evidence, open questions) followed
88
+ by the goal as prose. The format and the `.robot_lab_<name>/` directory
89
+ convention deliberately match `robot_lab-to`'s decision files and run state,
90
+ so the consumer needs nothing new to read it.
data/docs/index.md ADDED
@@ -0,0 +1,74 @@
1
+ # robot_lab-am
2
+
3
+ **A standalone repo activity monitor** — a background daemon that watches what
4
+ *you* are doing in a repo (git activity, Claude Code sessions, terminal
5
+ commands) and distills it into a current-goal statement via a one-shot
6
+ [RubyLLM](https://rubyllm.com) call to a local model. Any tool can read the
7
+ intent artifact it writes; [`robot_lab-to`](https://madbomber.github.io/robot_lab-to/)
8
+ uses it to seed a takeover run, but nothing here depends on the RobotLab
9
+ framework.
10
+
11
+ The original point: when you say "take over," the robot should start from
12
+ *what you were actually doing* — not from a cold objective string you typed
13
+ at invocation time.
14
+
15
+ ![robot_lab-am pipeline](assets/pipeline.svg)
16
+
17
+ ## What it produces
18
+
19
+ Every inference writes `.robot_lab_am/current_intent.md` in the watched repo —
20
+ YAML front matter plus a prose goal statement:
21
+
22
+ ```markdown
23
+ ---
24
+ confidence: high
25
+ generated_at: '2026-09-01T23:22:56Z'
26
+ evidence:
27
+ - Commit d8b2f5f touches the daemon start/stop lifecycle
28
+ - Terminal shows repeated `asgard quality` runs
29
+ open_questions:
30
+ - Whether the launchd agent should auto-load after install
31
+ ---
32
+
33
+ Completing the robot_lab-am daemon implementation: start/stop/status
34
+ lifecycle, heartbeat, and launchd supervision, driven toward green
35
+ quality gates.
36
+ ```
37
+
38
+ `robot_lab-to` reads this file the way it already reads `notes.md` — as the
39
+ seed objective when none is given, or as extra context alongside one.
40
+
41
+ ## Design commitments
42
+
43
+ - **Local-first inference.** Your activity log — commits, commands, your own
44
+ Claude Code messages — never leaves the machine. Inference defaults to a
45
+ local model served by LM Studio's OpenAI-compatible API; no hosted provider,
46
+ no API key. See [Privacy & Redaction](privacy.md).
47
+ - **One repo per daemon.** Each daemon instance watches exactly one repo root.
48
+ Watching several repos means running several instances.
49
+ - **Read-side opt-in.** The terminal log spans every shell on the machine, but
50
+ the daemon only ever parses lines whose `cwd` falls inside the watched repo.
51
+ - **Redaction before storage.** Credential-shaped values are masked before an
52
+ event is written to disk or shown to the LLM.
53
+
54
+ ## The 60-second version
55
+
56
+ ```bash
57
+ gem install robot_lab-am
58
+
59
+ cd ~/src/my_project
60
+ am snapshot # one-shot: collect activity, infer, write current_intent.md
61
+ am start # or: run the daemon continuously
62
+ am status
63
+ ```
64
+
65
+ Continue with [Getting Started](getting-started.md).
66
+
67
+ ## Part of the RobotLab family
68
+
69
+ | Gem | Role |
70
+ |-----|------|
71
+ | [`robot_lab`](https://github.com/MadBomber/robot_lab) | Core framework: robots, networks, MCP, memory |
72
+ | **`robot_lab-am`** | Senses *human* activity outside any run and turns it into tasking context |
73
+ | [`robot_lab-to`](https://github.com/MadBomber/robot_lab-to) | The consumer — autonomous "takeover" loop seeded by the inferred intent |
74
+ | [`robot_lab-audit`](https://github.com/MadBomber/robot_lab-audit) | The structural sibling — logs *robot* activity, where this gem logs yours |
data/docs/privacy.md ADDED
@@ -0,0 +1,73 @@
1
+ # Privacy & Redaction
2
+
3
+ An activity monitor watches *you* — so the privacy posture is the design, not
4
+ an afterthought. Three layers:
5
+
6
+ ## 1. Local-first inference
7
+
8
+ Inference over your own activity log — commits, terminal commands, your Claude
9
+ Code messages — shouldn't require sending that log anywhere, or holding an API
10
+ key, just to summarize it.
11
+
12
+ The `Inferrer` therefore defaults to a **local model** served by LM Studio's
13
+ OpenAI-compatible API (`lms server start`, `http://localhost:1234/v1`),
14
+ explicitly not a hosted provider. With the default configuration, no network
15
+ call leaves the machine.
16
+
17
+ You *can* point it at any RubyLLM provider programmatically — that's a
18
+ deliberate, visible choice, never the default:
19
+
20
+ ```ruby
21
+ RobotLab::Am::Inferrer.new(model: "claude-sonnet-4", provider: :anthropic)
22
+ ```
23
+
24
+ ## 2. Read-side scoping of the terminal log
25
+
26
+ The `preexec` hook is necessarily global: it captures every command in every
27
+ shell, because a shell hook can't know in advance which repo will later be
28
+ "the watched one."
29
+
30
+ The opt-in boundary is enforced **at ingestion, not capture**: the watcher
31
+ only parses lines from `~/.activity_monitor/terminal_activity.log` whose
32
+ `cwd` falls inside the one repo root it's watching. Every other line in that
33
+ file is never parsed, stored, or sent to an LLM by this gem.
34
+
35
+ Git and Claude Code sources are already repo-scoped at the source.
36
+
37
+ ## 3. Redaction
38
+
39
+ Even in-scope activity can contain secrets — an API key in an `export`
40
+ statement, credentials in a test fixture showing up in a WIP diff summary.
41
+ The `Redactor` masks credential-shaped values in every event summary
42
+ **before** it is written to `events.jsonl` or shown to the LLM:
43
+
44
+ - `key` / `token` / `secret` / `password` / `credential` **assignments** —
45
+ `export MY_TOKEN=…`, `password: …`, `api_key => …` (value masked, name kept)
46
+ - **Bearer tokens** — `Authorization: Bearer …`
47
+ - **Well-known token formats** wherever they appear — OpenAI (`sk-…`), AWS
48
+ (`AKIA…`), GitHub (`ghp_…`, `github_pat_…`), Slack (`xoxb-…`)
49
+
50
+ ```text
51
+ export ANTHROPIC_API_KEY=sk-ant-abc123… → export ANTHROPIC_API_KEY=[REDACTED]
52
+ ```
53
+
54
+ !!! warning "Conservative by design — not a guarantee"
55
+ Redaction is pattern-based. It catches the common shapes of leaked
56
+ credentials, not every possible secret. Treat `.robot_lab_am/` like any
57
+ other local development artifact: keep it out of version control
58
+ (`/.robot_lab_am/` in `.gitignore`) and be deliberate if you ever switch
59
+ inference to a hosted provider.
60
+
61
+ ## What is stored, and where
62
+
63
+ Everything stays in the watched repo's `.robot_lab_am/` directory — redacted
64
+ event summaries, the inferred intent, and daemon runtime state. Nothing is
65
+ written outside the repo, and nothing is transmitted anywhere with the
66
+ default local-model configuration.
67
+
68
+ ## Log rotation
69
+
70
+ The global terminal log belongs to your dotfiles, and rotating or truncating
71
+ it there is always safe for this gem: the watcher takes only the most recent
72
+ in-scope lines each poll, and fingerprint dedupe means rotation can never
73
+ cause re-ingestion.