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.
- checksums.yaml +7 -0
- data/.envrc +6 -0
- data/.github/workflows/deploy-github-pages.yml +52 -0
- data/.loki +10 -0
- data/.rubocop.yml +1 -0
- data/Archspec.rb +58 -0
- data/CHANGELOG.md +37 -0
- data/CLAUDE.md +99 -0
- data/LICENSE.txt +21 -0
- data/README.md +118 -0
- data/Rakefile +30 -0
- data/bin/am +13 -0
- data/docs/assets/pipeline.svg +100 -0
- data/docs/cli-reference.md +118 -0
- data/docs/daemon.md +104 -0
- data/docs/getting-started.md +112 -0
- data/docs/how-it-works.md +90 -0
- data/docs/index.md +74 -0
- data/docs/privacy.md +73 -0
- data/lib/robot_lab/am/cli.rb +180 -0
- data/lib/robot_lab/am/collector.rb +48 -0
- data/lib/robot_lab/am/config/defaults.yml +18 -0
- data/lib/robot_lab/am/config.rb +58 -0
- data/lib/robot_lab/am/daemon.rb +118 -0
- data/lib/robot_lab/am/daemon_controller.rb +152 -0
- data/lib/robot_lab/am/event.rb +22 -0
- data/lib/robot_lab/am/event_log.rb +40 -0
- data/lib/robot_lab/am/heartbeat.rb +45 -0
- data/lib/robot_lab/am/inferrer.rb +123 -0
- data/lib/robot_lab/am/intent.rb +9 -0
- data/lib/robot_lab/am/intent_writer.rb +36 -0
- data/lib/robot_lab/am/launchd.rb +108 -0
- data/lib/robot_lab/am/pid_file.rb +49 -0
- data/lib/robot_lab/am/redactor.rb +43 -0
- data/lib/robot_lab/am/version.rb +7 -0
- data/lib/robot_lab/am/watchers/claude_watcher.rb +84 -0
- data/lib/robot_lab/am/watchers/git_watcher.rb +57 -0
- data/lib/robot_lab/am/watchers/terminal_watcher.rb +42 -0
- data/lib/robot_lab/am.rb +40 -0
- data/mkdocs.yml +119 -0
- data/sig/robot_lab/am.rbs +6 -0
- metadata +118 -0
|
@@ -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
|
+

|
|
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.
|