Search / 6 matching notes

Notes &
other things.

2026

My agentic workflow

I plan to put all this packages into a separate repo. For now they are linked in this post directly, use as (require 'name).

My agentic workflow

I plan to put all this packages into a separate repo. For now they are linked in this post directly, use as (require 'name).

As of October 2026 I no longer write code by hand. My flow is to steer AI agents from and within Emacs to let them autonomously work on projects. As the moment of writing, my Emacs steers 25 sessions of Claude Code (95% sessions), OpenCode (running Astra) and Pi (running Astra) over two Mac machines.

Sessions are not sticky and I'm trying to start every week fresh. They store their progress in Org Mode notes (via sexps using Org API, not manual file editing).

My setup: I mostly use two Macs - (i) Mac Mini - always on homeserver (ii) MacBook Pro - working machine that I carry with me. Syncthing syncs work files between them and iCloud for personal documents.

I use several orthogonal ideas.

Vulpea (Org Roam) for Notes

I've been using Org Roam for 5 years now. I always kept notes in ~/Notes and ~/WorkNotes and had .dir-locals to support multiple root directories. Then I found d12frosted/vulpea which natively supports multiple top-level directories, and it clicked - now I can keep my (human-written) notes separately from AI-written notes, but they link between each other via IDs.

I also often move/rename files and having stable ID is a must for me; so Denote doesn't fit my workflow (I keep denotef.el btw to rename auxiliary files).

select 'notes' tbl, count(*) from notes union all
select 'files', count(*) from files union all
select 'links', count(*) from links union all
select 'properties', count(*) from properties;
tbl count(*)
notes 3324
files 2446
links 39654
properties 5248
Emacs runs ghostel that runs agent that calls Emacs

Emacs runs ghostel that runs CLI agent that calls Emacs via emacsclient. AI is good at Elisp and it can execute arbitrary Elisp code in the running Emacs instance to change in while it running.

Excerpt from CLAUDE.md
* Emacs
Emacs configuration directory is ~/.config/emacs.  I use self-built
emacs-mac binary with native compilation support, not emacs-plus.

Whenever you see inline sexp, it's an Emacs elisp expressions.  Use
=emacsclient-eval= to evaluate elisp expressions, passing via heredoc
syntax or stdin to avoid quote escaping.  The result will be either
direct output or a file name (for long output).  Do NOT use raw
=emacsclient=.  Do NOT use HotterEmacs unless explicitly asked -
HotterEmacs is a remote Emacs instance.

#+begin_example sh
emacsclient-eval <<'ELISP'
(+ 1 2)
ELISP
#+end_example

Never update user notes - ~/Notes and ~/WorkNotes directories - on your
own unless explicitly asked by the user.

Org Mode links like =id:<id>= or =ID:<id>= are Vulpea notes.  If you need
to get note details use (vulpea-ux-get-note "<id>"), that returns
plist with keys :body - raw text, and :note - (describe-symbol 'vulpea-note)
struct.

When prompt contains (◊doc 'TERM), evaluate it and treat its output as
public API documentation.  Learn documented functions and variables for
subsequent requests.  Do not reproduce, summarize, or acknowledge
output unless explicitly asked.  Use documented APIs directly; do not
load a skill when documentation is sufficient.

When prompt contains a TODO org heading (statuses TODO or DOING), it
means you are asked to do / continue the task.  If multiple TODO
headings - use subagents to execute them in parallel.  If heading
contains a tag matching agent name - use that agent.

If text contains an elisp sexp with non-standard function - it's a
helper, usually name is self-explainatory.  If in doubt, what sexp
does, use ◊doc helper, example: (◊doc '◊grep).

Whenever you bring anything for the user, it MUST be in the form of
sexp that will bring the user there directly.  Most commonly used are
◊ff and ◊github-status, check (◊doc 'lozenge) for others.

Ghostel runs agent directly, not start ghostel, cd into dir, then run claude but {M-x agent-start-claude-code}1.

Ghostel APIs - namely ghostel-send-(key|string) - give agents capability to talk to each other as if it was user typing into their terminal. To read what another session is doing it's reading the buffer string.

Tip: at any session I can mention the buffer by name and ask Claude to do something about it, like "Figure out *Warnings* buffer and fix them in my config".

Gotcha 2: when Emacs hangs I open claude in Terminal and say "why Emacs hung, fix it" or "Emacs is slow, fix it". I know about "kill -USR2", but Claude is doing it much better, often sending direct syscalls to alter the running process.

emacsclient-eval wrapper

emacsclient --eval works quite well unless output is big, then it starts chunking. To prevent that I wrote emacsclient-eval wrapper. Initially it was just always sending output into temporary file from within emacs, then at the shell counted the size, and if it's 8KB or less then cat it, otherwise return the full file name.

But then I realised that it's a good place to add save-excursion so that Claude can run file-based sexps or open windows or basically any sexp and my current point and windows stay intact.

Then I needed to inject the Claude process id into the sexp (more on that later), and emacsclient-eval also helped.

It's also a good place to add filtering, hooks, Jev for security assessment etc.

Start agents with agent.el

agent.el is a small package wrapping agent launchers that I use for adhoc sessions.

They exist to quickly launch an agent without first jumping into ghostel, then to shell and manually type. My drivers are {M-x agent-start-claude-code}, {M-x agent-start-pi}, {M-x agent-start-opencode}.

Also {M-x agent-copy-path-spec} - very handy - select a region and copy path spec for the agent to pinpoint the region to read.

Don't write skills, give API with (◊doc 'feature)

Enter lozenge.el. Another idea is to instead of writing skills, give agent a public API it can read and call. I have a plenty of packages for services, and ◊doc macro that returns feature doc strings so Agent reads it and calls it.

Instead of writing a skill to work with Slack, I say "read (◊doc 'slack)"; or (◊doc 'datadog) or (◊doc 'snowflake). My butler (later) skill is literally "You are the butler, read (◊doc 'butler) and catch up".

◊doc also works for ordinary functions.

output of (◊doc 'lozenge)
"== Entry point ==

Command: (◊insert-sexp)
Insert ◊ sexp and complete the command that follows it.

Command: (◊insert-lozenge)
Insert ◊ symbol.

Variable: ◊eval-look-back-lines
How many lines above point ◊eval scans for the start of a sexp.

Command: (◊eval &optional replace)
Evaluate the outermost sexp around point and echo its value.
With REPLACE (C-u), replace the sexp with its value instead,
as ‘◊eval-and-replace’ does.

Works in any major mode, so a sexp written in a note runs where it
is written.

Command: (◊eval-and-replace)
Evaluate the outermost sexp around point and replace it with its value.

A string value goes in as its text, any other value as Lisp would
print it.  A sexp run for its effect gives no output - nil, or an
object that does not print readably, like the process ‘browse-url’
starts - so it stays, as it does when evaluation fails.

== Documentation ==

Command: (◊doc thing)
Return documentation for THING.
When THING is a loadable feature (something ‘require’ can find), this
returns its public functions, variables, and struct types, sectioned
by the ‘;;;’ headings of the file that defines them - see
‘◊--doc-feature’.  Otherwise THING must already be a bound function,
variable, or ‘cl-defstruct’ type, and its own documentation is
returned alone - see ‘◊--doc-symbol’.

Output is plain text: signature or variable/struct name, then its
doc-string.

Interactively, prompt for THING, offering the symbol at point, and
browse it shortdoc-style in a *doc-THING* buffer: one entry per
symbol with a button to its full description, ‘n’ and ‘p’ between
entries, ‘N’ and ‘P’ between sections.

== Buffers ==

Command: (◊copy-buffer-name)
Copy the current buffer name to the kill ring.

Function: (◊ghostel-buffer session-name)
Switch to the ghostel buffer running the SESSION-NAME session.

== The web ==

Command: (◊github-search term &rest --cl-rest--)
Search GitHub code for TERM, narrowed to ORG and REPO.

Command: (◊github-repo repo &rest --cl-rest--)
Open REPO on GitHub.
REPO may be a bare name or ORG/REPO; a bare name is qualified
with ORG.

Command: (◊shortcut-story story)
Open the Shortcut STORY, given as an id, sc-id or [id].

Command: (◊shortcut-epic epic)
Open the Shortcut EPIC, given as an id, sc-id or [id].

Command: (◊duckduckgo term)
Search DuckDuckGo for TERM.

Command: (◊google term)
Search Google for TERM.

Function: (◊curl url &rest rest)
Request URL with ‘plz-see’, adding auth headers for the host.
METHOD defaults to ‘get’.  Headers passed explicitly win over the
auth-source ones.  NAME names the response buffer, replacing any
buffer of that name; it then takes only AS ‘buffer’, and THEN and
ELSE receive the renamed buffer.

== Git ==

Command: (◊show-commit repo ref)
Show commit REF of REPO in Magit.

Command: (◊git-status repo)
Show Magit status of REPO, a directory or a repository name symbol.

== Searching ==

Command: (◊grep repo term)
Grep REPO for the string TERM with ‘deadgrep’.

REPO is repository name (symbol or string), searched across
‘◊repo-roots’, not a full path.  TERM is a string to search for.

Command: (◊grep-re repo term)
Grep REPO for the regexp TERM with ‘deadgrep’.

REPO is repository name (symbol or string), searched across
‘◊repo-roots’, not a full path.  TERM is a string to search for.

Command: (◊grep-notes term)
Grep the work notes for the string TERM.

Function: (◊ack-rx expr &optional directory)
Run ‘ack’ in DIRECTORY for EXPR, an ‘rx’ form.

== Files ==

Command: (◊find-file repo pattern)
Open the first file matching PATTERN in REPO.
PATTERN may carry a trailing :LINE or :LINE:COL, and point lands
there.  REPO is looked up across ‘◊repo-roots’.

Function: (◊read repo pattern)
Return the contents of the first file matching PATTERN in REPO.
REPO is looked up across ‘◊repo-roots’; nil means PATTERN is a path.
A trailing :LINE reads from that one-based line through end of file.
An optional :COL after :LINE is ignored.  Read from disk without
visiting the file.

Command: (◊elpa-find-file pattern)
Open the file matching PATTERN in the elpa directory.
PATTERN may carry a trailing :LINE or :LINE:COL.  Error when two
versions of the same package match - the stale one has to go.

Command: (◊recover-all-files &optional dir)
Run ‘recover-file’ for every file in DIR that has auto-save data.
DIR defaults to ‘default-directory’.  Candidates are found from
their auto-save files (#name#); ‘recover-file’ prompts per file.

== Other ==

Variable: plz-see-base-url
Prefix to add to the URL argument of ‘plz-see’, if relative.
Relative here means, paradoxically, that the URL in question
starts with \"/\".

Variable: ◊repo-roots
Alist of roots under which a repository is looked up by name.
Each value is a directory holding repository subdirectories; the
first root containing the repository wins.

Variable: ◊doc-section-order
How ‘◊doc’ orders a feature’s sections.
‘reverse’ puts the last section of the source file first: a Lisp file
defines primitives before the things that use them, so reading it
backwards leads with what a caller wants.  ‘source’ keeps the file’s
order.  Either way an explicit order set with (put FEATURE
=’◊doc-sections =’(SECTION ...)) is honoured first, and Other
comes last.

Variable: ◊github-org
GitHub organisation assumed when a repository is named bare.

"

Lozenge is this symbol - ◊. I saw it in the Pollen publishing system by Matthew Butterick. My lozenge.el has nothing to do with Pollen except all symbols there start with ◊. This gives me a separate namespace for functions that (i) guaranteed to having no symbol collissions (ii) looks like some strange markup that I can copy-paste directly into Slack, like (◊github-search "MakePayment"). I have {C-c l l} to insert (◊) and open completing-read with the symbols, and {C-c l i} to insert ◊ symbol.

I force Claude to write using sexps, so that I can put point on them and execute with ◊eval {C-c l e}.

Here's the example output of a session, {C-c C-t} then put point inside sexp then {C-c l e}.

The Migrate Batch Jobs to k8s session is open. Squad-Projects.org
didn't change, since the heading already had a monkey-id.

(switch-to-buffer "*claude: migrate-batch-jobs-to-k8s*")

Another handy examples - you can figure out what they do:

  • (◊git-status "~/.wt/proxy_remove-ecs-iac")
  • (◊ff 'metadata-registry "Export/v1/schema.json")
  • (◊github-search "cross_account_invoke_lambda")
  • (◊grep 'repo-name "cio.trackEvent")
  • (◊duckduckgo "github actions changed files")
monkey.el - flowchart-based execution

TL;DR: most typing into the claude terminal is literally this:

start monkey worker (buffer-local monkey-session-id carries ID and then the skill figures everything by looking at backlinks)

adopt monkey session for project (it derives the project from the current session name)

continue on the next :todo from monkey-task-context

monkey repo is private as it contains work details, if anybody interested I'll open it.

I'm running sessions that span weeks. No, they don't work 24/7 nor I'm token-maxxing. Session keeps track of a project, a significant feature, operational work for the week etc.

monkey.el is to:

  1. Keep user notes (Org Mode heading, TASK in monkey.el terminology) - in ~/Notes or ~/WorkNotes - separate from agent notes (Org Mode file in ~/Sync/Monkey, NOTE). Agent only writes to NOTE and reads from TASK.
  2. Add the place for Agent to break down work into ITEMS (Org Mode headings with their TODO status). It's like Plan mode, but I stopped it in favour of "here's TODOs, go do them and write results here".
  3. Ensure execution follows the flowchart. The flowchart itself uses monkey notation. I used graphviz, but switched to reladraw (my fork with diamond shapes).
  4. Ensure that agent writes to Org using sexps, adds metadata so I can quickly check what's going on.

NOTE is a separate Org file, lives under ~/Sync/Monkey that are under vulpea-db-sync-directories.

monkey.el is accompanied with Monkey-Worker.org skill that explains writing preferences, teaches notation, instructs with using flowcharts.

Monkey predates lozenge.el so I still didn't move the skill to "read (◊doc 'monkey) and act".

Example of flowchart - agent reads source code, I look at the graph representation. Every node has immutable code encoding node type and flowchart itself (C26x - Change Handling, x - terminal).

change handling flowchart
style decision  shape: diamond  fill: theme-secondary-subtle  border: theme-secondary
style terminal  shape: pill  fill: theme-primary-subtle  border: theme-primary
style wait      border: #e0a030
style key       text: (size: small, color: theme-muted)

default edge  text: (size: small)

node C26x "C26x ITEM needs / a code change"  style: terminal
node C1  "C1 (monkey-get-worktree \"<repo>\" \"<branch>\") / -> (:type TYPE :dir DIR :meta META :tags TAGS) / (monkey-note-add-item ID \"DOING\" TITLE :stage \"C1\" / ​  :meta META :tags TAGS)"  below C26x
node C2  "C2 implement the change inside DIR"  below C1
node C24d "C24d MODE afk?"  below C2  style: decision
node C3  "C3 user gate / (monkey-item-edit-body ID TITLE \"<what to look at>\") / (monkey-item-edit-heading ID TITLE / ​  :status \"TODO\" :add-tags '(\"@user\") :stage \"C3\")"  below-left of C24d  style: wait  shape: half-pill
node C25 "C25 self-review: tests, lint, \/code-review / (monkey-item-edit-body ID TITLE \"<what was checked>\")"  below-right of C24d
node C4d "C4d user's verdict / (@user tag, LOGBOOK)"  below C3  style: decision and wait
node C5  "C5 implement the requested changes"  above-left of C4d
node C22d "C22d asked to commit? / (:git route)"  below C4d (gap: wide)  style: decision
node C23x "C23x leave DIR uncommitted / (monkey-item-edit-body ID TITLE \"<diff summary>\") / (monkey-item-edit-heading ID TITLE / ​  :status \"DONE\" :stage \"C23x\")"  below-left of C22d (gap: wide)  style: terminal
node C6  "C6 commit the ITEM's files, push"  below C22d
node C17 "C17 watch the deploy the merge triggers / (pipeline run, terraform apply, image rollout)"  below C16d (gap: wide)  style: wait  shape: half-pill
node C18d "C18d deployed and healthy?"  below C17  style: decision
node C19 "C19 fix, or revert / a change like any other"  left of C18d (gap: wide)
node C20 "C20 (monkey-note-add-item ID \"TODO\" / \"Verify <effect> after <when>\") / for every effect that shows up later"  below C18d
node C21x "C21x (monkey-item-edit-body / ID TITLE \"<what you observed>\") / (monkey-item-edit-heading / ID TITLE :status \"DONE\" :stage \"C21x\")"  below C20  style: terminal

node C7  "C7 commit, open PR / (problem + approach, no code prose)"  below C25 (gap: wide)
node C8  "C8 (monkey-item-meta-set ID TITLE \"PR\" \"\[\[<url>]\[<title> ⋅ <repo>#NNN]]\") / (monkey-item-edit-heading ID TITLE :status \"DOING\" :stage \"C8\")"  below C7
node C9d "C9d CI green?"  below C8  style: decision and wait
node C10 "C10 fix, push to the PR branch"  right of C9d (gap: wide)
node C11 "C11 request review from the team in Github"  below C9d
node C12d "C12d review comments?"  below C11  style: decision and wait
node C13d "C13d need the user? (rare)"  right of C12d (gap: wide)  style: decision
node C15 "C15 push the fix, reply to the comment"  right of C13d (gap: wide)
node C14 "C14 (monkey-note-add-item ID \"TODO\" \"<what is needed>\" / :tags '(\"@user\"))"  below C13d (gap: wide)  level with C16d
node C16d "C16d user merged PR?"  below C12d (gap: wide)  style: decision and wait

edge C26x -> C1
edge C1 -> C2
edge C2 -> C24d
edge C24d -> C3 "no"
edge C24d -> C25 "yes"
edge C25 -> C22d ":git route"  from: left  to: right  line: (path: square)
edge C25 -> C7 ":wt route"  from: bottom  to: top
edge C3 -> C4d "later turn"
edge C4d -> C5 "changes requested"  from: left  to: bottom  line: (path: square)
edge C5 -> C3  from: top  to: left  line: (path: square)
edge C4d -> C22d "approved, :git route"
edge C4d -> C7 "approved, :wt route"  from: right  to: left  line: (path: square)
edge C22d -> C6 "yes"
edge C22d -> C23x "no (default)"
edge C6 -> C17  from: bottom  to: left  line: (path: square)
edge C7 -> C8
edge C8 -> C9d
edge C9d -> C10 "errors"
edge C10 -> C9d  from: top  to: top  line: (path: square)
edge C9d -> C11 "green"
edge C11 -> C12d
edge C12d -> C13d "yes"
edge C12d -> C16d "none"  from: bottom  to: top
edge C13d -> C14 "yes"
edge C13d -> C15 "no"
edge C15 -> C12d  from: top  to: top  line: (path: square)
edge C14 -> C16d  from: left  to: right
edge C16d -> C12d "not yet"  from: left  to: left  line: (path: square)
edge C16d -> C17 "merged"  from: bottom  to: top
edge C17 -> C14 "needs a human / (manual apply, / approval, access)"  from: right  to: bottom  line: (path: square)
edge C17 -> C18d
edge C18d -> C19 "no"
edge C19 -> C1  from: left  to: left  line: (path: square)  left of C23x
edge C18d -> C20 "yes"
edge C20 -> C21x

node key_d "decision"  right of C21x (gap: wide)  style: decision
node key_x "terminal"  below key_d (gap: tight)  style: terminal
node key_w "wait node"  below key_x (gap: tight)  style: wait  shape: half-pill
monkey-pr__flow.svg

Using Monkey is like this:

  • Write an Org Mode heading, add metadata (repo, project), run {M-x monkey-jack-in}, then in a session say "start monkey worker".
  • Start a session (I do this from emacs-web, later), rename it after the project with "/rename project", then say "adopt monkey session".

Here's the literal content of the skill, based on ID claude reads the actual Org mode file using vulpea.

---
name: monkey-worker
description: work a Monkey TASK session in dwim fashion - "start" begins the NOTE's ITEMs, "continue" folds new TASK TODOs into the NOTE and resumes, "adopt" turns the current session into a monkey session. Use when the session has a Monkey NOTE ID (buffer monkey-session-id or session name) and the user says "start", "continue", or gives no direction, or when the user says "work here as a monkey".
---

Load the /orgmode skill first - everything written into a NOTE is Org
Mode and follows its constructs.

Read [Monkey Worker skill](id:20260716T113128.723996).
emacs-web.el

Okay, we're close to butler, the one that we're here for.

But before - emacs-web.el - a web UI to manage Emacs. It all started when I went walking acrosss the lake with my son and wanted to have mobile client for OpenCode. I asked Claude (remote) - "build me a web app with listing of Emacs buffers in Notes.app UI style". By the end of the walk I had buffers, pins, killing buffer, rendering (htmlize does this), typing into Claude, and even voice input using Whisper with custom prompt.

I'm using it now all the time, it has (i) two my Macs connecting to it (two Emacs instances); (ii) sharing buffers, so some of the Claude sessions I share with my wife - they manage her Shopify shop; (iii) voice input for memos that go into daily note.

Here's the minimal running config, after putting emacs-web.el into your load-path:

(use-package emacs-web :ensure nil
  :config (emacs-web-start))

if something's off, ask Claude to fix it.

Then you need to bring http://localhost:4678 to Internet, e.g. Cloudflare Tunnels:

cloudflared tunnel --url http://localhost:4678
my config with two machines
(use-package emacs-web :ensure nil
  :config
  (defun v/emacs-web-claude-in-notes ()
    "Start a Claude Code session in the Notes, named after the time."
    (agent-start-claude-code "~/Notes/" (format-time-string "notes-%H%M")))
  (defun v/emacs-web-opencode-in-notes ()
    "Start a OpenCode session in the Notes, named after the time."
    (agent-start-opencode "~/Notes/" (format-time-string "notes-%H%M")))
  (defun v/emacs-web-pi-in-notes ()
    "Start a Pi session in the Notes, named after the time."
    (agent-start-pi "~/Notes/" (format-time-string "notes-%H%M")))
  (defun v/emacs-web-butler-in-notes ()
    "Start the butler in the Notes, named after the time: a Claude Code
session that catches up on the other sessions every 15 minutes."
    (let ((buffer (agent-start-claude-code "~/Notes/" (format-time-string "butler %H%M"))))
      (agent-type buffer '("Load (◊doc 'butler) and act as butler"
                           "/loop 15m catch up"))
      buffer))
  (setq emacs-web-claude-resume-function
        (lambda (dir id name) (agent-start-claude-code dir name id)))
  (defun v/memo-after-send (start)
    "Tidy the text sent from the phone from START: drop the tag dictation
starts it with, fill it to the phone's width, leave a blank line
after it, and save."
    (save-excursion
      (goto-char start)
      (when (looking-at (regexp-quote "[voice input] "))
        (delete-region (match-beginning 0) (match-end 0))))
    (let ((end (point-marker))
          (fill-column (or emacs-web-sent-columns fill-column)))
      (fill-region start end)
      (goto-char end)
      (set-marker end nil))
    (insert "\n")
    (let ((delete-trailing-lines nil))
      (save-buffer)))
  (defun v/emacs-web-new-memo ()
    "Start a Memo heading at the end of today's daily note, point under it,
and save the note.  Dictation sent to it from the phone goes in
without its tag, followed by a blank line, and is saved."
    (vulpea-dailies-goto-today)
    (add-hook 'emacs-web-after-send-functions #'v/memo-after-send nil t)
    (goto-char (point-max))
    (unless (bolp) (insert "\n"))
    (insert (format-time-string "\n* Memo %H:%M\n"))
    (let ((delete-trailing-lines nil))
      (save-buffer))
    (dolist (window (get-buffer-window-list nil nil t))
      (set-window-point window (point)))
    (current-buffer))
  ;;
  (pcase (system-name)
    ("mini"
     (setq emacs-web-title "Mac mini"
           emacs-web-computers '(("Mac mini" . "./") ("M4 Pro" . "pro/"))
           ;; the shared buffers alone, for jozh.nl/pix-n-art
           emacs-web-shared-port 4680)
     (setq emacs-web-list-actions
           '(("New Claude Code session in Notes" . v/emacs-web-claude-in-notes)
             ("New OpenCode session in Notes" . v/emacs-web-opencode-in-notes)
             ("New Pi session in Notes" . v/emacs-web-pi-in-notes)
             ("New Butler" . v/emacs-web-butler-in-notes)
             ("Resume Claude session" . emacs-web-claude-resume-session)
             ("New memo" . v/emacs-web-new-memo))))
    ("m4pro"
     (defun v/emacs-web-claude-in-worknotes ()
       "Start a Claude Code session in the WorkNotes, named after the time."
       (agent-start-claude-code "~/WorkNotes/" (format-time-string "worknotes-%H%M")))
     (setq emacs-web-title "M4 Pro"
           emacs-web-computers '(("Mac mini" . "../") ("M4 Pro" . "./"))
           ;; the Mini's Whisper hears dictation for both
           emacs-web-transcribe-at "../")
     (setq emacs-web-list-actions
           '(("New Claude Code session in Notes" . v/emacs-web-claude-in-notes)
             ("New Claude Code session in WorkNotes" . v/emacs-web-claude-in-worknotes)
             ("New OpenCode session in Notes" . v/emacs-web-opencode-in-notes)
             ("New Pi session in Notes" . v/emacs-web-pi-in-notes)
             ("New Butler" . v/emacs-web-butler-in-notes)
             ("Resume Claude session" . emacs-web-claude-resume-session)
             ("New memo" . v/emacs-web-new-memo)))))
  ;;
  (emacs-web-start))
butler.el

Butler is a Claude session stat steers other ghostel sessions across both of Macs.

Butler started with these instructions:

you're a butler, load (◊doc 'butler) and catch up every 30 minutes

It knows ghostel sessions by listing buffers in Emacs, then goes to each session and types there:

Report briefly: what you delivered in this session, what is left unfinished, and what needs my decision.

After Butler collected status, it decomposes status into asks for the user, giving each ask a sequential ID and places them into the ring, so user (me) always presented with a single action, stating the ID, it's type (D - Decision, B - Blocked, U - User action needed), session name and details.

Pager
github
velppa/claude-pager

Claude, OpenCode and Pi are configured with EDITOR=emacs-pager, so typing {C-g} opens a new Emacs buffer where I can type an answer, press {C-c C-c} to confirm or {C-c C-k} to abort. For Claude, the buffer also contains the session transcript with tool calls converted into Org syntax, tables are arranged etc.

Pager closes the gap of navigating and working with the session transcript as a real buffer. Ghostel' modes are great, but they are still behind ordinary buffer.

Tip: use org-store-link (I bound it at {C-c n L} and then in the prompt buffer org-insert-link, {C-c n I} to insert the pointer for agent to check/act on.

emate.el

Emate is an Elisp implementation of kunchenguid/firstmate that uses Emacs/ghostel instead of tmux/Bash for steering. I also removed all the bluff of "adress user as a Captain".

I used it for a week and then switched back to Monkey Workers + Butler steering.

Auxiliary stuff - reload.el, Monkey Kanban, Stepper

There are some helper packages for my use case - using two Macs.

  • refresh.el - automatically reloads the package when it was changed. Helpful when a session on Mini changes the package and Emacs on MBP picks it up.
  • Monkey Kanban is a Go app that renders Monkey sessions on a Kanban board. Links open Emacs using org-protocol.
  • Stepper is my implementation of AWS StepFunctions - replacement for Claude Routines and Datadog Synthetics. Whenever I want to run AI on schedule, I create a StepFunction.

    Examples:

    • credentials updaters (login into SSO every few hours)
    • proactive service monitoring (runs a skill that checks metrics and logs in Datadog and Kubernetes)
    • handling Datadog Cases (I even had instruction "whenever new case arrived, start a monkey session with the fix" - and observed PRs opened, with humans as bottleneck, so disabled it for now)
    • monitoring YouTube and generate summaries.
    • summaries for changed tickets in the past working day in Shortcut (like Jira).
gptel

I was using gptel heavily before Claude Code became so useful. I still have {C-c j} prefix for gptel actions, but using it less and less these days. Some useful functions that I have:

  • v/gptel-summarize-buffer
  • v/gptel-generate-commit-message
  1. In simplest form agent-start-claude-code is this:
    (defun agent-start-claude (name)
      "Start Claude Code session named NAME in `default-directory'."
      (interactive "sName: ")
      (let ((buf (get-buffer-create (format "*claude: %s*" name))))
        (ghostel-exec buf "claude" (list "--remote-control" "--name" name))
        (switch-to-buffer buf)))
    
    ↩

Daisy' (Anthropic Claude Code engineer) workflow

On I read in stumbled on this post - Anthropic: Please Have Daisy the CC Engineer Do a Video! : r/ClaudeAI.

Daisy' (Anthropic Claude Code engineer) workflow

On <2026-08-23 Sun> I read in stumbled on this post - Anthropic: Please Have Daisy the CC Engineer Do a Video! : r/ClaudeAI.

In this week's newsletter, Anthropic included this:

"My daily driver currently looks like: two lead agents that keep each other accountable and restart the other if either fails. These delegate to tech lead or PM agents for the 8-10 projects I'm running at any one time, and each project has 5-10 IC [individual contributor] agents, generalists or specialists depending on the problem. Across all of these I'm still only doing 30-50 prompts per day, and my IC agents typically work autonomously for 2-3 days. About 60% of my interaction is with the leads, 35% with a project lead, and 5% is when something has gone off the rails. All of these agents communicate directly with the SendMessage tool."

– Daisy, Engineer on Claude Code

Turning it into mindmap:

                                      ╭─ keep each other accountable
                   ╭─ two lead agents ┼─ restart the other if either fails
                   │                  ╰─ delegate to ┬─ tech lead ── 8-10 projects...
                   │                                 ╰─ PM agents
                   │                                                 ╭─ . ┬─ generalists
« Daisy workflow » ┼─ 8-10 projects ── per project ── 5-10 IC agents ┤    ╰─ specialists
                   │                                                 ╰─ work autonomously ── for 2-3 days
                   │                        ╭─ 60% with leads
                   ├─ 30-50 prompts per day ┼─ 35% with project lead
                   │                        ╰─ 5% off rails
                   ╰─ agents communicate ── SendMessage tool

My thoughts:

Why two lead agents? Likely they both run "/loop 30m check projects" with offset, so one is checking status while another is ready for communication. This is where "60% with leads" coming from; sometimes they both are checking, and Daisy goes to project leads directly. Those two leads have permanent session names and never closing.

8-10 projects. I also have about the same number of big topics a week, often carried to other weeks. So this sounds like the right amount of parallelism. Also keeping 8-10 projects on track is within cognitive abilities of an average human.

Beyond the basics with Claude CodeYouTube

Comments lead to this video:

YT
Beyond the basics with Claude Code - YouTube
author
Daisy Hollman
date
<2026-05-22 Fri>

It breaks down to this mindmap:

                                          ╭─ Slack
                 ╭─ where work comes from ┼─ CI/CD ── no point to fix CI/CD manually
                 │                        ├─ Dashboards
                 │                        ╰─ Internal Docs
                 ├─ tip ── try doing ALL work from within CC
                 │         ╭─ code conventions are not in the model
                 ├─ limits ┼─ fine-tuning doesn't work well ── leads to more hallucinations
                 │         ╰─ in-context learning ── ICL ── general AI wins over specific AIs
                 │                                                         ╭─ rigid templates
                 │                     ╭─ compensates lack of intelligence ┼─ guardrails
                 │                     │                                   ├─ "don't let it touch X"
                 ├─ two kinds of tools ┤                                   ╰─ gets less useful as model improves
                 │                     │                           ╭─ more access
                 │                     ╰─ scales with intelligence ┼─ more control
                 │                                                 ├─ faster feedback loops
                 │                                                 ╰─ gets more useful as model improves
                 │                          ╭─ context window stayed at 1M max ── models are way better yoy
                 │                          ├─ can't dump the whole X into context ── X ┬─ wiki
                 ├─ context window is a box ┤                                           ╰─ codebase
                 │                          ├─ example ── try to run npm on Arduino
                 │                          ╰─ don't pay for what you don't use ── [TK: no slop skills]
                 │           ╭─ cached tokens are cheap ── new are expensive
                 │           ├─ not an LRU problem
                 ├─ KV cache ┼─ change in early instructions invalidates the cache ┬─ add new tool
                 │           │                                                     ╰─ change in CLAUDE.md
                 │           ╰─ put stable stuff at the front ── volatile at the end ── CC teams tries to solve this problem
                 ├─ Large-scale Software Engineering ── main question ── Does it scale?
                 │                           ╭─ when it's the right tool?
                 │                           ├─ CC has a shell ── Skill+CLI has less overhead
                 │                           │                                 ╭─ name
                 │                    ╭─ MCP ┼─ tools definitions sit in front ┼─ arguments
                 │                    │      │                                 ╰─ description
« Agentic flow » ┤                    │      │              ╭─ puts just name in the system prompt
                 │                    │      ╰─ tool search ┼─ kind-of lazy-loaded
                 │                    │                     ╰─ needs to be very specific ── e.g. slack
                 │                    │         ╭─ in-context
                 │                    │         ├─ one-line description always loaded
                 │                    │         ├─ full SKILL.md and assets loaded on-demand
                 ├─ Plugin primitives ┼─ Skills ┤                 ╭─ body is pay-per-use ── good
                 │                    │         ╰─ does it scale? ┼─ reliably loading requires a paragraph in description
                 │                    │                           ╰─ not possible to lazy-load sub-skills ┬─ no hierarchy yet
                 │                    │                                                                   ╰─ what if 100k skills?
                 │                    ├─ Hooks ┬─ runs code on events ── shell scripts
                 │                    │        ╰─ zero-overhead abstraction
                 │                    │         ╭─ out-of-context ── fundamental difference to a skill
                 │                    │         │               ╭─ named role
                 │                    │         ├─ more Claudes ┼─ own system prompt
                 │                    ╰─ Agents ┤               ╰─ own tool set
                 │                              ├─ spawned with a task ┬─ returns result
                 │                              │                      ╰─ its transcript doesn't come back
                 │                              ╰─ description still in-context ── what if 100k agents?
                 │                             ╭─ loads fully unconditionally ┬─ doesn't scale
                 │                ╭─ CLAUDE.md ┤                              ╰─ every plugin adds it
                 ├─ Doesn't scale ┤            ╰─ better inject via SessionStart hook
                 │                │         ╭─ low-quality ── low-cost ── useful, different contract
                 │                ╰─ Memory ┼─ model-curated, not human-curated
                 │                          ╰─ plugins are human-authored, human-reviewed
                 │                 ╭─ worktrees ┬─ one repo ── N claudes each owns its branch
                 │                 │            ╰─ better make it long-lived ── less overhead ── [TK: try kunchenguid/treehouse?]
                 ├─ Multi-clauding ┼─ asynchrony ┬─ walk away, let it do the work
                 │                 │             ╰─ /loop 10m
                 │                 ├─ parallelism ── multiple of asynchrony
                 │                 ╰─ lot's of context switching
                 ╰─ Agent-teams ┬─ teammates ── long-lived ── unlike agents
                                ╰─ SendMessage tool ── Claude's talking to each other

Follow-up questions:

SafariTabs.app

We need RSS for sharing abundant vibe-coded apps post by Matt Webb mentions Tabulator app, part of Wall of Apps by Matt Sephton. All the apps there share the same minimalist design…

SafariTabs.app

We need RSS for sharing abundant vibe-coded apps post by Matt Webb mentions Tabulator app, part of Wall of Apps by Matt Sephton. All the apps there share the same minimalist design, native Swift UI, tiny and, supposedly, fast. The downside is that they are paid.

I wanted to try Tabulator, as I use seal_safari_tabs.lua with Hammerspoon. So I built SafariTabs.app (using Claude Code, a project), source code is here – velppa/SafariTabs. Version v0.1.0.

260508--safari-tabs-v0-1-0__app_macos.png

Features:

  • Fuzzy-searching for tabs
  • Keyboard navigation
  • Switching to the selected tab
  • Close tab
  • Headless mode, "safaritabs:" URL scheme

Let's see if I'm going to use it, if yes – will think about publishing a release. If you want to build it – clone the repo and ask Claude Code to build it.

Trying Textpod once again

On <2026-04-13 Mon> I installed Textpod on Mac Book Pro M1 using Cargo (Rust tooling).

Trying Textpod once again

On <2026-04-13 Mon> I installed Textpod on Mac Book Pro M1 using Cargo (Rust tooling).

Good:

  • Separate from notes, and from blog.
  • Managing ton of markdown files for small notes that I produce - huge overkill
  • I want something like this, write in org-mode, press a button (call elisp function) and heading is sent to website.
  • Open source, so I can fork and improve it for my needs.
    • ☑ make a fork - ~/Developer/src/github.com/velppa/textpod
  • Can make it prettier.

Open questions:

  • ☑ Does it have an API? Like I would select a Org text, convert it to Markdown and send to Textpod.
    • use {M-x textpod-org-region-to-note} function
  • ☑ How to increase font? Increased in the sources.
  • ☑ Token access, for hosting on finita-la-comedia
  • ☑ Internal Org Roam links are pasted as markdown files. So in theory, I could somehow expose them.

I ended up forking it to https://github.com/velppa/textpod and greatly improving it for my needs. You're reading notes served by Textpod v0.2.0. It is my project.

Notable changes:

  • Read-only mode with token authentication: The app now supports an optional --token flag that enables authentication. When a token is set, users without the token see a read-only interface where they can only search notes. Access is granted via a ?token=X query parameter (which sets a cookie) or a Bearer token in the Authorization header. The UI dynamically adjusts based on authentication status, hiding edit/delete controls and changing the editor placeholder for unauthenticated users.
  • Enhanced note management: Notes now have unique IDs based on timestamps, enabling direct access via /note/:id URLs. PUT method allows editing notes (upserting if not found).
  • Subpath support: to deploy on your domain under subpath, supply --base-path notes flag. Then notes will be under /notes subpath.
  • The app watches the notes file for external changes and automatically reloads.
  • The link archiving feature lets users prefix URLs with + to both raw urls and Markdown-formatted.
  • Special handling of links to .md files: They appear when exporting from Org-roam. These are internal links and Textpod now turns them into search by filename, effectively returning all mentions of this concept. Poor man zettelkasten import.
  • UI improvement: larger fonts, footer, better code snippets for dark mode, tags on the heading.
  • textpod.el: Companion Emacs package for quick sending notes to Textpod.

Later changes to setup will go into separate notes.

Standalone HTML Galleries

A tiny project on how to tell Claude "generate me an HTML with some pictures and host on my website".

Build HackerNews Legible Frontend

Let's build a legible frontend for HackerNews.