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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: cd50cc952ad3b357990770041acd1bcce2dd0de11ecba0074e5affb1d167f555
|
|
4
|
+
data.tar.gz: 5e60e4ecbd1eff84b5998d63fea8f64387f7f546f7e666268481877ccf055626
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 3e31eedae9a07189737093c35be19575d42a3307ef7523ed2221de5f08de0244144e33bdb1f8481875a360cfa6ac71f1ef4f07a6529f1978df59f7ade928def7
|
|
7
|
+
data.tar.gz: b512f414d2f4b1538ee11543dfdcbf64a6a4c161096feef9968b4e0359f33d56772a24da549374371e1de1fc2fb4f5aa29fc1b56365f584cc17ccc31cfc26c4c
|
data/.envrc
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
name: Deploy Documentation to GitHub Pages
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches:
|
|
5
|
+
- main
|
|
6
|
+
- develop
|
|
7
|
+
paths:
|
|
8
|
+
- "docs/**"
|
|
9
|
+
- "mkdocs.yml"
|
|
10
|
+
- ".github/workflows/deploy-github-pages.yml"
|
|
11
|
+
workflow_dispatch:
|
|
12
|
+
|
|
13
|
+
permissions:
|
|
14
|
+
contents: write
|
|
15
|
+
pages: write
|
|
16
|
+
id-token: write
|
|
17
|
+
|
|
18
|
+
jobs:
|
|
19
|
+
deploy:
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
steps:
|
|
22
|
+
- name: Checkout code
|
|
23
|
+
uses: actions/checkout@v4
|
|
24
|
+
with:
|
|
25
|
+
fetch-depth: 0
|
|
26
|
+
|
|
27
|
+
- name: Setup Python
|
|
28
|
+
uses: actions/setup-python@v5
|
|
29
|
+
with:
|
|
30
|
+
python-version: 3.x
|
|
31
|
+
|
|
32
|
+
- name: Install dependencies
|
|
33
|
+
run: |
|
|
34
|
+
pip install mkdocs
|
|
35
|
+
pip install mkdocs-material
|
|
36
|
+
pip install mkdocs-macros-plugin
|
|
37
|
+
pip install mike
|
|
38
|
+
|
|
39
|
+
- name: Configure Git
|
|
40
|
+
run: |
|
|
41
|
+
git config --local user.email "action@github.com"
|
|
42
|
+
git config --local user.name "GitHub Action"
|
|
43
|
+
|
|
44
|
+
- name: Build MkDocs site
|
|
45
|
+
run: mkdocs build
|
|
46
|
+
|
|
47
|
+
- name: Deploy to GitHub Pages
|
|
48
|
+
uses: peaceiris/actions-gh-pages@v4
|
|
49
|
+
with:
|
|
50
|
+
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
51
|
+
publish_dir: ./site
|
|
52
|
+
keep_files: true
|
data/.loki
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# robot_lab-am — watches repo activity (git, terminal, Claude Code sessions) to infer intent.
|
|
3
|
+
|
|
4
|
+
import_up "repo_dev.loki"
|
|
5
|
+
|
|
6
|
+
class Tasks
|
|
7
|
+
@@gem_name ||= "robot_lab-am".freeze
|
|
8
|
+
|
|
9
|
+
header "robot_lab-am v#{gem_version} — activity monitor for RobotLab"
|
|
10
|
+
end
|
data/.rubocop.yml
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
inherit_from: ../.rubocop-base.yml
|
data/Archspec.rb
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# robot_lab-am is a plain Ruby gem (RobotLab::Am), not a Rails app -- there is
|
|
2
|
+
# no app/ tree, no controllers/models/views, so the :rails preset doesn't
|
|
3
|
+
# apply. These are the actual boundaries described in ARCHITECTURE.md and
|
|
4
|
+
# CLAUDE.md.
|
|
5
|
+
|
|
6
|
+
component :watchers, in: "lib/robot_lab/am/watchers/**/*.rb"
|
|
7
|
+
component :event_log, in: "lib/robot_lab/am/event_log.rb"
|
|
8
|
+
component :inferrer, in: "lib/robot_lab/am/inferrer.rb"
|
|
9
|
+
component :intent_writer, in: "lib/robot_lab/am/intent_writer.rb"
|
|
10
|
+
component :cli, in: "lib/robot_lab/am/cli.rb"
|
|
11
|
+
|
|
12
|
+
# NOTE: every file here reopens `module RobotLab` (shared with the external
|
|
13
|
+
# robot_lab gem's own top-level module -- Inferrer calls RobotLab.build), so
|
|
14
|
+
# a `dependencies.forbid` rule keyed on components would treat that one bare
|
|
15
|
+
# `RobotLab.build` call as "depends on every component" -- every file
|
|
16
|
+
# "defines" the bare RobotLab constant. Naming the actual target constants
|
|
17
|
+
# instead of components sidesteps that ambiguity (same fix robot_lab's own
|
|
18
|
+
# Archspec.rb applies for the same reason).
|
|
19
|
+
|
|
20
|
+
# Watchers are pure activity collectors -- git/Claude Code/terminal in,
|
|
21
|
+
# Event out (see ARCHITECTURE.md "Ingestion & normalization"). They must not
|
|
22
|
+
# know how an Event gets stored, summarized, or reported; CLI is the only
|
|
23
|
+
# thing that wires collection to storage/inference.
|
|
24
|
+
watchers.cannot_reference_constants "RobotLab::Am::EventLog", "RobotLab::Am::Inferrer",
|
|
25
|
+
"RobotLab::Am::IntentWriter", "RobotLab::Am::CLI",
|
|
26
|
+
because: "a watcher's only job is producing Events -- what " \
|
|
27
|
+
"happens to an Event afterward is not its concern"
|
|
28
|
+
|
|
29
|
+
# Inferrer consumes a bounded window of Events (already collected) and
|
|
30
|
+
# produces an Intent; it must not reach back into how those Events were
|
|
31
|
+
# gathered, or which specific source produced them.
|
|
32
|
+
inferrer.cannot_reference_constants "RobotLab::Am::Watchers::GitWatcher",
|
|
33
|
+
"RobotLab::Am::Watchers::ClaudeWatcher",
|
|
34
|
+
"RobotLab::Am::Watchers::TerminalWatcher",
|
|
35
|
+
"RobotLab::Am::EventLog", "RobotLab::Am::IntentWriter",
|
|
36
|
+
"RobotLab::Am::CLI",
|
|
37
|
+
because: "Inferrer only knows the normalized Event shape, never " \
|
|
38
|
+
"a specific watcher, the log file, or how its own " \
|
|
39
|
+
"output is used"
|
|
40
|
+
|
|
41
|
+
# IntentWriter is a pure renderer: Intent in, front-matter + prose file out.
|
|
42
|
+
# It must not know how that Intent was produced.
|
|
43
|
+
intent_writer.cannot_reference_constants "RobotLab::Am::Watchers::GitWatcher",
|
|
44
|
+
"RobotLab::Am::Watchers::ClaudeWatcher",
|
|
45
|
+
"RobotLab::Am::Watchers::TerminalWatcher",
|
|
46
|
+
"RobotLab::Am::EventLog", "RobotLab::Am::Inferrer",
|
|
47
|
+
"RobotLab::Am::CLI",
|
|
48
|
+
because: "IntentWriter only serializes an already-built " \
|
|
49
|
+
"Intent -- it has no business asking how one was " \
|
|
50
|
+
"inferred"
|
|
51
|
+
|
|
52
|
+
# CLAUDE.md/ARCHITECTURE.md: GitWatcher subprocesses `git` and must never
|
|
53
|
+
# build a command via string interpolation (the same rule robot_lab-to's
|
|
54
|
+
# CommitManager enforces for its own git ops) -- Open3.capture3 with an
|
|
55
|
+
# explicit argv array only.
|
|
56
|
+
watchers.cannot_call :system, receiver: :none,
|
|
57
|
+
because: "git subprocess calls must go through Open3.capture3 with an " \
|
|
58
|
+
"argv array, never system()/backticks with an interpolated string"
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
## [Unreleased]
|
|
2
|
+
|
|
3
|
+
- **Standalone: dropped the robot_lab dependency.** Inference is a one-shot
|
|
4
|
+
prompt, so `Inferrer` now calls RubyLLM directly (scoped
|
|
5
|
+
`RubyLLM.context`, `assume_model_exists: true`) instead of building a
|
|
6
|
+
`RobotLab::Robot`. Dependencies are now just `ruby_llm` + `myway_config`,
|
|
7
|
+
making the gem useful outside the robot_lab-to environment. New `api_key`
|
|
8
|
+
setting / `RLAM_API_KEY` (cascades to the provider's conventional
|
|
9
|
+
env var, then a placeholder for keyless local servers); the
|
|
10
|
+
`ROBOT_LAB_RUBY_LLM__OPENAI_API_BASE` coupling is gone. Injectable seam
|
|
11
|
+
renamed `robot:` -> `chat:`.
|
|
12
|
+
|
|
13
|
+
- Continuous daemon: `am start|stop|status` — forked/detached process with pid
|
|
14
|
+
file, per-tick heartbeat (`.robot_lab_am/heartbeat.json`), and debounced
|
|
15
|
+
inference (new-activity flag + `--debounce` window; failed inference warns
|
|
16
|
+
and retries instead of killing the daemon)
|
|
17
|
+
- launchd integration: `am install|uninstall` write/remove a per-repo agent
|
|
18
|
+
plist (`RunAtLoad` + `KeepAlive`) that supervises `am start --foreground`
|
|
19
|
+
- `Collector`: fingerprint-based dedupe against `events.jsonl`, so repeated
|
|
20
|
+
snapshots and daemon restarts never duplicate events
|
|
21
|
+
- `Redactor`: masks credential-shaped values (key/token/secret/password
|
|
22
|
+
assignments, bearer tokens, known token formats) before events are stored
|
|
23
|
+
or sent to the LLM
|
|
24
|
+
- `GitWatcher` no longer reports the gem's own `.robot_lab_am/` state
|
|
25
|
+
directory as uncommitted work
|
|
26
|
+
- `am snapshot` now reports only *new* events and infers over the accumulated
|
|
27
|
+
recent window of the event log
|
|
28
|
+
- MkDocs documentation site (Material theme) with GitHub Pages deploy workflow
|
|
29
|
+
- myway_config-based configuration (`RobotLab::Am::Config`, same pattern as
|
|
30
|
+
robot_lab-to): bundled defaults → `~/.config/robot_lab_am/robot_lab_am.yml` →
|
|
31
|
+
`RLAM_*` env vars → CLI flags. Tunables: provider, model, api_base,
|
|
32
|
+
interval, debounce, inference_window, terminal_log. `am --help` now shows
|
|
33
|
+
the effective interval/debounce
|
|
34
|
+
|
|
35
|
+
## [0.1.0] - 2026-08-31
|
|
36
|
+
|
|
37
|
+
- Initial release
|
data/CLAUDE.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## What This Gem Does
|
|
6
|
+
|
|
7
|
+
`robot_lab-am` ("activity monitor") is a **standalone** gem (RobotLab-family
|
|
8
|
+
naming, but no robot_lab dependency — just `ruby_llm` + `myway_config`) that
|
|
9
|
+
watches a repo working directory — git activity, Claude Code session
|
|
10
|
+
transcripts, and terminal commands — and infers the current goal or
|
|
11
|
+
direction of work via a one-shot local-LLM call. The inferred intent seeds
|
|
12
|
+
[`robot_lab-to`](https://github.com/MadBomber/robot_lab-to)'s takeover runs with
|
|
13
|
+
real context instead of a cold objective string, but any tool can consume it.
|
|
14
|
+
|
|
15
|
+
**Status: complete v1 — one-shot pipeline and continuous daemon.**
|
|
16
|
+
`am snapshot` collects git/Claude Code/terminal activity, infers a goal via
|
|
17
|
+
a local LLM, and writes `.robot_lab_am/current_intent.md`. `am start`/`stop`/
|
|
18
|
+
`status` run the continuous daemon (detached, pid file + heartbeat,
|
|
19
|
+
debounced inference), and `am install`/`uninstall` manage a launchd agent
|
|
20
|
+
that supervises it. All verified end to end against a live daemon and a
|
|
21
|
+
real LM Studio model. See `ARCHITECTURE.md` for the component survey and
|
|
22
|
+
decisions.
|
|
23
|
+
|
|
24
|
+
## Commands
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
bundle exec rake test # all tests
|
|
28
|
+
bundle exec rake test_file[path] # single test file
|
|
29
|
+
asgard quality # all *_check gates in parallel (tests + coverage,
|
|
30
|
+
# rubocop, flog, flay, reek, fasterer, typos, ...)
|
|
31
|
+
asgard doc_builder # build the MkDocs site
|
|
32
|
+
asgard doc_server # serve docs locally
|
|
33
|
+
bin/console # IRB shell with gem loaded
|
|
34
|
+
bin/am --help # CLI help
|
|
35
|
+
bin/am snapshot [--repo PATH] # one-shot: collect + infer + write intent
|
|
36
|
+
bin/am start [--foreground] # start the daemon (detached by default)
|
|
37
|
+
bin/am stop|status # stop / inspect the daemon
|
|
38
|
+
bin/am install|uninstall # manage the launchd agent plist
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Decided so far (see ARCHITECTURE.md for detail)
|
|
42
|
+
|
|
43
|
+
- **Deployment model**: continuous background daemon, not an on-demand
|
|
44
|
+
snapshot — it should notice drift across sessions, not just seed one
|
|
45
|
+
takeover run.
|
|
46
|
+
- **Terminal capture**: `~/.bashrc_history` is a single global `HISTFILE`
|
|
47
|
+
shared across every terminal and repo, with no timestamps or cwd, so it
|
|
48
|
+
can't attribute a command to a repo or a point in time. Instead,
|
|
49
|
+
`~/.bashrc__activity_monitor` (outside this repo, in the user's dotfiles)
|
|
50
|
+
registers a `preexec_functions` hook that appends
|
|
51
|
+
`epoch.microseconds\tcwd\tcommand` to `~/.activity_monitor/terminal_activity.log`
|
|
52
|
+
on every command. It's tab-delimited, not JSON, because it runs in
|
|
53
|
+
`preexec` — *before* the command executes — so it must not fork a
|
|
54
|
+
subprocess or every command gets delayed. Parsing/structuring that file
|
|
55
|
+
is this gem's job on the read side.
|
|
56
|
+
- **Storage**: JSONL (`.robot_lab_am/events.jsonl` in the watched repo),
|
|
57
|
+
not SQLite — the only consumer is an LLM summarization pass, not ad
|
|
58
|
+
hoc queries.
|
|
59
|
+
- **Inference model**: local, not hosted. `Inferrer` defaults to
|
|
60
|
+
`provider: :openai` / `model: "qwen/qwen3.8-27b"` against LM Studio's
|
|
61
|
+
OpenAI-compatible server (`lms server start`, `localhost:1234/v1`) —
|
|
62
|
+
explicitly not Anthropic. Your own activity log shouldn't have to
|
|
63
|
+
leave the machine, or need an API key, just to be summarized.
|
|
64
|
+
- **RubyLLM directly, no robots** (2026-09-02): the inference is a
|
|
65
|
+
one-shot prompt, so `Inferrer` calls
|
|
66
|
+
`RubyLLM.context.chat(model:, provider:, assume_model_exists: true)`
|
|
67
|
+
in a scoped context (host apps' global RubyLLM config inherited,
|
|
68
|
+
never mutated). The gem no longer depends on `robot_lab` — deps are
|
|
69
|
+
`ruby_llm` + `myway_config` only, so it's usable outside the
|
|
70
|
+
robot_lab-to environment. Injectable seam is `chat:` (responds to
|
|
71
|
+
`#ask(prompt)` returning a message with `#content`).
|
|
72
|
+
- **Configuration**: `Config < MywayConfig::Base` (same pattern as
|
|
73
|
+
robot_lab-to). Cascade: `config/defaults.yml` →
|
|
74
|
+
`~/.config/robot_lab_am/robot_lab_am.yml` (flat keys) →
|
|
75
|
+
`RLAM_*` env vars → CLI flags / constructor keywords.
|
|
76
|
+
Settings: provider, model, api_base, interval, debounce,
|
|
77
|
+
inference_window, terminal_log. `Am.config` is the memoized
|
|
78
|
+
process-wide instance (`Am.reset_config!` in tests). Caveat: the
|
|
79
|
+
defaults.yml-backed ivars are assigned by `super()` — never pre-nil
|
|
80
|
+
them in `Config#initialize`.
|
|
81
|
+
|
|
82
|
+
- **Inference cadence**: debounced — the daemon polls watchers every
|
|
83
|
+
`--interval` seconds (default 15) but re-infers only when new events
|
|
84
|
+
have arrived since the last inference, and at most once per
|
|
85
|
+
`--debounce` seconds (default 300). A failed inference (LLM down) is
|
|
86
|
+
retried on the same debounce, and never kills the daemon.
|
|
87
|
+
- **Redaction**: `Redactor` masks credential-shaped values (key/token/
|
|
88
|
+
secret/password assignments, bearer tokens, well-known token formats)
|
|
89
|
+
before any event is stored or sent to the LLM.
|
|
90
|
+
- **Dedupe**: `Collector` fingerprints events against the existing
|
|
91
|
+
`events.jsonl`, so repeated snapshots/daemon restarts never duplicate
|
|
92
|
+
log lines. `wip` events dedupe on summary (their timestamps are
|
|
93
|
+
collection-time), everything else on timestamp + summary.
|
|
94
|
+
|
|
95
|
+
## Testing
|
|
96
|
+
|
|
97
|
+
Minitest with SimpleCov (branch coverage tracked). Same conventions as the
|
|
98
|
+
other gems in this family — see the workspace-level `CLAUDE.md` and
|
|
99
|
+
`.claude/rules/rubocop.md`.
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dewayne VanHoozer
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# robot_lab-am
|
|
2
|
+
|
|
3
|
+
A standalone activity monitor that watches what's happening in a repo working
|
|
4
|
+
directory — git activity, Claude Code session transcripts, and terminal
|
|
5
|
+
commands — and infers the current goal/direction using a local LLM via
|
|
6
|
+
[RubyLLM](https://rubyllm.com). The inferred intent is written to a well-known
|
|
7
|
+
artifact (`.robot_lab_am/current_intent.md`) that any tool can read;
|
|
8
|
+
[`robot_lab-to`](https://github.com/MadBomber/robot_lab-to) uses it to seed a
|
|
9
|
+
takeover run with real context instead of a cold objective string, but nothing
|
|
10
|
+
here depends on the [RobotLab](https://github.com/MadBomber/robot_lab)
|
|
11
|
+
framework.
|
|
12
|
+
|
|
13
|
+
**Documentation: [madbomber.github.io/robot_lab-am](https://madbomber.github.io/robot_lab-am)**
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
Add to your Gemfile:
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
gem "robot_lab-am"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Dependencies are just `ruby_llm` and `myway_config` — no robot framework
|
|
24
|
+
required.
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
am snapshot # one-shot: collect activity, infer the goal, write
|
|
30
|
+
# .robot_lab_am/current_intent.md in the repo
|
|
31
|
+
|
|
32
|
+
am start # start the continuous daemon for this repo (detached)
|
|
33
|
+
am status # is it running? heartbeat, event count, last inference
|
|
34
|
+
am stop # stop it
|
|
35
|
+
|
|
36
|
+
am install # write a launchd agent so the daemon runs at login
|
|
37
|
+
am uninstall # remove the launchd agent
|
|
38
|
+
|
|
39
|
+
# All commands accept --repo PATH (default: current directory).
|
|
40
|
+
# `am start` also accepts --foreground, --interval N (poll seconds, default 15),
|
|
41
|
+
# and --debounce N (minimum seconds between inference runs, default 300).
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The daemon polls three signal sources — git commits and uncommitted changes,
|
|
45
|
+
your own messages in Claude Code session transcripts, and terminal commands run
|
|
46
|
+
inside the repo — appends new (redacted, deduplicated) events to
|
|
47
|
+
`.robot_lab_am/events.jsonl`, and re-infers `.robot_lab_am/current_intent.md`
|
|
48
|
+
on a debounced cadence whenever activity arrives.
|
|
49
|
+
|
|
50
|
+
### Prerequisites
|
|
51
|
+
|
|
52
|
+
- **Local LLM**: inference defaults to LM Studio's OpenAI-compatible server on
|
|
53
|
+
`localhost:1234` (`lms server start`) so your activity log never leaves the
|
|
54
|
+
machine. Any RubyLLM provider/model can be passed to `Inferrer` instead.
|
|
55
|
+
- **Terminal capture** (optional): commands are read from
|
|
56
|
+
`~/.activity_monitor/terminal_activity.log`, written by a bash `preexec` hook
|
|
57
|
+
(`~/.bashrc__activity_monitor` in your dotfiles). Without the hook, git and
|
|
58
|
+
Claude Code signals still work.
|
|
59
|
+
|
|
60
|
+
## Configuration
|
|
61
|
+
|
|
62
|
+
Settings cascade via [myway_config](https://github.com/MadBomber/myway_config),
|
|
63
|
+
lowest to highest precedence:
|
|
64
|
+
|
|
65
|
+
1. Bundled defaults
|
|
66
|
+
2. User config file — `~/.config/robot_lab_am/robot_lab_am.yml` (flat keys)
|
|
67
|
+
3. `RLAM_*` environment variables
|
|
68
|
+
4. CLI flags (`--interval`, `--debounce`)
|
|
69
|
+
|
|
70
|
+
### Environment variables
|
|
71
|
+
|
|
72
|
+
| Variable | Default | Meaning |
|
|
73
|
+
|----------|---------|---------|
|
|
74
|
+
| `RLAM_PROVIDER` | `openai` | RubyLLM provider used for inference |
|
|
75
|
+
| `RLAM_MODEL` | `qwen/qwen3.8-27b` | Model name |
|
|
76
|
+
| `RLAM_API_BASE` | `http://localhost:1234/v1` | OpenAI-compatible server URL (applied to the `openai` provider) |
|
|
77
|
+
| `RLAM_API_KEY` | *(none)* | API key; unset falls back to the provider's own env var, then a placeholder (fine for LM Studio) |
|
|
78
|
+
| `RLAM_INTERVAL` | `15` | Seconds between daemon watcher polls |
|
|
79
|
+
| `RLAM_DEBOUNCE` | `300` | Minimum seconds between inference runs |
|
|
80
|
+
| `RLAM_INFERENCE_WINDOW` | `100` | Most recent events sent to the model |
|
|
81
|
+
| `RLAM_TERMINAL_LOG` | `~/.activity_monitor/terminal_activity.log` | Preexec command log path |
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
# point inference at a different local model/server
|
|
85
|
+
export RLAM_PROVIDER=openai
|
|
86
|
+
export RLAM_MODEL=llama-3.3-70b
|
|
87
|
+
export RLAM_API_BASE=http://localhost:8080/v1
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Notes:
|
|
91
|
+
|
|
92
|
+
- `am --help` prints the *currently effective* interval/debounce, so you can
|
|
93
|
+
see what your config file and environment resolve to.
|
|
94
|
+
- API keys cascade: `RLAM_API_KEY` if set, else the chosen provider's
|
|
95
|
+
conventional env var (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, …), else a
|
|
96
|
+
placeholder — which is all a local LM Studio server needs. The default
|
|
97
|
+
local setup requires no key; your activity log never leaves the machine.
|
|
98
|
+
- Inference runs in a scoped RubyLLM context, so embedding robot_lab-am in a
|
|
99
|
+
larger app never mutates that app's global RubyLLM configuration.
|
|
100
|
+
|
|
101
|
+
## Development
|
|
102
|
+
|
|
103
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then run
|
|
104
|
+
`bundle exec rake test` to run the tests. `bin/console` gives an interactive
|
|
105
|
+
prompt with the gem loaded.
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
bundle exec rake test # all tests
|
|
109
|
+
asgard quality # all quality gates (tests + coverage, rubocop,
|
|
110
|
+
# flog, flay, reek, fasterer, typos, ...)
|
|
111
|
+
asgard doc_builder # build the MkDocs site
|
|
112
|
+
asgard doc_server # serve docs locally
|
|
113
|
+
bin/console # IRB shell with gem loaded
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## License
|
|
117
|
+
|
|
118
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Quality gates (quality, rubocop_check, flog_check, flay_check, ...),
|
|
4
|
+
# documentation tasks (doc_builder, doc_server), and the gem lifecycle
|
|
5
|
+
# (build, install, release) live in asgard — see .loki and the shared
|
|
6
|
+
# dev/*.loki files it imports. This Rakefile keeps only the task asgard
|
|
7
|
+
# itself delegates to: the test suite.
|
|
8
|
+
|
|
9
|
+
require 'rake/testtask'
|
|
10
|
+
|
|
11
|
+
Rake::TestTask.new(:test) do |t|
|
|
12
|
+
t.libs << 'test'
|
|
13
|
+
t.libs << 'lib'
|
|
14
|
+
t.test_files = FileList['test/**/*_test.rb', 'test/**/test_*.rb'].exclude('**/*_helper.rb')
|
|
15
|
+
t.verbose = true
|
|
16
|
+
t.ruby_opts << '-rtest_helper'
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
task default: :test
|
|
20
|
+
|
|
21
|
+
desc 'Run tests with verbose output'
|
|
22
|
+
task :test_verbose do
|
|
23
|
+
ENV['TESTOPTS'] = '--verbose'
|
|
24
|
+
Rake::Task[:test].invoke
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
desc 'Run a single test file'
|
|
28
|
+
task :test_file, [:file] do |_t, args|
|
|
29
|
+
ruby "test/#{args[:file]}"
|
|
30
|
+
end
|
data/bin/am
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Activate bundler if running from the gem's own dev environment.
|
|
5
|
+
if File.exist?(File.expand_path("../Gemfile", __dir__))
|
|
6
|
+
require "bundler/setup"
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
require "robot_lab/am"
|
|
10
|
+
|
|
11
|
+
Signal.trap("INT") { exit 130 }
|
|
12
|
+
|
|
13
|
+
RobotLab::Am::CLI.run(ARGV)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 700" font-family="Helvetica, Arial, sans-serif" role="img" aria-label="robot_lab-am pipeline">
|
|
2
|
+
<defs>
|
|
3
|
+
<marker id="a" markerWidth="9" markerHeight="9" refX="7" refY="3" orient="auto" markerUnits="strokeWidth">
|
|
4
|
+
<path d="M0,0 L7,3 L0,6 Z" fill="#94a3b8"/>
|
|
5
|
+
</marker>
|
|
6
|
+
</defs>
|
|
7
|
+
<style>
|
|
8
|
+
.t { fill:#ffffff; font-size:13px; font-weight:600; }
|
|
9
|
+
.s { fill:#e2e8f0; font-size:10px; }
|
|
10
|
+
.hd { fill:#cbd5e1; font-size:12px; font-weight:700; letter-spacing:.5px; }
|
|
11
|
+
.lg { fill:#cbd5e1; font-size:11px; }
|
|
12
|
+
</style>
|
|
13
|
+
|
|
14
|
+
<!-- signal sources -->
|
|
15
|
+
<text x="40" y="14" class="hd">SIGNAL SOURCES (human activity)</text>
|
|
16
|
+
<rect x="40" y="24" width="250" height="46" rx="10" fill="#64748b"/>
|
|
17
|
+
<text x="165" y="45" text-anchor="middle" class="t">Git repo</text>
|
|
18
|
+
<text x="165" y="61" text-anchor="middle" class="s">commits · uncommitted changes</text>
|
|
19
|
+
|
|
20
|
+
<rect x="325" y="24" width="250" height="46" rx="10" fill="#64748b"/>
|
|
21
|
+
<text x="450" y="45" text-anchor="middle" class="t">Claude Code transcripts</text>
|
|
22
|
+
<text x="450" y="61" text-anchor="middle" class="s">~/.claude/projects/<slug>/*.jsonl</text>
|
|
23
|
+
|
|
24
|
+
<rect x="610" y="24" width="250" height="46" rx="10" fill="#64748b"/>
|
|
25
|
+
<text x="735" y="45" text-anchor="middle" class="t">Terminal preexec log</text>
|
|
26
|
+
<text x="735" y="61" text-anchor="middle" class="s">~/.activity_monitor/terminal_activity.log</text>
|
|
27
|
+
|
|
28
|
+
<line x1="165" y1="70" x2="165" y2="98" stroke="#94a3b8" stroke-width="2" marker-end="url(#a)"/>
|
|
29
|
+
<line x1="450" y1="70" x2="450" y2="98" stroke="#94a3b8" stroke-width="2" marker-end="url(#a)"/>
|
|
30
|
+
<line x1="735" y1="70" x2="735" y2="98" stroke="#94a3b8" stroke-width="2" marker-end="url(#a)"/>
|
|
31
|
+
|
|
32
|
+
<!-- watchers -->
|
|
33
|
+
<rect x="40" y="100" width="250" height="46" rx="10" fill="#0891b2"/>
|
|
34
|
+
<text x="165" y="121" text-anchor="middle" class="t">GitWatcher</text>
|
|
35
|
+
<text x="165" y="137" text-anchor="middle" class="s">recent commits + WIP (own state dir filtered)</text>
|
|
36
|
+
|
|
37
|
+
<rect x="325" y="100" width="250" height="46" rx="10" fill="#0891b2"/>
|
|
38
|
+
<text x="450" y="121" text-anchor="middle" class="t">ClaudeWatcher</text>
|
|
39
|
+
<text x="450" y="137" text-anchor="middle" class="s">your own plain-language messages</text>
|
|
40
|
+
|
|
41
|
+
<rect x="610" y="100" width="250" height="46" rx="10" fill="#0891b2"/>
|
|
42
|
+
<text x="735" y="121" text-anchor="middle" class="t">TerminalWatcher</text>
|
|
43
|
+
<text x="735" y="137" text-anchor="middle" class="s">only commands with cwd inside the repo</text>
|
|
44
|
+
|
|
45
|
+
<line x1="165" y1="146" x2="435" y2="196" stroke="#94a3b8" stroke-width="2" marker-end="url(#a)"/>
|
|
46
|
+
<line x1="450" y1="146" x2="450" y2="196" stroke="#94a3b8" stroke-width="2" marker-end="url(#a)"/>
|
|
47
|
+
<line x1="735" y1="146" x2="465" y2="196" stroke="#94a3b8" stroke-width="2" marker-end="url(#a)"/>
|
|
48
|
+
|
|
49
|
+
<!-- collector -->
|
|
50
|
+
<rect x="300" y="198" width="300" height="52" rx="10" fill="#f59e0b"/>
|
|
51
|
+
<text x="450" y="220" text-anchor="middle" class="t">Collector</text>
|
|
52
|
+
<text x="450" y="237" text-anchor="middle" class="s">Redactor masks secrets · fingerprint dedupe</text>
|
|
53
|
+
<line x1="450" y1="250" x2="450" y2="278" stroke="#94a3b8" stroke-width="2" marker-end="url(#a)"/>
|
|
54
|
+
|
|
55
|
+
<!-- event store -->
|
|
56
|
+
<rect x="325" y="280" width="250" height="46" rx="10" fill="#475569"/>
|
|
57
|
+
<text x="450" y="301" text-anchor="middle" class="t">.robot_lab_am/events.jsonl</text>
|
|
58
|
+
<text x="450" y="317" text-anchor="middle" class="s">append-only event log, one JSON line per event</text>
|
|
59
|
+
<line x1="450" y1="326" x2="450" y2="354" stroke="#94a3b8" stroke-width="2" marker-end="url(#a)"/>
|
|
60
|
+
|
|
61
|
+
<!-- inferrer + local LLM -->
|
|
62
|
+
<rect x="325" y="356" width="250" height="52" rx="10" fill="#a855f7"/>
|
|
63
|
+
<text x="450" y="378" text-anchor="middle" class="t">Inferrer</text>
|
|
64
|
+
<text x="450" y="395" text-anchor="middle" class="s">bounded recent window (last 100 events)</text>
|
|
65
|
+
|
|
66
|
+
<rect x="640" y="356" width="220" height="52" rx="10" fill="#10b981"/>
|
|
67
|
+
<text x="750" y="378" text-anchor="middle" class="t">Local LLM</text>
|
|
68
|
+
<text x="750" y="395" text-anchor="middle" class="s">LM Studio · localhost:1234 · no API key</text>
|
|
69
|
+
<line x1="575" y1="376" x2="638" y2="376" stroke="#94a3b8" stroke-width="2" marker-end="url(#a)"/>
|
|
70
|
+
<line x1="638" y1="392" x2="577" y2="392" stroke="#94a3b8" stroke-width="2" marker-end="url(#a)"/>
|
|
71
|
+
|
|
72
|
+
<line x1="450" y1="408" x2="450" y2="436" stroke="#94a3b8" stroke-width="2" marker-end="url(#a)"/>
|
|
73
|
+
|
|
74
|
+
<!-- intent artifact -->
|
|
75
|
+
<rect x="325" y="438" width="250" height="52" rx="10" fill="#6366f1"/>
|
|
76
|
+
<text x="450" y="460" text-anchor="middle" class="t">.robot_lab_am/current_intent.md</text>
|
|
77
|
+
<text x="450" y="477" text-anchor="middle" class="s">goal · confidence · evidence · open questions</text>
|
|
78
|
+
<line x1="450" y1="490" x2="450" y2="518" stroke="#94a3b8" stroke-width="2" marker-end="url(#a)"/>
|
|
79
|
+
|
|
80
|
+
<!-- consumer -->
|
|
81
|
+
<rect x="325" y="520" width="250" height="46" rx="10" fill="#0ea5e9"/>
|
|
82
|
+
<text x="450" y="541" text-anchor="middle" class="t">robot_lab-to</text>
|
|
83
|
+
<text x="450" y="557" text-anchor="middle" class="s">seeds a takeover run with real context</text>
|
|
84
|
+
|
|
85
|
+
<!-- daemon loop -->
|
|
86
|
+
<rect x="40" y="280" width="220" height="128" rx="10" fill="none" stroke="#22d3ee" stroke-width="2" stroke-dasharray="6 4"/>
|
|
87
|
+
<text x="150" y="304" text-anchor="middle" class="t">Daemon (am start)</text>
|
|
88
|
+
<text x="150" y="326" text-anchor="middle" class="lg">polls watchers every 15s</text>
|
|
89
|
+
<text x="150" y="344" text-anchor="middle" class="lg">debounced inference (≥300s apart)</text>
|
|
90
|
+
<text x="150" y="362" text-anchor="middle" class="lg">heartbeat.json every tick</text>
|
|
91
|
+
<text x="150" y="380" text-anchor="middle" class="lg">pid file · SIGTERM stop</text>
|
|
92
|
+
<text x="150" y="398" text-anchor="middle" class="lg">supervised by launchd (am install)</text>
|
|
93
|
+
<line x1="260" y1="344" x2="298" y2="240" stroke="#22d3ee" stroke-width="2" stroke-dasharray="6 4" marker-end="url(#a)"/>
|
|
94
|
+
<line x1="260" y1="382" x2="323" y2="382" stroke="#22d3ee" stroke-width="2" stroke-dasharray="6 4" marker-end="url(#a)"/>
|
|
95
|
+
|
|
96
|
+
<!-- legend -->
|
|
97
|
+
<text x="40" y="620" class="hd">ONE-SHOT PATH</text>
|
|
98
|
+
<text x="40" y="640" class="lg">am snapshot runs the same pipeline once — collect → infer → write — with no daemon.</text>
|
|
99
|
+
<text x="40" y="660" class="lg">Everything runs on your machine: the activity log is only ever summarized by a local model.</text>
|
|
100
|
+
</svg>
|