Compare commits

...
29 Commits
Author SHA1 Message Date
dckc 237b470be8 wiring-diagram: who holds what, drawn like MarkM's ERTP slides
A capability wiring diagram as a self-contained HTML/SVG page plus a
headless-Chromium PNG. Every arrow has to point to something in the
code, and a Mallory holding what's likeliest to leak shows what that
doesn't reach. template.html has the drawing helpers, adapted from
capsite's cookie-rotation-wiring.

README: a TODO to consider adapting agoric-sdk's
sequence-diagram-actor-sim as its companion.

Generated-by: Claude Opus 5.5
2026-10-11 14:22:44 -05:00
dckc 9538b4ea0a discord-agent-workspace: set someone up with a Discord-driven Claude Code workspace
Drafted for a friend of Dan's who isn't a developer, so their Claude can
walk them through it.

Generated-by: Claude Opus 5.5
2026-10-11 12:32:34 -05:00
dckc 581e8af605 disclose-gen-ai: the draft caveat is for PRs posted as the human
Generated-by: Claude Opus 5.5
2026-10-10 14:51:18 -05:00
dckc f115f6eda3 disclose-gen-ai: unreviewed agent PRs are drafts with a caveat
Generated-by: Claude Opus 5.5
2026-10-10 14:49:00 -05:00
dckcandClaude Opus 5.5 a4308a5722 hackmd-deck: drop the no-questions rule
It came from one deck's revision, not from how these talks go; step 4
returns to the draft's wording.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 14:09:46 -05:00
dckcandClaude Opus 5.5 6d150c4450 hackmd-deck: 🎉 marks the TADA, not ✅
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 14:08:21 -05:00
dckcandClaude Opus 5.5 2a0815f004 feat(hackmd-deck): add skill
Quick HackMD decks for light talks such as Agoric's engineering show &
tell: a flashback arc (TADA, then how, then challenges), real artifacts
as eye candy, and emoji tags that add up to a retro slide.

In the Portland Pattern Repository form notify-me tried, with an example
skeleton under the h1. The forces come from drafting the Good Fences
deck: the TADA has to be on the talk's own thread; slides show
challenges, they don't pose questions; designed, built in dev, and
deployed must be told apart.

The emoji legend is from the Jun 25 deck; meanings for 🔔 📈 🪄 are
inferred, and 🧱 is new.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 12:17:55 -05:00
dckc 4f6e23fdec feat(obsidian-markdown): add skill
Generated-by: Claude Opus 5.5
2026-10-09 02:12:41 -05:00
dckc c5e06ba156 docs-files-api: headings say something
Generated-by: Claude Opus 5.5
2026-10-09 01:25:47 -05:00
dckc 349c6479e1 docs-files-api: two sentences, then getting started, then features
Generated-by: Claude Opus 5.5
2026-10-09 01:25:13 -05:00
dckc 522950dd6e feat(docs-files-api): README sells and gets to TADA; CONTRIBUTING has the rest
AGENTS.md is a symlink to CONTRIBUTING.md.

Generated-by: Claude Opus 5.5
2026-10-09 01:23:08 -05:00
dckcandClaude Opus 5.5 91e6124c37 feat(notify-me): add skill
Notify at the end of any turn long enough that the operator has
probably looked away, asked or not. opencode-web notifies on every
reply, which is too much; silence let a long turn sit unseen. -t 0
keeps a notice up through a walk (tested on MATE: the default notice
expired; the -t 0 one stayed until closed).

An experiment in form: the body is a Portland Pattern Repository
pattern (context, problem, forces, therefore, resulting context),
after Alexander, rather than the usual rules-and-recipe skill.

The tension: forces give an agent reasons to handle cases the rules
don't anticipate, and make the trade-offs visible to whoever edits
the skill next; but the form runs longer, and every word is paid on
each load, with the actionable part buried under the preamble.

The resolution: an example notify-send right under the h1, the
forces cut to one line each, and the description left in matcher
form. The result is shorter than the rules-style draft (459 words vs.
562). Resulting context: if it reads well in practice, judgment-call
skills may adopt the form; recipe skills (retcon, yarn.lock) likely
gain little from it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 23:19:44 -05:00
dckcandClaude Opus 5.5 abb660e327 rename stop-blathering to grice-maxims
A civil name for the same skill; "stop blathering" stays a trigger
phrase in the description, which now leads with replies: they happen
every turn, so they never look like a task that calls for a skill.
The README gloss now covers all four maxims, not just brevity.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 22:52:56 -05:00
dckcandClaude Opus 5.5 d59efbcd16 feat(ibis-design): add argument placement, numbering, and layout guidance
Lessons from IBISing a2030.md's "Good Fences Make Good Agents":
arguments got pinned to the wrong position, a ruling-out comment was
really a con, and several issues were already answered by the design
text. Numbering made terse replies ("2: ...") unambiguous, and one big
fenced block outgrew a screen.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 21:52:39 -05:00
dckcandClaude Opus 5.5 1ecf625427 docs: add README
What's here, and how to add a skill.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 18:36:03 -05:00
dckcandClaude Opus 5.5 20072d1080 feat(ibis-design): add skill
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 18:36:03 -05:00
dckcandClaude Opus 5.5 dd5db1b32d feat(stop-blathering): add skill
From the "be concise" section of ~/.codex/AGENTS.md (Grice's maxims),
citing Strunk's Elements of Style and Payne's Lively Art of Writing.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 18:35:03 -05:00
dckcandClaude Opus 5.5 d1a9c88b30 fix: make skill frontmatter valid YAML
A ": " inside a plain-scalar description is a parse error; use a
folded block scalar instead.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 18:35:03 -05:00
dckcandClaude Opus 5.5 fe9ad200c4 refactor(no-ambient-authority): rename from ocap-discipline
Name the skill for what triggers it. Cite Wikipedia on capability-based
security and ambient authority, and point newcomers to awesome-ocap.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 18:35:03 -05:00
dckc 65199cafa7 Add what-was-i-doing skill
Reconstruct past activity from an artifact's mtime: day-in-repos.sh
lists one day's commits and reflog entries (pulls, checkouts, clones:
local events git log never shows) across every repo locate knows.

Generated-by: Claude Opus 5.5
2026-10-08 07:21:44 -05:00
dckc ab335b3061 Add run-confined skill
Untrusted installers (curl | bash, unvetted npx packages) run in a
transient systemd user unit: $HOME unreadable, only ~/.local writable.
The script is a copy of office-admin/tools/run-confined.sh; it aborts
rather than run unconfined when AppArmor blocks user namespaces.

Generated-by: Claude Opus 5.5
2026-10-08 07:21:44 -05:00
dckc 5f9527a8f0 docs-files-api: write comments for the reader, not the conversation
Comments say what the code is and why in the reader's terms: no traces
of the design conversation, and nothing the code already says.

Generated-by: Claude Opus 5.5
2026-10-07 09:46:33 -05:00
dckc 018339689b docs-files-api: a script that prints usage keeps its manual in HELP
Refines the program-script exception: when the script prints its usage,
the manual is a HELP string constant under a one-line @file (JS has no
docstrings), not a comment copied into code or re-read from source.

Generated-by: Claude Opus 5.5
2026-10-07 09:44:26 -05:00
dckc da4e709ef4 docs-files-api: program scripts' @file is their manual
The exception: a script run directly (shebang or import.meta.main) puts
its purpose, usage synopsis, arguments, and outputs in @file. Readers
open a script to learn how to run it; its main has no call sites for
JSDoc to show at.

Generated-by: Claude Opus 5.5
2026-10-07 09:37:19 -05:00
dckc 7a9b25538b docs-files-api: API docs are JSDoc, not // comments
What a declaration is or does goes in /** */ on it (IDEs show it at call
sites and on hover); `//` is for the why of a line inside a body. The
description gains the trigger: a `//` comment explaining a declaration.

Generated-by: Claude Opus 5.5
2026-10-07 09:31:04 -05:00
dckc a1e181fd54 Add io-between-turns skill
Event-loop turns run to completion, so I/O on shared, remote, or slow
state belongs between turns (async); synchronous access is fine only for
state local, prompt, and private to the actor, such as a Durable
Object's SQLite. Sourced from Miller, Tribble & Shapiro, "Concurrency
Among Strangers" (TGC 2005), §5.2 and §6 footnote 5.

Generated-by: Claude Opus 5.5
2026-10-07 09:00:22 -05:00
dckc fe3f9dae75 watchme: trim description to triggers; drop dangling references
The description repeated the body's procedure (which file, the losing
diagnosis, the exact winning? replies). Keep only trigger phrases and
artifact names, and add two missed triggers: reading a recording
without the magic words, and how to set one up.

Drop the link to ../lab-notebook-template.md and the mention of a
`watchme` specimen capture; neither exists in this repo.

Generated-by: Claude Opus 5.5
2026-10-07 02:51:47 -05:00
dckc a9750957cd Add silent-failures-are-evil skill
Every failure path leaves a trace in the logs, and no failure is
invented without checking what the API actually returns. Prompted by
diigo-cache's password-manager save reporting "not saved" on success
(CredentialsContainer.store() resolves with undefined).

Generated-by: Claude Opus 5.5
2026-10-07 02:27:12 -05:00
dckc 792e535a56 Polish retcon and yarn-lock-separate-commit for this workspace
Prune references to other kriscendobot/garden skills and roles
(fixer, weaver, judge's panel, boatman/ferry, journal entries,
references/endo-but-for-bots) and per-PR session lore. Replace the
garden's created/updated/author frontmatter with name + a
trigger-first description. Keep the two skills cross-linked, since
both now live here, and credit upstream with links pinned to the
source commits.

retcon: tag pre-retcon tip as a numbered step; verify with a single
empty `git diff pre-retcon HEAD`; drop `git -C project`.
yarn-lock-separate-commit: promote the rebase --skip recipe from
field notes to its own section.

Generated-by: Claude Opus 5.5
2026-10-07 02:40:25 -05:00
22 changed files with 1466 additions and 112 deletions

No files matched your search

+79
View File
@@ -0,0 +1,79 @@
# mad skills
How I like code written, reviewed, and committed — written down so the
bots I pair with can pick it up without me repeating myself.
These are [Agent Skills](https://agentskills.io/specification). Each
`description` front-loads the words that should trigger it — the things
I actually say, like "retcon", "watch me", or "citation needed".
I keep this repo checked out at `.agents/skills/` in a workspace and
symlink `.claude/skills` and `.opencode/skills` to it, so [Claude
Code](https://code.claude.com/docs/en/skills) and
[opencode](https://opencode.ai/docs/skills/) see the same set.
## writing code
- [no-ambient-authority](no-ambient-authority/SKILL.md) — clocks,
RNGs, `fetch`, and the filesystem come in as parameters.
- [io-between-turns](io-between-turns/SKILL.md) — sync I/O is a pointy
instrument.
- [silent-failures-are-evil](silent-failures-are-evil/SKILL.md) — every
failure path leaves a trace in the logs.
- [lazy-tdd](lazy-tdd/SKILL.md) — works the first time? fine. Goofed?
You owe a test.
- [docs-files-api](docs-files-api/SKILL.md) — README sells and gets
you to TADA; CONTRIBUTING has the rest; AGENTS.md links to it.
Exported API docs over file docs. Avoid chit-chat.
## commits and PRs
- [disclose-gen-ai](disclose-gen-ai/SKILL.md) — `Generated-by:`
trailers, per the ASF generative tooling guidance.
- [retcon](retcon/SKILL.md) — regroup a PR's net diff into clean
per-package commits; the merge result doesn't change.
- [yarn-lock-separate-commit](yarn-lock-separate-commit/SKILL.md) —
`yarn.lock` changes ship alone.
## writing prose
- [grice-maxims](grice-maxims/SKILL.md) — say what's needed and no
more, only what you can back up, in an order easy to follow.
- [hypertext-links](hypertext-links/SKILL.md) — link text says where
the link goes; no bare URLs, no "click here".
- [citation-needed](citation-needed/SKILL.md) — back a challenged claim
with live links, quoted and dated.
- [hackmd-deck](hackmd-deck/SKILL.md) — a light talk as a flashback:
TADA, then how, then the challenges, tagged for an emoji retro.
- [ibis-design](ibis-design/SKILL.md) — search a design space:
issues, positions, and arguments.
- [wiring-diagram](wiring-diagram/SKILL.md) — who holds what, drawn
like MarkM's ERTP slides: every arrow cites the code, and Mallory
shows what a leak doesn't reach.
TODO: consider copying or adapting its companion for message order,
sequence-diagram-actor-sim (agoric-sdk `.agents/skills/` on branch
`connolly/ago-1299-agentic-planning-tdd`).
- [obsidian-markdown](obsidian-markdown/SKILL.md) — the workspace is
an Obsidian vault: one line per paragraph, relative links, no file
symlinks you expect Obsidian to show.
## at the terminal
- [discord-agent-workspace](discord-agent-workspace/SKILL.md) — set
someone up like this: Claude Code in a workspace folder, messaged
from Discord.
- [watchme](watchme/SKILL.md) — I record the session with `script -f`;
read the typescript instead of asking me what happened.
- [run-confined](run-confined/SKILL.md) — run an untrusted installer in
a throwaway systemd user unit that can't see `~/.ssh` and friends.
- [what-was-i-doing](what-was-i-doing/SKILL.md) — where did this file
come from? Correlate mtimes with git history across every repo.
- [notify-me](notify-me/SKILL.md) — desktop notification at the end
of a long turn, sticky if I'm stepping away; not every reply.
## adding one
Name the directory for the trigger, not the topic. Write the
`description` for the matcher: what I'd say, what I'd be doing, and
what it's *not* for. Cite the source; quote the key line; explain the
rest.
+14
View File
@@ -34,6 +34,20 @@ Generated-by: Claude Opus 5.5
trailer only on those commits. Don't add it to every commit. trailer only on those commits. Don't add it to every commit.
- **PR descriptions:** say which parts were AI-generated. The commit - **PR descriptions:** say which parts were AI-generated. The commit
trailers stay the record that counts. trailers stay the record that counts.
- **Unreviewed PRs are drafts.** When an agent opens a PR under the
human's GitHub account and the human hasn't read it yet, open it as
a draft (`gh pr create --draft`) and start the description with:
```
> [!WARNING]
> **Unreviewed LLM output.** Generated by <Tool> <Version>; no human
> has reviewed this code yet.
```
The human removes the caveat and marks it ready after reviewing.
The caveat exists because the PR appears to come from the human.
A PR opened from a bot account already shows who wrote it, so it
doesn't need the caveat.
- **The human stays responsible.** The commit author vouches for the - **The human stays responsible.** The commit author vouches for the
contribution. That includes disclosing material that is not their contribution. That includes disclosing material that is not their
original creation, and making sure the tool's terms grant enough original creation, and making sure the tool's terms grant enough
+69
View File
@@ -0,0 +1,69 @@
---
name: discord-agent-workspace
description: Set up a Claude Code agent you can message from Discord, working in a workspace folder of your projects — when someone says "set me up like Dan's bot", "I want to text my Claude from my phone", "Claude Code on Discord", or "make a workspace for my agent". Walks a non-developer through the Discord bot, the workspace folder (AGENTS.md, skills, repos), access control, and keeping it running. Not for chat-only Claude apps with no computer behind them.
---
# Discord agent workspace
Dan's arrangement, reproduced for someone else:
- **Claude Code** runs in a terminal on a computer that stays on.
- **The Discord channel plugin** turns a Discord bot into a pipe: allowed people's messages arrive in that session, and Claude answers with a `reply` tool ([plugin README](https://github.com/anthropics/claude-plugins-official/tree/main/external_plugins/discord)).
- **A workspace folder** holds the projects plus written-down conventions, so each fresh session knows where it is and how the owner likes things done.
Your user may not be a developer. Do each step with them, one at a time; say what a command does before running it, and stop at anything that needs their account, their browser, or a secret. Never paste a token into chat; have them put it in the file or command themselves.
## 0. Is it worth it for them?
Ask what they'd send it. Good fits: tasks over files (code, documents, notes, data) that take a sentence to ask, minutes to do, and a glance to check. Poor fits: work that lives only in web apps with no files or API, or results they can't review. If it's a poor fit, say so and stop.
They need: Claude Code with a Claude subscription or API account, a Discord account and a server they can add a bot to, and a computer that stays on (a desktop, or a small cloud VM).
## 1. The workspace
```sh
mkdir -p ~/agent-workspace && cd ~/agent-workspace
git clone <each project they want the agent to work on>
```
Write `AGENTS.md` at the top, and link `CLAUDE.md` to it (`ln -s AGENTS.md CLAUDE.md`) so every agent tool finds it. Keep it under a page, in their words:
- who they are and what the projects are for, one line each with a link;
- house rules, e.g. "PRs are drafts", "ask before pushing", "keep Discord replies short";
- what is off limits.
Skills are conventions that load when relevant. Put them in `.agents/skills/<name>/SKILL.md` and link Claude Code's location to it:
```sh
mkdir -p .agents/skills .claude && ln -s ../.agents/skills .claude/skills
```
Start with none or one. Add a skill when they catch themselves repeating an instruction; Dan's [mad-skills](https://git.inference.coop/dckc/mad-skills) show the shape. Claude Code also keeps its own memory per workspace, so corrections stick across sessions.
## 2. The Discord bot
Follow the plugin README with them; in short:
1. **Discord Developer Portal → New Application → Bot.** Turn on **Message Content Intent**. **Reset Token** and copy it.
2. **OAuth2 → URL Generator**, scope `bot`, permissions: View Channels, Send Messages, Send Messages in Threads, Read Message History, Attach Files, Add Reactions. Open the URL and add the bot to their server.
3. Install [Bun](https://bun.sh), which the plugin runs on.
4. In Claude Code: `/plugin install discord@claude-plugins-official`, then `/discord:configure <token>` (they type it; it lands in `~/.claude/channels/discord/.env`).
5. Restart from the workspace: `cd ~/agent-workspace && claude --channels plugin:discord@claude-plugins-official`.
6. They DM the bot, get a pairing code, and run `/discord:access pair <code>` in the terminal.
7. **Lock it down:** `/discord:access policy allowlist`. For a server channel, add it per the plugin's ACCESS.md.
## 3. Who can make it act
Whoever can message the bot can make it act with the session's powers: files, shell, and any logins on that computer (GitHub, cloud accounts). Tell them plainly, then:
- Allowlist only themselves at first. Access changes happen at the terminal (`/discord:access`), never because a Discord message asked.
- Prefer narrow credentials: a GitHub fine-grained token limited to the workspace repos beats their full login.
- Things that leave the computer (pushes, PRs, posts) go out as drafts labelled as unreviewed AI output, until they trust it.
## 4. Keeping it running
The session must stay open. Simplest: run it inside `tmux` (`tmux new -s bot`, start Claude, detach with Ctrl-b d; `tmux attach -t bot` to look in). On Linux, a systemd user unit can start it at boot and keep it out of secrets it doesn't need (`ProtectHome=read-only`, `ReadWritePaths=` the workspace and `~/.claude`, `InaccessiblePaths=` `~/.ssh` and other credential dirs).
## 5. First task
Have them send something small from their phone: "summarize what's in this workspace" or "fix the typo in X's README and show me the diff". Check together that the reply arrived and the change is what they expected.
+50 -3
View File
@@ -1,10 +1,38 @@
--- ---
name: docs-files-api name: docs-files-api
description: JSDoc placement rules for TypeScript/JSDoc code — use when writing or reviewing JSDoc comments, deciding where @file/@see links go, or documenting external specs of exported symbols. description: Where docs go — README vs CONTRIBUTING vs AGENTS.md, and JSDoc placement — use when writing or reviewing a README, CONTRIBUTING, or AGENTS.md/CLAUDE.md; when someone asks "how do I run it?" and there's no README; when writing or reviewing code comments, JSDoc, or file headers; deciding where @file/@see links go; documenting exported symbols or external specs; when a `//` comment explains what a declaration is or does (it belongs in JSDoc).
--- ---
# Documentation Conventions # Documentation Conventions
## README, CONTRIBUTING, AGENTS.md
A **README** sells, then gets the reader running:
- **Up top, about two sentences:** what it is and why to care. Then
straight to getting started.
- **Getting started:** get them to TADA! with no fuss. Readers skip to
the first fenced block and copy it; if it doesn't work, boo. So that
block runs as-is from a fresh clone and ends with something to look
at (a URL, output). No architecture, no principles.
- **After that:** features and benefits, as much as earns its place.
**Headings say something.** A heading is prime real estate: it shows in
the outline and in link targets. "What's in it", "Overview", "Features",
"Details" say nothing; name the point instead ("Bookmarks in, a zine
out"). Plain labels are fine only where the reader hunts for them by
name: "Getting started", "Deploying".
Everything else goes in **CONTRIBUTING**: design, layout of the code,
how to test, conventions, deploying.
**AGENTS.md is a symlink to CONTRIBUTING.md** (likewise CLAUDE.md, if a
tool wants one). Good advice is good advice, for people and bots alike.
If agents need something people don't, give it a short section in
CONTRIBUTING.
## JSDoc
JSDoc placement rules (from the JSDoc placement rules (from the
[portfolio-contract contributing notes](https://github.com/Agoric/agoric-sdk/blob/master/packages/portfolio-contract/CONTRIBUTING.md#documentation-conventions)): [portfolio-contract contributing notes](https://github.com/Agoric/agoric-sdk/blob/master/packages/portfolio-contract/CONTRIBUTING.md#documentation-conventions)):
@@ -14,9 +42,28 @@ JSDoc placement rules (from the
- **Entry-point JSDoc:** Put the meaningful docs on the *exported* symbols — - **Entry-point JSDoc:** Put the meaningful docs on the *exported* symbols —
that's what shows up in IDE call sites. External URLs belong here when they that's what shows up in IDE call sites. External URLs belong here when they
define the canonical ABI/spec of the thing being exported. define the canonical ABI/spec of the thing being exported.
- **API docs are JSDoc, not `//`:** What a function, class, method,
type, field, or constant *is* or *does* goes in a `/** … */` comment on
its declaration — IDEs show it at call sites and on hover; a `//` line
above the declaration shows nowhere. `//` is for implementation notes
inside a body: the *why* of a non-obvious line, next to that line.
- **Exception: program scripts** (run directly: a shebang, or
`import.meta.main`). The manual goes at the top: a one-line purpose, a
usage synopsis, the arguments, and what it writes or prints. People
open a script to learn how to run it, and its `main` has no call sites
for JSDoc to show at. If the script prints its usage, make the manual a
`HELP` string constant right under a one-line `@file` (JavaScript has
no docstrings; a string literal is the honest equivalent) rather than
copying it or reading the script's own source. Helpers inside still get
their own JSDoc.
- **Write for the reader, not the conversation:** Comments say what the
code is and why, in the reader's terms. No traces of how it came to be
(design discussions, who asked, "for now", analogies used while
deciding), and nothing the code or its shebang already says.
- **External design constraints:** If a module is constrained by external - **External design constraints:** If a module is constrained by external
contracts, standards, or off-repo design docs, add a short JSDoc note with a contracts, standards, or off-repo design docs, add a short JSDoc note with a
`@see` link to the authoritative source at the relevant entry point(s). `@see` link to the authoritative source at the relevant entry point(s).
In short: brief at the top of the file, meaty on exports, `@see` to In short: brief at the top of the file, meaty on exports (in JSDoc),
authoritative external sources only where they define the contract. `//` only for the why inside bodies, `@see` to authoritative external
sources only where they define the contract.
+62
View File
@@ -0,0 +1,62 @@
---
name: grice-maxims
description: >-
Grice's maxims: be concise, truthful, relevant, clear — use before any reply, and when writing or copy editing prose for a reader (docs, READMEs, skills, commit messages, PR descriptions); when someone says "tighter", "too wordy", "stop blathering", "omit needless words", or cites Strunk & White or Grice; when about to say something you lack adequate evidence for. Optimize for reader attention: say what's needed, nothing the reader already knows, in an order easy to follow.
---
# Be concise; optimize for reader attention
Take care to apply [Grice's four maxims](https://en.wikipedia.org/wiki/Cooperative_principle#Grice's_maxims)
of conversation: quantity, quality, relation, and manner.
The maxim of **quantity** is: be informative.
Submaxims:
- Make your contribution as informative as is required (for the current
purposes of the exchange).
**Do not make your contribution more informative than is required.**
The maxim of **quality** is: be truthful.
Supermaxim:
- Try to make your contribution one that is true.
Submaxims:
- Do not say what you believe is false.
- Do not say that for which you **lack adequate evidence**.
The maxim of **relation** is: be relevant: the information provided
should be relevant to the current exchange and omit any irrelevant
information. Do not spend words on things the reader already knows.
The maxim of **manner** is: be clear.
Be brief — i.e., avoid unnecessary verbosity.
Be orderly — i.e., provide information in an order that makes sense,
and makes it easy for the recipient to process it.
If you can say it with fewer words, do.
## in practice
- Link a well-known convention; don't restate it.
- Cut tails that repeat a name, a link's target, or the obvious.
- Quote the key line of a source; explain the rest in your own words.
## sources
- William Strunk Jr., [The Elements of Style](https://www.gutenberg.org/files/37134/37134-h/37134-h.htm)
(1918; later editions with E. B. White). Rule 13, "Omit needless words":
> Vigorous writing is concise. A sentence should contain no unnecessary
> words, a paragraph no unnecessary sentences, for the same reason that
> a drawing should have no unnecessary lines and a machine no
> unnecessary parts.
- Lucile Vaughan Payne,
[The Lively Art of Writing](https://openlibrary.org/books/OL7410114M/The_Lively_Art_of_Writing)
(Prentice Hall, 1975; ISBN 978-0205091607)
- [Cooperative principle — Wikipedia](https://en.wikipedia.org/wiki/Cooperative_principle)
+59
View File
@@ -0,0 +1,59 @@
---
name: hackmd-deck
description: HackMD slide deck for a light talk — use when asked to draft slides, a deck, or a talk for show & tell, office hours, or a demo; when someone says "hackmd deck", "whip up some slides", or names a talk title and points at work to present (repos, git history, session transcripts). Covers the flashback story arc, emoji markers for wins and friction, and real artifacts as eye candy. Not for .pptx files or polished conference talks.
---
# HackMD deck
```markdown
---
title: Good Fences Make Good Agents
type: slide
---
# Good Fences Make Good Agents
---
## 🎉 Monday's issue drafts itself
---
## How: who holds what
![wiring](weekly-roundup-wiring.png)
```
Example: [Good Fences Make Good Agents](../../../capsite/docs/good-fences-slides.md); an early, overboard one: [Jun 25 slides](https://hackmd.io/T9-oFWEPSb2zzqWAGFv60Q#/).
## Context
A talk at something like Agoric's engineering show & tell: "Presentations should take little to no preparation", by engineers for engineers. The work already exists in repos, commits, test output, and session transcripts.
## Problem
**I have done a bunch of stuff I think is interesting, but I can't justify the time to make a polished presentation.**
## Therefore
**Let an agent do a quick-n-dirty one: a flashback, built from the record.**
The arc:
1. 🎉 TADA! Look what I did. It must be on the thread the talk follows.
2. Now that you're hooked, how I did that.
3. Eye candy along the way: diagrams, verbatim code, test output.
4. Conclusions and/or challenges that lead to discussion.
Ground every slide in the record: quote code and test output verbatim (trim with `...`), check claims against the code and git history, and mark what's only designed or not deployed with ⏳.
Mark wins, losses, challenges, and friction with emojis: visually appealing, intuitive at a glance.
- wins: 🎉 the TADA · ✅ shipped · ✨ sparkle, delight · 💡 aha: now I see how to address the issue · 🙌 help, or a gift from someone else's work
- friction and follow-ups: ⏳ delays, not yet · 🧰 fragile tooling, ceremony · 💤 silent failures · 📘 docs · 🔔 alerts · 📈 observability · 🪄 a wish, often one a problem brings to mind · 🔧 fixes owed · 🧱 walls
Write the deck in the repo the talk is about (e.g. `docs/<title>-slides.md`), images beside it, and commit it.
## Resulting context
The deck takes 15 minutes: the agent spends 5, the presenter polishes for 10. The user pastes it into HackMD and uploads the images.
+121
View File
@@ -0,0 +1,121 @@
---
name: ibis-design
description: >-
IBIS / issue-based information system — use when writing or reviewing a design discussion, decision record, or outline of issues, positions, and pro/con arguments; when someone marks lines with `?`, `?!`, `?~`, `:`, `:+`, `:-`, `:~+`, `+`, `-`, `!`, `.`; when asked to "IBIS this", lay out options for a decision, record a fallback, or capture why a choice was made. Source: dckc/vattr97 wiki IBIS page.
---
# IBIS design discussions
[IBIS page on the vattr97 wiki](https://github.com/dckc/vattr97/wiki/IBIS),
condensed to working rules. See also
[Issue-based information system on Wikipedia](https://en.wikipedia.org/wiki/Issue-based_information_system).
IBIS develops solutions to issues asynchronously and keeps the
reasoning around so the resolution can evolve. It's an outline; a
leading symbol on each item says what kind of element it is.
## symbols
| Symbol | Meaning |
| :---: | :--- |
| `?` | an issue to be considered |
| `?!` | a resolved issue |
| `?~` | tentatively resolved, or still open but with a fallback position |
| `:` | a position on an issue (positions on one issue may overlap) |
| `:+` | an accepted position |
| `:-` | a rejected position |
| `:~+` | tentatively accepted, or the fallback position for the issue |
| `+` | pro: an argument for a position |
| `-` | con: an argument against a position |
| `!` | a to-do on an element (optionally with initials: `!jf`) |
| `.` | a comment on an element |
## rules
1. **Phrase issues to admit overlapping positions**, as complete
sentences — not yes/no. "? What tools should we provide for
enterprise developers?", not "? Should we support VSCode?"
2. **Positions are complete sentences** proposing a full or partial
solution. They may overlap or conflict, and needn't capture every
ramification — refine them with sub-positions.
3. **Nesting:**
- A sub-issue discusses a portion of its outer issue; its positions
nest under it.
- A sub-position refines its parent *assuming the parent is
accepted*; its arguments nest under it.
- Arguments are always pro or con with respect to their containing
issue, so a `+` under a `-` is typically a mitigation ("here's why
that con isn't so bad").
4. **An issue is resolved only when some position is accepted.** Not
every position needs an accept/reject verdict.
5. **Use fallbacks (`?~`, `:~+`) to agree on a minimum bar** while
leaving the issue open. The first fallback is often the status quo.
Being the fallback must not preclude considering other options —
that's what keeps fallbacks easy to agree on.
6. **When one issue is really several choices, say so** with a `.`
comment and split it into sibling `?` sub-issues.
7. **Attach each argument to the position it bears on.** If it applies
equally to the siblings, it belongs higher up, or the issue is
several choices (rule 6). A comment that rules a position out is a
con, and the position is rejected (`:-`).
8. **Check the source before raising an issue.** If the design text
already answers it, record the issue as resolved and cite that text
as the pro.
9. **Past three levels of nesting, split out a sub-issue.**
## numbering
Number elements so replies can refer to them ("2: …"): issues `1`,
`2`; positions under them `1a`, `1b`; anything below that `1a-i`,
`1a-ii`, then `1a-i-a`. The number follows the symbol: `?! 2. …`,
`:+ 2c. …`. Renumbering breaks references made in chat or other docs,
so append rather than renumber once people are citing numbers. Show
dependencies between issues with "(see 5)".
## writing it in markdown
A leading `-` or `.` collides with list syntax and editor
auto-bulleting. Put each symbol after the list marker, escaping where
needed (`- \- con`, `- \+ pro`), or use a fenced block with indentation
as in the example below.
Past a screenful, give each top-level issue its own heading
(`### ? 3. What is the fence?`) with its elements in a fenced block
below. Headings show up in the editor outline and give each issue a
link target. Links don't render inside a fence, so put sources after
the block and refer to them from the line they support.
## example
```
? How should we test our software?
: use a test runner of some form
: build a QA department
:~+ use tape
+ it's what we currently use
+ it's very simple
+ it works in numerous environments
- it's not very expressive
- it doesn't have code coverage
: use Jest
+ lots of people like it
- guess-my-syntax DSL for expressing assertions
+ you can separate the assertion choice from the framework
+ can do integration and unit tests very well
!jf can it do them better than tape?
: roll our own test runner
: make them importable ESM
. then we can run them in XS
+ could be simpler than tape
- requires implementation and maintenance
:+ use test262
+ required for JS standards testing
- clunky for other uses
. there are actually three separate choices: test runner, test environment, and assertion library
? What assertion library should we use?
? What test runner should we use?
? What test environments should we support?
: SES
: NodeJS
: in-browser
```
+53
View File
@@ -0,0 +1,53 @@
---
name: io-between-turns
description: I/O between turns — use when writing or reviewing code that reads or writes files, sockets, databases, or other shared external state; when choosing between sync and async APIs (readFileSync vs fs/promises, DatabaseSync, blocking calls); when someone says "sync I/O is a pointy instrument" or "I/O should be done between turns". Sync access is fine only for state that is local, prompt, and private to the one actor (e.g. a Durable Object's SQLite).
---
# I/O between turns
Event-loop concurrency (E, JavaScript, Workers) runs one **turn** at a
time: deliver one event, run the resulting calls to completion, then
take the next. A turn that never waits on the outside world is
isolated: nothing else can change what it can see while it runs. I/O
on shared or slow resources belongs **between** turns: issue it, end
the turn, and continue when the result arrives as a later event.
> "Execution proceeds by taking a pending-delivery from the queue,
> delivering its message to its object, and processing all the
> resulting immediate-calls in conventional call-return order. This is
> called a turn. … Because each event's turn runs to completion before
> the next is serviced, they are temporally isolated."
> — [Concurrency Among Strangers: Programming in E as Plan Coordination](https://agoric.com/assets/pdf/papers/concurrency-among-strangers.pdf),
> Miller, Tribble & Shapiro, TGC 2005, §5.2
> "The actual E system does provide synchronous file I/O operations.
> When these files are local, prompt, and private to the vat accessing
> them, this does not violate turn isolation, but since files may be
> remote, non-prompt, or shared, the availability of these synchronous
> I/O operations does violate the E model."
> — same paper, §6, footnote 5
(Mark Miller's thesis, *Robust Composition*, 2006, builds on this
paper.)
## the rule
- **Shared, remote, or slow state → async, between turns.** The
filesystem, the network, other processes' databases: use promise
APIs (`node:fs/promises`, `fetch`) and `await` them. No
`readFileSync`/`writeFileSync`, no blocking calls inside a turn.
- **Local, prompt, and private to the actor → sync is fine.** A
Durable Object's SQLite (`ctx.storage.sql`) is reachable only by its
own DO, so it is "more or less more RAM": synchronous access keeps
turn isolation.
- **Only a sync API exists, for shared state?** (e.g. `node:sqlite`'s
`DatabaseSync` on a file other processes may touch.) Keep it at the
program's edge and say why in a comment, so it doesn't read as a
precedent.
## see also
- [no-ambient-authority](../no-ambient-authority/SKILL.md): who gets to do I/O at
all (authority passed in, not ambient); this skill is about *when*.
- [silent-failures-are-evil](../silent-failures-are-evil/SKILL.md): an
awaited I/O failure still needs a trace.
+1 -1
View File
@@ -46,5 +46,5 @@ amounts", not "fix for goof #3".
If the goof can't reasonably be captured in an automated test (e.g. it If the goof can't reasonably be captured in an automated test (e.g. it
lives in config, a one-off script, or untestable I/O with no seam), say lives in config, a one-off script, or untestable I/O with no seam), say
so explicitly, explain why, and suggest the seam that would make it so explicitly, explain why, and suggest the seam that would make it
testable (see [ocap-discipline](../ocap-discipline/SKILL.md) for testable (see [no-ambient-authority](../no-ambient-authority/SKILL.md) for
injecting I/O). Don't silently skip the debt. injecting I/O). Don't silently skip the debt.
@@ -1,6 +1,7 @@
--- ---
name: ocap-discipline name: no-ambient-authority
description: Use when writing or reviewing code whose functions touch I/O, clocks, random number generators, process.env, fetch, or filesystem; when someone says "ambient authority", "wash your mouth out", or "that function shouldn't reach for that"; when debating dependency injection or default-arg seams; when asked what OCap discipline is. Source: the Agoric wiki's OCap Discipline page. description: >-
Use when writing or reviewing code whose functions touch I/O, clocks, random number generators, process.env, fetch, or filesystem; when someone says "ambient authority", "wash your mouth out", or "that function shouldn't reach for that"; when debating dependency injection or default-arg seams; when asked what OCap discipline is. Source: the Agoric wiki's OCap Discipline page.
--- ---
# OCap Discipline # OCap Discipline
@@ -10,14 +11,22 @@ condensed to working rules.
## the background, in three lines ## the background, in three lines
Object-capability security = memory safety + encapsulation ("no Object-capability security
([capability-based security](https://en.wikipedia.org/wiki/Capability-based_security))
= memory safety + encapsulation ("no
reference to an object except: create it, be given it at creation, or reference to an object except: create it, be given it at creation, or
receive it in a message") combined with **primitive effects only via receive it in a message") combined with **primitive effects only via
references** — all world-touching primitives are embodied by objects, references** — all world-touching primitives are embodied by objects,
and *anything globally accessible is immutable data*. No `open(filename)` and *anything globally accessible is immutable data*. No `open(filename)`
in the global namespace: converting a string to file access is like in the global namespace: converting a string to file access is like
casting an integer to a pointer. JavaScript doesn't enforce this; the casting an integer to a pointer; that's
discipline is a convention (layerable via SES). [ambient authority](https://en.wikipedia.org/wiki/Ambient_authority).
JavaScript doesn't enforce this; the discipline is a convention
(layerable via SES).
New to object capabilities? Start with
[awesome-ocap](https://github.com/dckc/awesome-ocap), a curated list of
capability-based languages, systems, papers, and talks.
## the working rules (from the wiki) ## the working rules (from the wiki)
@@ -66,6 +75,8 @@ export const mintKey = (entropy: Entropy): string => b32(entropy(16));
- [OCap Discipline — Agoric wiki](https://github.com/Agoric/agoric-sdk/wiki/OCap-Discipline) - [OCap Discipline — Agoric wiki](https://github.com/Agoric/agoric-sdk/wiki/OCap-Discipline)
(Miller, *Robust Composition* PhD thesis, 2006, is the deep source) (Miller, *Robust Composition* PhD thesis, 2006, is the deep source)
- [ambient authority — Wikipedia](https://en.wikipedia.org/wiki/Ambient_authority) - [Ambient authority — Wikipedia](https://en.wikipedia.org/wiki/Ambient_authority)
- [Capability-based security — Wikipedia](https://en.wikipedia.org/wiki/Capability-based_security)
- [awesome-ocap: Awesome Object Capabilities and Capability-based Security](https://github.com/dckc/awesome-ocap)
- in this workspace: `capsite/docs/webkeys.md` (URL-borne bearers), - in this workspace: `capsite/docs/webkeys.md` (URL-borne bearers),
Agoric's eslit-based enforcement notes for contracts Agoric's eslit-based enforcement notes for contracts
+60
View File
@@ -0,0 +1,60 @@
---
name: notify-me
description: Notify me / ping me — desktop (XDG) notification via `notify-send` at the end of any long turn (many steps, builds, test runs, deploys, waiting on background work), asked or not; and when the operator says "notify me", "XDG notify me", "ping me when you're done", "let me know about progress", or "make it stick / indefinite duration" because they're stepping away. Covers done vs. progress notices, sticky vs. transient, and updating one notice in place. Not for Discord replies (use the reply tool) or for quick back-and-forth turns.
---
# Notify me
```sh
notify-send -a capsite "done: diigo-cache" "3 commits on main, not pushed"
```
## Context
The operator hands a session a batch of work, then turns to another session or goes for a walk.
## Problem
**Without going to look, the operator can't tell which session is done or stuck.**
## Forces
- opencode-web notifies on every reply; notices without news teach the operator to ignore them all.
- Silence is worse: a finished turn sits unseen.
- The operator won't always think to ask.
- Notices expire before the operator is back from a walk.
- Several sessions share one desktop.
- Progress notices pile up.
## Therefore
**At the end of any turn long enough that the operator has probably looked away, send one notice: which session, what state, what the operator should do next. Not during quick back-and-forth.**
Long means more than a handful of tool calls, or waiting on a build, tests, deploy, or background job. "Notify me when you're done" forces one even for a short turn.
- `-a` names the session (repo or task).
- The summary leads with the status: `done`, `blocked` (including a question that stops the work), `failed`, `progress`.
- The body says what's next: "needs a decision on the DO migration".
**Sticky** ("indefinite duration", "going for a walk"): `-t 0` (never expire). `-u critical` only if asked.
**Progress** ("notify me about progress"): one per milestone (tests pass, deployed), replacing the last rather than stacking:
```sh
id=$(notify-send -p -a capsite "progress: diigo-cache" "tests pass; deploying")
notify-send -r "$id" -a capsite "done: diigo-cache" "deployed; not pushed"
```
Keep `$id` in a file (e.g. `.tmp/notify-id`) if the shell doesn't persist.
## Resulting context
The operator can leave a session alone and trust it to call. A notice means news, so it gets read.
`notify-send` needs the session bus; under a confined unit or ssh it may fail. Say so in the reply ([silent-failures-are-evil](../silent-failures-are-evil/SKILL.md)).
The agent must still remember this pattern at the end of a long turn, just when that slips.
## TODO
- A Claude Code `Stop` hook could time the turn and notify past ~2 min; opencode would need its own plugin hook.
+42
View File
@@ -0,0 +1,42 @@
---
name: obsidian-markdown
description: Markdown in an Obsidian vault that agents also write — use when an agent writes or edits any .md file in a workspace with a `.obsidian/` folder (notes, READMEs, CONTRIBUTING, AGENTS.md, skills, docs); when someone mentions Obsidian, hard wrap, ragged "poetry" margins, wikilinks, or the vault; when adding a symlink to markdown. Not for Discord or PR text (see grice-maxims).
---
# Markdown for an Obsidian vault agents write in
The workspace is an [Obsidian](https://obsidian.md/) vault: a `.obsidian/` folder at its root. Dan reads and edits in Obsidian; agents write the same files from a terminal. Write markdown that reads well in Obsidian and on GitHub, and is easy for an agent to edit.
## One line per paragraph
Don't hard-wrap prose. A paragraph is one line; so is each list item. Break lines only between blocks: paragraphs, list items, headings, fences.
Wrapped paragraphs render with a ragged "poetry" margin in Dan's Obsidian, and they make diffs of a one-word edit reflow a whole paragraph. Code blocks keep their line breaks, of course.
When you rewrite a wrapped paragraph, unwrap it. Don't unwrap a whole file you aren't otherwise rewriting: that's a noisy diff in someone else's prose.
## Links
- Standard markdown links with relative paths: `[the webkeys pattern](docs/webkeys.md)`. Obsidian follows them, and GitHub renders them. `[[wikilinks]]` render only in Obsidian.
- No spaces in file names, so links need no `%20`.
- Link text says where the link goes (see hypertext-links).
## File names and headings
Obsidian shows file names in tabs, the file explorer, and search, so name files for what's in them.
Headings show in Obsidian's outline pane: make them say something.
## Frontmatter
Obsidian shows YAML frontmatter as Properties. Keep it valid YAML: quote values that contain `: ` or start with a special character.
## Symlinks
From [Obsidian's symlinks page](https://obsidian.md/help/symlinks):
> Obsidian ignores any symlink to a parent folder of the vault, or from one folder in the vault to another folder in the same vault.
> File symlinks (as opposed to folder symlinks) may work, but aren't officially supported at this time.
So a folder symlink inside the vault (`.claude/skills` → `.agents/skills`) is invisible to Obsidian, and that's fine: no duplicates. A file symlink like `AGENTS.md` → `CONTRIBUTING.md` may not show or update in Obsidian. Edit the target, never the link.
+94 -71
View File
@@ -1,106 +1,129 @@
--- ---
created: 2026-05-14 name: retcon
updated: 2026-05-14 description: Retcon a PR branch — use when a maintainer says "retcon", or asks to clean up, regroup, squash, or reorganize a PR's commits into sensibly grouped commits; one commit per affected package with implementation and tests together, conventional-commit messages, and yarn.lock isolated in its own commit. Resets to the base and restages the same net diff; the merge result is byte-identical. Not a rebase onto a new base.
author: gardener
--- ---
# Skill: retcon # Retcon
Reset a PR's branch to its base and restage the same net diff as a sensibly grouped commit history. The PR's diff is unchanged; only the commit grouping changes. The verb is the maintainer's: *"retcon"* = retroactively continuity-fix the branch's commit history. Reset a PR's branch to its base and restage the same net diff as a
sensibly grouped commit history. The PR's diff doesn't change. Only the
The retcon's typical executor is the [fixer](../../roles/fixer/AGENT.md), dispatched against an open PR whose substance is right but whose commit grouping is not. The [weaver](../../roles/weaver/AGENT.md) handles rebase-onto-current-base; the retcon is the orthogonal operation of regrouping commits without moving the base. commit grouping changes. The name is short for *retroactive continuity*:
fixing the story the branch's history tells.
## When to use ## When to use
- A maintainer says "retcon #N" or "please retcon this branch". The verb names this procedure. - A maintainer says "retcon #N" or "please retcon this branch".
- A PR's commit history grew sprawling during the jury-fixer loop (one commit per fixer round, intermediate work-in-progress commits, lockfile churn interleaved with code). - A PR's history sprawled during review: one commit per round of
- Pre-ferry: the boatman's upstream is reviewing commit-by-commit and the bot-side history is not in shape for that. Retcon, then ferry. feedback, work-in-progress commits, lockfile churn mixed in with code.
- Reviewers upstream read commit by commit, and the history isn't ready
for that.
The retcon is **not** a rebase onto a new base. If the branch lags its base, run the weaver first (or chain "weave then retcon"). Retcon assumes the base is current. A retcon is **not** a rebase onto a new base. If the branch lags its
base, rebase first, then retcon. A retcon assumes the base is current.
## What the retcon produces ## What a retcon produces
A linear history on the PR branch with commits grouped as: A linear history with commits grouped as:
- **One commit per affected package.** Changes under `packages/foo/` ship in one commit; changes under `packages/bar/` ship in another. Conventional-commit message scoped to the package (`feat(foo): ...`, `fix(bar): ...`, etc.). - **One commit per affected package.** Changes under `packages/foo/` go
- **One `chore: Update yarn.lock` commit** for the lockfile, separate from any `package.json` commit that caused it. Cite [yarn-lock-separate-commit](../yarn-lock-separate-commit/SKILL.md) for the discipline; the retcon is the place that discipline is *applied retroactively* across the whole branch, not commit-by-commit at write time. in one commit, and changes under `packages/bar/` go in another. Each
- **Implementation and tests in the same commit.** A package's behavior change and the tests covering that change ship together. This is a deliberate departure from the "test, then implement" or "implement, then test" patterns that produce two commits per feature; the retcon wants one. message is scoped to its package: `feat(foo): ...`, `fix(bar): ...`.
- **Conventional-commit messages** on every commit. The first line is `<type>(<scope>): <imperative summary>`; the body (when present) names *why*, not the diff. - **One `chore: Update yarn.lock` commit**, separate from the
`package.json` change that caused it. This applies the
[yarn.lock separate-commit rule](../yarn-lock-separate-commit/SKILL.md)
to the whole branch at once.
- **Implementation and tests in the same commit.** A package's behavior
change and the tests covering it ship together, not as a "test" commit
plus an "implement" commit.
- **Conventional-commit messages.** The first line is
`<type>(<scope>): <imperative summary>`. The body, if any, says *why*,
not what the diff shows.
The PR's net diff (the merge result) is byte-identical before and after the retcon. The judge's panel re-runs against the new commit shape: each per-package commit gets its own scope of attention rather than the panel having to mentally re-group sprawling history. The PR's net diff (its merge result) is byte-identical before and
after.
## Procedure ## Procedure
Run inside the PR's project worktree on its head branch. The branch must be up to date with its base; if not, run a weaver first. Run on the PR's head branch, in a worktree that is up to date with the
base.
```sh ```sh
# 0. Sanity: confirm the working tree is clean and the branch tip matches the # 0. Sanity: clean tree, and the tip matches the remote we'll push to.
# remote we'll push to. git status --porcelain # must be empty
git -C project status --porcelain # must be empty git log --oneline origin/<head-branch>..HEAD # must be empty
git -C project log --oneline origin/<head-branch>..HEAD # must be empty
# 1. Identify the base. For a PR, this is the base branch named in the PR's # 1. Save the pre-retcon tip for the no-net-change check.
# metadata (typically master or main, or llm for design-roadmap PRs). git tag pre-retcon HEAD
BASE=$(gh pr view <N> -R <owner>/<repo> --json baseRefName --jq '.baseRefName')
# 2. Reset to the base, keeping the net diff as a working-tree state. # 2. Find the base branch.
# --mixed (the default) unstages everything; --soft keeps the index. Use BASE=$(gh pr view <N> --json baseRefName --jq .baseRefName)
# --mixed so the next step's `git add` is deliberate.
git -C project reset --mixed origin/$BASE
# 3. Restage by package. For each affected package, add the package's files # 3. Reset to the base, keeping the net diff as unstaged changes.
# and commit with a conventional-commit message. # --mixed, so each `git add` that follows is deliberate.
git -C project add packages/foo/ git reset --mixed origin/$BASE
git -C project commit -m "feat(foo): <one-line summary>"
git -C project add packages/bar/ # 4. Restage by package: implementation + tests together.
git -C project commit -m "fix(bar): <one-line summary>" git add packages/foo/
git commit -m "feat(foo): <summary>"
git add packages/bar/
git commit -m "fix(bar): <summary>"
# ... one commit per affected package ...
# (... one commit per affected package, implementation+tests bundled ...) # 5. Lockfile last.
git add yarn.lock
git commit -m "chore: Update yarn.lock"
# 4. Stage and commit the lockfile last, per yarn-lock-separate-commit. # 6. Verify: no net change.
git -C project add yarn.lock git diff pre-retcon HEAD # must be empty
git -C project commit -m "chore: Update yarn.lock"
# 5. Confirm the net diff is unchanged. # 7. Push without clobbering concurrent work, then clean up.
git -C project diff origin/$BASE..HEAD --stat # compare to the pre-retcon diff git push --force-with-lease origin HEAD:<head-branch>
git -C project diff <pre-retcon-sha>..HEAD # must be empty (no net change) git tag -d pre-retcon
# 6. Force-push with --force-with-lease.
git -C project push --force-with-lease origin HEAD:<head-branch>
``` ```
The `--force-with-lease` (not `--force`) refuses to overwrite work pushed since the agent's last fetch; if the lease is rejected, fetch again and abort the retcon rather than clobbering concurrent work. Run `git status` after each `git add` to confirm that only the intended
files are staged.
### Save a pre-retcon reference ## Changes that don't fit one package
Before step 2, tag (or note) the pre-retcon tip so the no-net-change verification in step 5 has something to diff against. `git tag pre-retcon-<short-id> HEAD` works locally; delete the tag after the push lands. Name these groupings when you report the retcon, so the reviewer reads
the new history as a deliberate shape:
### Capture grouping decisions - **Top-level config or build:** one `chore:` or `build:` commit.
- **Cross-package refactors that must land atomically:** one
When the branch's changes do not map cleanly to a per-package commit (a cross-package refactor, a touch in a top-level config file), name the groupings in the retcon's report so the panel and the maintainer can read the new history as a deliberate shape rather than guessing. Typical buckets when a per-package split does not exhaust the diff: `refactor(<area>):` commit, where the area names the concept, not a
package.
- **Top-level config or build changes.** One commit, `chore: <summary>` or `build: <summary>`. - **Docs-only files** (READMEs, design notes): one `docs:` commit, or
- **Cross-package refactors that have to land atomically.** One commit covering all packages it touches, `refactor(<area>): <summary>`. The "area" names the conceptual change, not a single package. `docs(<scope>):` if it's scoped.
- **Documentation-only files (READMEs, design notes).** One commit, `docs: <summary>` or `docs(<scope>): <summary>` when scoped.
## Net diff is invariant ## Net diff is invariant
The single load-bearing property of a retcon: the PR's net diff (base ... new head) is byte-identical to the PR's pre-retcon diff (base ... pre-retcon head). The retcon changes commit grouping; it does not change what the PR proposes to merge. Step 5's two diffs verify this: This is the property everything else rests on. `git diff pre-retcon
HEAD` must be empty, because the two heads have the same tree. If it
- `git diff origin/$BASE..HEAD --stat` against the pre-retcon stat: same files, same line counts. isn't empty, **don't push.** Run `git reset --hard pre-retcon` and start
- `git diff <pre-retcon-sha>..HEAD`: empty. The two heads have the same tree. over.
If either check fails, the retcon went wrong; do not push. Roll back with `git reset --hard <pre-retcon-sha>` and start over.
## Notes from the field
- _2026-05-14_: the verb landed when kriskowal said *"A new verb I would like to use is 'retcon' meaning 'Please reset this branch and restage the changes in sensibly grouped commits. This should generally have a single commit for each affected package, a separate commit for chore: Update yarn.lock, conventional-commit messages, and a single implementation and test commit.'"* The directive bundles three disciplines (per-package grouping, lockfile separation, implementation+tests together) that the garden had as separate skills or norms; the retcon names the procedure that applies all three at once to a branch's full history.
## Pitfalls ## Pitfalls
- **Forgetting the pre-retcon tag.** Without it, step 5's no-net-change check has no reference. Tag before resetting. - **No pre-retcon tag:** the no-net-change check has nothing to compare
- **`git reset --hard` instead of `--mixed`.** `--hard` discards the working tree and loses the net diff. Use `--mixed` (or `--soft` if you want the index preserved) so the diff stays available as unstaged changes. against. Tag before you reset.
- **Lockfile interleaved with a package commit.** If `git add packages/foo/` accidentally stages a lockfile that lives under that package (rare; typically lockfiles are root-level), the lockfile leaks into the package commit. Run `git status` after each `git add` and verify only the intended package's files are staged. - **`git reset --hard` instead of `--mixed`:** it throws away the net
- **Implementation without tests, or tests without implementation.** The discipline is one commit per package containing both. A package commit with no test changes when the implementation is new behavior is wrong; either the tests live elsewhere (a cross-package test commit, separately) or the implementation is genuinely untested (a panel finding for the next round, not a retcon target). diff. Use `--mixed`, or `--soft` to keep the index.
- **Conflicting with concurrent fixer pushes.** A retcon force-pushes; if a fixer pushed since the agent's last fetch, `--force-with-lease` rejects the push. Do not override with `--force`; abort and rebase the retcon onto the new tip. - **Lockfile in a package commit:** `git add packages/foo/` can stage a
lockfile that lives under that package. Check `git status`.
- **Implementation without tests:** if a package commit adds behavior
and changes no tests, either the tests live in a separate cross-package
commit, or the code is untested. That's a review finding, not
something the retcon should fix.
- **The lease is rejected:** someone pushed since your last fetch. Don't
fall back to `--force`. Fetch, then redo the retcon on the new tip.
## Origin
Kris Kowal coined the verb: *"Please reset this branch and restage the
changes in sensibly grouped commits. This should generally have a single
commit for each affected package, a separate commit for chore: Update
yarn.lock, conventional-commit messages, and a single implementation and
test commit."* Adapted from
[the retcon skill in kriscendobot/garden](https://github.com/kriscendobot/garden/blob/aa94ebd0877a931ac67e29018b0fd4b62c257ece/skills/retcon/SKILL.md)
(May 2026).
+67
View File
@@ -0,0 +1,67 @@
---
name: run-confined
description: Run confined / sandbox an untrusted command — use for `curl ... | bash` installers, npm/npx/bunx of unvetted packages, or any third-party script that should not see ~/.ssh, ~/.config/gh, tokens, or the rest of $HOME; when someone says "run it confined", "sandbox this", or "don't pipe that into my shell". Runs the command in a transient systemd user unit whose only writable place is ~/.local.
---
# Run confined: untrusted code in a one-shot systemd user unit
[run-confined.sh](run-confined.sh) runs a command in a transient
`systemd-run --user` service with kernel-enforced sandboxing:
- `$HOME` is an unreadable tmpfs (`ProtectHome=yes`): no `~/.ssh`,
`~/.config/gh`, `.env` files, or other secrets.
- The only writable place is `/tmp/confined`, a bind mount of `~/.local`;
inside the unit `$HOME` points there. Whatever gets installed lands in
`~/.local` on the host.
- System dirs are read-only; `/tmp` and `/dev` are private.
- The calling shell's environment is not inherited (no `GH_TOKEN`,
`SSH_AUTH_SOCK`, ...). Network is allowed.
- Node (newest nvm v22, bound read-only) is on `PATH` when present.
Before running anything, the script does a wet test: if `~/.ssh` is still
visible from inside a probe unit, it aborts with exit 3 and runs nothing.
## Usage
```sh
.agents/skills/run-confined/run-confined.sh -- cmd [args...]
```
Arguments naming paths under `$HOME` or `~/.local` are rewritten to the
`/tmp/confined` equivalents; add `--preserve-paths` to opt out.
stdin, stdout, and stderr are piped through (`systemd-run -P`), so a
download-and-run installer works as a pipe. Fetch with `curl` on the host
(read it first), then run the interpreter confined. Pass environment
explicitly with `env`, since the unit doesn't inherit any:
```sh
curl -fsSL https://bun.com/install -o /tmp/bun-install.sh
less /tmp/bun-install.sh
.agents/skills/run-confined/run-confined.sh -- \
env BUN_INSTALL=/tmp/confined bash < /tmp/bun-install.sh
# -> ~/.local/bin/bun on the host
```
Point installers at `/tmp/confined` (e.g. `BUN_INSTALL`, `npm_config_prefix`,
which the script already sets), not `$HOME/.foo`. That way binaries land in
`~/.local/bin`, which is already on the host `PATH`. Rc-file edits an
installer makes go to `~/.local/.bashrc` and similar, where they have no
effect; check for them and delete them.
## When the wet test aborts
On Ubuntu 24.04, `kernel.apparmor_restrict_unprivileged_userns=1` (the
default) stops the systemd user manager from creating namespaces, and
systemd *silently* skips the sandbox. The script refuses rather than run
unconfined. The operator decides whether to lift it:
```sh
sudo sysctl kernel.apparmor_restrict_unprivileged_userns=0 # until reboot
```
Don't run the command unconfined as a fallback. Report the abort and
let the operator choose. The script header lists the full symptom set.
The canonical copy of this script is
`~/projects/office-admin/tools/run-confined.sh`.
+214
View File
@@ -0,0 +1,214 @@
#!/bin/bash
# Run a command inside a one-shot, kernel-confined systemd USER unit.
#
# Usage: run-confined.sh -- cmd [args...]
#
# Everything after the first "--" is executed in a transient user service
# with strong sandboxing. Inside the unit:
# * $HOME is an unreadable tmpfs - ~/.ssh, ~/.config/gh, CI secrets
# do not exist / cannot be read. Denial by kernel namespace.
# * the only writable place is /tmp/confined, which is a bind mount of
# ~/.local, and $HOME points there. Installs for this machine land in
# ~/.local and stay there.
# * system dirs are read-only; /dev and /tmp are private.
#
# The unit is started by the systemd user manager, so the calling shell's
# environment is NOT inherited (no GH_TOKEN, SSH_AUTH_SOCK, etc.) unless
# explicitly passed via Environment= below.
#
# Requires the user manager to be able to create mount/user namespaces.
# Ubuntu 24.04 ships kernel.apparmor_restrict_unprivileged_userns=1 which
# blocks that; run-confined refuses to run unless the sandbox is proven
# effective by a wet test, so it never silently runs unconfined.
#
# TROUBLESHOOTING
# --------------
# On Ubuntu 24.04 the AppArmor userns boundary (
# kernel.apparmor_restrict_unprivileged_userns=1 , set by default) stops
# the systemd user manager from creating namespaces. systemd then SILENTLY
# skips sandbox setup rather than failing the unit, so the command still
# "runs" while the sandbox is off. Symptoms:
# * wet-test abort with a leak report that lists the full $HOME (incl.
# ~/.ssh), instead of an empty / "Permission denied" home.
# * "UTS namespace setup is prohibited (container manager?), ignoring
# namespace setup." in `journalctl --user -b`.
# * units die at once with status=218/CAPABILITIES + journal line
# "Failed to drop capabilities: Operation not permitted" when any of
# PrivateDevices/ProtectClock/ProtectHostname/ProtectKernel* is set.
# * unrelated sandboxes (firejail, unshare -m, podman rootless) fail too.
# None of this means the box is broken; it is a deliberate AppArmor
# containment boundary. To use namespaced sandboxes again, either
# temporarily with:
# sudo sysctl kernel.apparmor_restrict_unprivileged_userns=0
# or persist it across reboots with:
# echo 'kernel.apparmor_restrict_unprivileged_userns=0' \
# | sudo tee /etc/sysctl.d/60-userns.conf
#
# Some hardening properties need capset() / extra namespaces and are not
# available on every kernel; each is probed and dropped with a note.
set -euo pipefail
usage() {
sed -n '3,9p' "$0"
exit 2
}
[[ $# -ge 1 && $1 == "--" ]] || usage
shift
[[ $# -ge 1 ]] || usage
carve="/tmp/confined"
writerowed="$HOME/.local"
wet_marker=".run-confined-wet"
# Command args that reference files under $HOME are normally tab-completed
# on the host, but inside the unit $HOME is blanked and the carve lives at
# $carve. Rewrite such args so you type the host path and the unit still
# finds it. Pass the literal directory with "--preserve-paths" to opt out.
preserve_paths=0
for a in "$@"; do
[ "$a" = "--preserve-paths" ] && preserve_paths=1
done
map_path() {
case "$1" in
"$writerowed"/*) printf '%s/%s' "$carve" "${1#"$writerowed"/}" ;;
"$writerowed") printf '%s' "$carve" ;;
"$HOME"/*) printf '%s/%s' "$carve" "${1#"$HOME"/}" ;;
"$HOME") printf '%s' "$carve" ;;
*) printf '%s' "$1" ;;
esac
}
cmd=()
for a in "$@"; do
if [ "$a" = "--preserve-paths" ]; then
continue
elif [ "$preserve_paths" -eq 1 ]; then
cmd+=("$a")
else
cmd+=("$(map_path "$a")")
fi
done
[[ ${#cmd[@]} -ge 1 ]] || usage
# Node for the unit. No downloads, no checksum theater: the newest nvm
# v22 install is bound in READ-ONLY (so the unit gets node+npm but cannot
# tamper with ~/.nvm). Override with CONFINED_NODE=/path/to/node-dir (the
# dir that has bin/node). A standalone carve node is preferred if present.
unit_path="/usr/local/bin:/usr/bin:/bin"
bind_node_dir=""
node_target="/tmp/confined-node"
if [[ -x "$HOME/.local/share/pi-node/current/bin/node" ]]; then
unit_path="$HOME/.local/share/pi-node/current/bin:$unit_path"
elif [[ -n "${CONFINED_NODE:-}" && -x "$CONFINED_NODE/bin/node" ]]; then
bind_node_dir="$CONFINED_NODE"
unit_path="$node_target/bin:$unit_path"
else
newest_bin=$(ls -d "$HOME"/.nvm/versions/node/v22.*/bin 2>/dev/null | sort -V | tail -1)
if [[ -n "$newest_bin" && -x "$newest_bin/node" ]]; then
bind_node_dir="${newest_bin%/bin}"
unit_path="$node_target/bin:$unit_path"
fi
fi
base_props=(
"ProtectSystem=strict"
"ProtectHome=yes"
"BindPaths=$writerowed:$carve"
"PrivateTmp=yes"
"PrivateMounts=yes"
"ProtectKernelTunables=yes"
"ProtectControlGroups=yes"
"RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK"
"RestrictRealtime=yes"
"RestrictSUIDSGID=yes"
"LockPersonality=yes"
"NoNewPrivileges=yes"
"MemoryMax=1G"
"TasksMax=200"
"TimeoutStartSec=infinity"
)
# capset-dependent; known to fail under some kernels -> probe and drop.
optional_props=(
"PrivateDevices=yes"
"ProtectClock=yes"
"ProtectHostname=yes"
"ProtectKernelModules=yes"
"ProtectKernelLogs=yes"
)
probe_pass() {
timeout 10 systemd-run --user --wait -q --property="$1" -- /bin/true 2>/dev/null
}
# A real sandbox blanks $HOME (unreadable), exposes only the carve, and
# allows writes there. Concretely: $HOME/.ssh must not be listable, and a
# touch/rm round-trip must work inside the carve. Anything else means
# systemd silently skipped the namespace setup -> not confined.
sandbox_wet_test() {
timeout 20 systemd-run --user --wait -q \
--property="ProtectHome=yes" \
--property="BindPaths=$writerowed:$carve" \
--property="PrivateTmp=yes" \
--property="Environment=HOME=$carve" \
-- /bin/sh -c "ls -d \"\$HOME/.ssh\" >/dev/null 2>&1 && exit 1; touch \"$carve/$wet_marker\" && rm \"$carve/$wet_marker\"" 2>/dev/null
}
# Show what leaked, to diagnose an ineffective sandbox.
sandbox_leak_report() {
timeout 20 systemd-run --user --wait -P \
--property="ProtectHome=yes" \
--property="BindPaths=$writerowed:$carve" \
--property="PrivateTmp=yes" \
--property="Environment=HOME=$carve" \
-- /bin/sh -c 'echo "HOME listing (expect Permission denied):"; ls -A "$HOME" 2>&1; echo; echo ".ssh (expect cannot access):"; ls -d "$HOME/.ssh" 2>&1; echo; echo "carve:"; ls "$carve" 2>&1 | head -5' 2>/dev/null
}
props=("${base_props[@]}")
if [[ -n "$bind_node_dir" ]]; then
props+=("BindReadOnlyPaths=$bind_node_dir:$node_target")
fi
for p in "${optional_props[@]}"; do
if probe_pass "$p"; then
props+=("$p")
else
echo "note: dropping unsupported hardening property: $p" >&2
fi
done
if ! sandbox_wet_test; then
echo "abort: sandbox is not actually enforced on this kernel/session" >&2
echo " ~/.ssh must not exist inside the transient unit and the carve" >&2
echo " write must succeed; the wet test failed, so NOTHING ran." >&2
echo " This is the apparmor_restrict_unprivileged_userns=1 symptom on" >&2
echo " Ubuntu 24.04 (see the header for the full symptom list). Fix:" >&2
echo " sudo sysctl kernel.apparmor_restrict_unprivileged_userns=0" >&2
echo " or persist: echo 'kernel.apparmor_restrict_unprivileged_userns=0' |" >&2
echo " sudo tee /etc/sysctl.d/60-userns.conf" >&2
echo "Leak report from the probe unit follows:" >&2
sandbox_leak_report >&2 || true
exit 3
fi
unit_name="run-confined-$(date +%s%N)"
systemd_run_args=(
--user --wait -P
--unit="$unit_name"
--property="Type=oneshot"
--property="WorkingDirectory=$carve"
--property="Environment=HOME=$carve"
--property="Environment=PATH=$unit_path"
--property="Environment=TMPDIR=${TMPDIR:-/tmp}"
--property="Environment=TERM=vt100"
--property="Environment=npm_config_prefix=$carve"
--property="Environment=npm_config_cache=$carve/share/node/npm-cache"
)
for p in "${props[@]}"; do
systemd_run_args+=(--property="$p")
done
systemd-run "${systemd_run_args[@]}" -- "${cmd[@]}"
+37
View File
@@ -0,0 +1,37 @@
---
name: silent-failures-are-evil
description: >-
No silent failures — use when writing or reviewing error handling: catch blocks, promise rejections, fallbacks for unsupported features, or any branch that treats a result as "didn't work"; when a UI shows a failure with nothing logged; when someone says "silent failure". Every failure path leaves a trace in the logs, and no failure is invented without checking what the API actually returns.
---
# Silent failures are evil
A failure nobody can see is a failure nobody can fix. Every failure path
must leave a trace.
## the rules
- **Log every failure path**: a caught exception, a rejected promise, a
falsy result treated as "didn't work", an unsupported-feature
fallback. Changing a label on screen is not enough — log it, with an
`event` tag for grepping, and no secrets (no keys, cookies, header or
param values).
- **Don't invent failures.** Before branching on a result, check what
the API actually returns (the spec or MDN, not a guess). A success
misreported as failure is as bad as a failure reported as nothing.
- **Say only what you know.** When an API can't tell you the outcome,
report what it did tell you ("offered", not "saved").
- **Make it testable.** Pass the logger in (see
[no-ambient-authority](../no-ambient-authority/SKILL.md)) so a test can assert
that the failure path logged; a discovered silent failure is a goof
that owes a test ([lazy-tdd](../lazy-tdd/SKILL.md)).
## the goof that prompted this
diigo-cache's password-manager save button said "not saved", with
nothing in the console. The code read `c ? "saved" : "not saved"` on the
result of [`CredentialsContainer.store()`](https://developer.mozilla.org/en-US/docs/Web/API/CredentialsContainer/store),
which resolves with `undefined` per spec — even on success. So every
save was reported as a failure, silently. The fix
(`capsite/diigo-cache/src/pm-save.ts`) reports "offered", logs every
outcome, and is covered by `tests/pm-save.test.ts`.
+3 -4
View File
@@ -1,6 +1,6 @@
--- ---
name: watchme name: watchme
description: Use when the operator says "watch me", "losing", or "winning?" about a session recorded with `script -f xyz.script` — the typescript is shared ground truth. Unless otherwise stated, the newest `.tmp/*.script` is the file to look at. For "losing", read the tail for what was done and what the machine said, diagnosing from evidence instead of asking for a re-telling. For "winning?", answer one of exactly "Yes, that's as expected; next, do X." or "Yes, as expected. Done!" — no partial verdicts. description: Watch me — terminal / shell session recorded with `script -f` (a typescript, `.tmp/*.script`) as shared ground truth. Use when the operator says "watch me", "losing", or "winning?"; when diagnosing a shell session from its recorded transcript instead of asking for a re-telling; when asked how to record a terminal session.
--- ---
# Watch Me: a typescript as shared ground truth # Watch Me: a typescript as shared ground truth
@@ -60,8 +60,7 @@ Script done on 2026-09-02 23:48:46-05:00 [COMMAND_EXIT_CODE="0"]
``` ```
The header records when, on which TTY, and the terminal geometry; the footer The header records when, on which TTY, and the terminal geometry; the footer
records the exit code. `watchme` is a short specimen — the smallest useful records the exit code.
capture, including the escape noise described below.
For a longer session the value is that one file holds both halves of the For a longer session the value is that one file holds both halves of the
story: the operator's commands and the machine's responses, interleaved in story: the operator's commands and the machine's responses, interleaved in
@@ -69,7 +68,7 @@ order. `tail` it while it is live; `grep` it after the fact.
## Relationship to the lab notebook ## Relationship to the lab notebook
The typescript is raw evidence; the [lab notebook](../lab-notebook-template.md) The typescript is raw evidence; a lab notebook
is interpretation. Read the script to establish what happened, then promote is interpretation. Read the script to establish what happened, then promote
the finding — hypothesis, evidence, next question — into a notebook entry so the finding — hypothesis, evidence, next question — into a notebook entry so
it survives independently of the transcript. it survives independently of the transcript.
+75
View File
@@ -0,0 +1,75 @@
---
name: what-was-i-doing
description: What was I doing on <date>? Reconstruct past activity on this machine — use when the operator asks what they were doing on a day, where a stray file / install / symlink came from, or "why is this here?"; when a timestamp (ls, stat, cache dir) is the only clue. Correlates file mtimes with git commits and reflogs across every repo `locate` finds.
---
# What was I doing? Timestamps as a diary
The machine already keeps a diary; it's just scattered. A mystery artifact
(a stray `~/.bun`, an odd symlink, a cache dir) carries an mtime. Line that
time up against git activity across all repos, and the project usually
falls out within a few minutes.
## 1. Pin the clue's timestamp to the minute
```sh
ls -la --time-style=+'%F %H:%M' ~/.bun ~/.bun/* ~/.local/bin/bunx
```
Look at the artifact *and its neighbors*: the directory, its children, and
anything that points at it (symlink targets, `readlink`). The newest mtime
inside a cache says when it was last used. Caches also say *what* was
used: `ls ~/.bun/install/cache`, `~/.npm/_cacache`, `~/.cache/pip`.
A cache full of `@vitejs`, `@reduxjs`, `@tailwindcss` means a frontend
build.
## 2. Sweep every repo for that day
[day-in-repos.sh](day-in-repos.sh) lists commits *and reflog entries* for
one day across every repo in the `locate` database. Reflog is the important
half: pulls, checkouts, rebases, and amends are local-only, never show in
`git log`, and are often exactly what you did that day.
```sh
.agents/skills/what-was-i-doing/day-in-repos.sh 2026-05-25 | grep -v heartbeat
.agents/skills/what-was-i-doing/day-in-repos.sh 2026-05-25 --author=dckc
```
- Extra args go to `git log`. `--author` cuts upstream and bot commits, but
reflog rows are never filtered. They're always yours.
- A commit shared by several clones/worktrees prints once, listing them all.
- Reflog begins at `clone: from <url>` and is pruned by `git gc` (default
90 days, 30 for unreachable), so older days may be patchy. The mtime of
`.git/FETCH_HEAD` gives the last fetch even when the reflog doesn't.
- The `locate` DB is rebuilt nightly. Repos created today are missing;
`sudo updatedb` if that matters.
- Filter noise with `grep -v`. Bot heartbeats and dependabot bumps
flood some repos.
## 3. Correlate, then confirm in the candidate repo
Look for repo activity within minutes of the artifact's mtime. Then confirm
inside that repo: does it reference the tool (`git grep -i bun`, CI
workflows, lockfiles), and do its build outputs (`node_modules`, `dist`)
carry matching mtimes?
Report it as a timeline table: time, event, source. Say which links
are evidence and which are inference ("probably linked by hand").
## Other diaries, when git is quiet
- `journalctl --user --since ... --until ...`: user units, systemd-run.
- Shell history: `~/.bash_history` has timestamps only if `HISTTIMEFORMAT`
was set; `.tmp/*.script` typescripts (see the watchme skill).
- File mtimes broadly: `find ~ -xdev -newermt 2026-05-25 ! -newermt
2026-05-26 -not -path '*/node_modules/*' -not -path '*/.cache/*'`
([fselect](https://github.com/jhspetersson/fselect) does the same in SQL,
if installed).
## Worked example
`~/.local/bin/bunx` → `~/.bun/bin/bun` (1.3.14), both 2026-05-25 12:27.
The sweep showed `projects/pinchtab` reflog: pull 12:19, forced-update
12:35, checkout `v0.13.0` 12:36. `pinchtab/dashboard/` has a `bun.lock`,
CI runs `bun install`, and `dashboard/node_modules` was created at 12:28.
Conclusion: bun was installed to build pinchtab's dashboard for v0.13.0.
+52
View File
@@ -0,0 +1,52 @@
#!/bin/bash
# List git activity on one day across every repo `locate` knows about.
#
# Usage: day-in-repos.sh YYYY-MM-DD [git-log-args...]
#
# Prints commits (by committer date) and reflog entries (pulls, checkouts,
# amends: local-only events that `git log` never shows), sorted by local
# time. A commit present in several clones/worktrees is printed once, with
# every repo it appears in. Extra args go to `git log`, e.g. --author=dckc
# to drop bot and upstream commits.
set -euo pipefail
[[ $# -ge 1 && $1 =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]] || {
sed -n '2,10p' "$0"
exit 2
}
day=$1
shift
next=$(date -d "$day + 1 day" +%F)
locate -r '/\.git$' \
| grep "^$HOME/" \
| grep -v -e /node_modules/ -e /.cache/ -e /.cargo/ -e /go/pkg/ \
| while read -r g; do
r=${g%/.git}
name=${r#"$HOME"/}
git -C "$r" log --all --since="$day 00:00" --until="$next 00:00" \
--date=format:%H:%M --format="%cd %h $name %s" "$@" 2>/dev/null || true
git -C "$r" reflog --all --date=format:'%F %H:%M' \
--format="%gd $name %gs" 2>/dev/null \
| awk -F'\t' -v day="$day" '
match($1, /\{[0-9-]+ [0-9:]+\}/) {
ts = substr($1, RSTART + 1, RLENGTH - 2)
if (substr(ts, 1, 10) == day)
printf "%s\treflog\t%s\t%s\n", substr(ts, 12), $2, $3
}' || true
done \
| awk -F'\t' '
# group by time+hash (reflog rows by time+subject)
{ key = ($2 == "reflog") ? $1 FS $4 : $1 FS $2
if (!(key in repos)) { order[++n] = key; line[key] = $0 }
if (!((key, $3) in seen)) {
seen[key, $3] = 1
repos[key] = repos[key] (repos[key] ? " " : "") $3
} }
END {
for (i = 1; i <= n; i++) {
split(line[order[i]], f, FS)
printf "%s %-7s %s [%s]\n", f[1], f[2], f[4], repos[order[i]]
}
}' \
| sort
+113
View File
@@ -0,0 +1,113 @@
---
name: wiring-diagram
description: >-
Capability wiring diagram, "who holds what" — use when asked for a wiring diagram, a picture of who holds what, a capability diagram, or "a diagram like the cookie-rotation one"; when reviewing a design for POLA and wanting to see the reference graph. MarkM ERTP-slide style: actors are balls, makers pale regions, objects rounded squares, state ledger tables, references dot-to-arrowhead; HTML/SVG plus a headless-Chromium PNG. Not for message order over time: that's a sequence diagram (sequence-diagram-actor-sim in agoric-sdk).
---
# Capability wiring diagram
One picture that answers, for a design: **who holds a reference to what,
and therefore who can do what.** It complements a sequence diagram,
which shows *when* messages flow. This one shows the standing graph of
authority that makes those messages possible. For sequences, see
`~/projects/ymax-workspace/agoric-sdk/.agents/skills/sequence-diagram-actor-sim/SKILL.md`
(on agoric-sdk branches `connolly/ago-1264-agentic-planning` and
`connolly/ago-1299-agentic-planning-tdd`, not yet on master).
Exemplars:
- `~/oros-bots/capsite-cookie-rotation/docs/cookie-rotation-wiring.html`
(+ `.png`): the original.
- `~/qtrx/packages/umb-download/docs/wiring.html` (+ `.png`): three
makers, two outside parties, and an adversary holding a webkey.
Start from [`template.html`](template.html), next to this file. It has
the drawing helpers and a small example scene to replace.
## Read the graph from the code, not the story
Every arrow must correspond to something real: a constructor argument, a
stored id, a binding, a held token, or a file someone keeps. Before
drawing, list the references as `holder → target: where in the code`.
Read the factories (`make…`, `newX`, `configure`) and anything that
stores an id: a DO id held as a string *is* a reference. If you can't
cite where a reference comes from, it doesn't get an arrow.
Watch for references that are dropped on purpose (e.g. a setup route
that mints an instance, hands its id to one holder and forgets it).
That's often the point of the design. Say so in a caption, or just
leave the arrow out.
## Vocabulary
| element | helper | means |
|---|---|---|
| blue ball | `actor(cx, cy, label)` | a person or a program with its own agency |
| grey ball | `actor(cx, cy, label, "url(#ext)")` | outside the system: a browser extension, a third-party service |
| pale region | `maker(x, y, w, h, "makeFoo")` | a maker (factory, DO class) and the instances it made; the stacked inner panes say "there are many" |
| rounded square | `obj(x, y, w, h, label, stacked)` | an object or facet; `stacked` when one is made per instance |
| ledger table | `table(x, y, title, rows, [colW0, colW1])` | an object's state; rows are key/value; `rowY(i)` and `colX` give arrow anchors |
| dot → arrowhead | `ref(x1, y1, x2, y2, bend)` | holder references target; `bend` (±0.05–0.3) curves around things |
| speech bubble | the `path` + `code` block | the one call the picture is about, in `E(target).method(args)` form |
| caption | `cap(x, y, text, attrs)` | what an actor holds, and what it can't reach |
Facets are separate objects (`store` vs `readOnly`): drawing them apart
is how the picture shows attenuation.
## Include an adversary
Add a Mallory who holds whatever is most likely to leak (an instance
id, a webkey, a URL), with an arrow to exactly what it reaches. The
caption says what she *can't* reach. Absent arrows are the security
argument, and Mallory makes them visible.
## Layout
- Actors in a row along the top (y≈200), makers below (y≈320 onward),
captions along the bottom (y≈ height − 14).
- Put each maker under the actors that mostly use it, so arrows run
short and downward.
- Speech bubble at the top, its tail pointing at the speaker. Move the
bubble's path (`M x,y`) and the tail segment (`h… l-38,48 l6,-48`)
together.
- Captions: two or three short lines, never on top of an arrow or a
ball. Use `"text-anchor": "start"` to set one beside a ball.
- Long arrows that cross a maker: increase `|bend|` until the curve
clears tables and objects. The control point is the midpoint offset
by `bend × (−dy, dx)`.
## Accessibility
Keep `<title>` short. Write `<desc>` as prose that names every actor and
maker, every reference, the bubble's call, and what the adversary
can't reach. Someone who can't see the picture should get the same
argument from it.
## Render and check
The page is self-contained (no network). Render a PNG with headless
Chromium. The viewport comes out shorter than `--window-size`, so render
taller and crop to the SVG size:
```sh
chromium --headless=new --disable-gpu --no-sandbox --hide-scrollbars \
--window-size=W,$((H+100)) --screenshot=full.png "file://$PWD/NAME.html"
python3 -c "from PIL import Image; Image.open('full.png').crop((0,0,W,H)).save('NAME.png')" && rm full.png
```
Then **look at the PNG** (Read it) and fix overlaps: captions on arrows,
arrows through tables, text clipped at the bottom. Expect two or three
rounds. Commit the HTML (source) and the PNG separately, so the source
diff stays readable.
## Review checklist
- Does every arrow cite something in the code? Is any real reference
missing?
- Does each actor hold only what its job needs (POLA)? If the picture
shows an actor holding more, that's a finding about the design, not a
drawing problem.
- Are facets drawn apart where they differ in authority?
- Does the adversary's caption match what the arrows show?
- Does `<desc>` tell the same story in prose?
- Is any secret value drawn? Elide values (`session=…`, `{…}`): the
picture shows *that* a row exists, not its contents.
+138
View File
@@ -0,0 +1,138 @@
<!doctype html>
<html><head><meta charset="utf-8">
<title>TITLE: who holds what</title>
<!-- drawn after the style of MarkM's ERTP slides; NAME.png is a headless-Chromium screenshot of this page -->
<style>
html, body { margin: 0; background: #fff; }
.sans { font-family: "Helvetica Neue", "DejaVu Sans", sans-serif; }
.obj { font-family: "DejaVu Serif", serif; font-style: italic; }
.mono { font-family: "DejaVu Sans Mono", monospace; }
</style></head>
<body>
<svg id="c" xmlns="http://www.w3.org/2000/svg" width="1120" height="820" viewBox="0 0 1120 820"
role="img" aria-labelledby="title desc">
<title id="title">TITLE: who holds what</title>
<desc id="desc">Prose for screen readers: name every actor and maker, every
reference (who holds what), the speech bubble's call, and what the
adversary can and cannot reach.</desc>
<defs>
<radialGradient id="ball" cx="40%" cy="35%" r="70%">
<stop offset="0%" stop-color="#7cc8f8"/>
<stop offset="100%" stop-color="#3aa6ee"/>
</radialGradient>
<radialGradient id="ext" cx="40%" cy="35%" r="70%">
<stop offset="0%" stop-color="#e3e3e3"/>
<stop offset="100%" stop-color="#b9b9b9"/>
</radialGradient>
<linearGradient id="objfill" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#6cbcf4"/>
<stop offset="100%" stop-color="#4aa8ec"/>
</linearGradient>
<marker id="head" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="9" markerHeight="9" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill="#1d4f7a"/>
</marker>
</defs>
</svg>
<script>
const svg = document.getElementById("c");
const NS = "http://www.w3.org/2000/svg";
const el = (name, attrs, parent = svg) => {
const e = document.createElementNS(NS, name);
for (const [k, v] of Object.entries(attrs)) e.setAttribute(k, v);
parent.appendChild(e);
return e;
};
const text = (x, y, s, cls, attrs = {}) => {
const t = el("text", { x, y, class: cls, "text-anchor": "middle", fill: "#1d3550", ...attrs });
t.textContent = s;
return t;
};
const INK = "#1d4f7a";
const dot = (x, y) => el("circle", { cx: x, cy: y, r: 4.5, fill: INK });
/** a reference: dot at the holder, arrowhead at the target */
const ref = (x1, y1, x2, y2, bend = 0, extra = {}) => {
dot(x1, y1);
const mx = (x1 + x2) / 2 - (y2 - y1) * bend, my = (y1 + y2) / 2 + (x2 - x1) * bend;
el("path", { d: `M${x1},${y1} Q${mx},${my} ${x2},${y2}`, fill: "none", stroke: INK, "stroke-width": 2, "marker-end": "url(#head)", ...extra });
};
// maker box: a pale rounded region, with stacked instances inside
function maker(x, y, w, h, label) {
el("rect", { x, y, width: w, height: h, rx: 26, fill: "#d6ecfb", stroke: "#b5dbf5", "stroke-width": 1.5 });
text(x + 18, y + 30, label, "obj", { "text-anchor": "start", "font-size": 20 });
for (let i = 2; i >= 0; i -= 1) {
el("rect", {
x: x + 18 + i * 9, y: y + 48 + i * 9, width: w - 52, height: h - 82, rx: 22,
fill: "#c8e5fa", stroke: "#8ec9f2", "stroke-width": 1.5, "fill-opacity": 0.9,
});
}
}
// an object: rounded square, optionally stacked (several made)
function obj(x, y, w, h, label, stacked = false) {
const n = stacked ? 3 : 1;
for (let i = n - 1; i >= 0; i -= 1) {
el("rect", { x: x + i * 7, y: y - i * 7, width: w, height: h, rx: 12, fill: "url(#objfill)", stroke: "#2f8fd6", "stroke-width": 1.5 });
}
text(x + w / 2, y + h / 2 + 7, label, "obj", { "font-size": 19 });
}
function actor(cx, cy, label, fill = "url(#ball)") {
el("circle", { cx, cy, r: 52, fill, stroke: "#2f8fd6", "stroke-width": 1.5 });
text(cx, cy + 6, label, "sans", { "font-size": label.length > 8 ? 15 : 17 });
}
// a table like the ERTP ledger
function table(x, y, title, rows, colW) {
const rowH = 28, w = colW[0] + colW[1];
el("rect", { x, y, width: w, height: 36 + rowH * rows.length, fill: "#5fb5f2", stroke: "#2f8fd6", "stroke-width": 1.5 });
text(x + w / 2, y + 25, title, "obj", { "font-size": 18 });
rows.forEach(([k, v], i) => {
const ry = y + 36 + i * rowH;
el("line", { x1: x, y1: ry, x2: x + w, y2: ry, stroke: "#2f8fd6", "stroke-width": 1.2 });
text(x + 10, ry + 19, k, "mono", { "text-anchor": "start", "font-size": 14 });
if (v) text(x + colW[0] + 10, ry + 19, v, "mono", { "text-anchor": "start", "font-size": 14 });
});
el("line", { x1: x + colW[0], y1: y + 36, x2: x + colW[0], y2: y + 36 + rowH * rows.length, stroke: "#1d4f7a", "stroke-width": 1.5 });
return { rowY: (i) => y + 36 + i * rowH + rowH / 2, x, w, colX: x + colW[0] };
}
// ---- scene: replace from here down ----
// speech bubble: the one invocation the picture is about; tail toward the speaker
el("path", {
d: "M30,22 h470 a16,16 0 0 1 16,16 v34 a16,16 0 0 1 -16,16 h-330 l-38,48 l6,-48 h-102 a16,16 0 0 1 -16,-16 v-34 a16,16 0 0 1 16,-16 z",
fill: "#d9d9d9", stroke: "#555", "stroke-width": 2,
});
const code = el("text", { x: 46, y: 62, class: "mono", "font-size": 17, fill: "#222" });
code.innerHTML = '<tspan fill="#b0177b">await</tspan> E(store).set(<tspan fill="#1a7f37">"cookie"</tspan>, freshCookie);';
// actors: people, programs, outside services (grey: "url(#ext)")
actor(150, 200, "Alice");
actor(560, 200, "client");
actor(930, 200, "Mallory");
// makers, with what they made inside
maker(30, 320, 470, 470, "makeMapStore");
maker(560, 320, 530, 470, "makeWebApp");
obj(80, 420, 120, 80, "store");
obj(80, 600, 120, 80, "readOnly", true);
const kv = table(260, 430, "kv", [["cookie", "session=…"], ["", ""], ["", ""]], [90, 110]);
obj(620, 560, 140, 90, "access", true);
const init = table(830, 420, "init", [["url", "…"], ["headerStore", ""]], [130, 85]);
// references: holder first; one comment per arrow, in the form "holder → target"
ref(140, 228, 125, 418, 0.05); // Alice → store
ref(172, 224, 508, 210, 0.04); // Alice → client (she wired it)
ref(554, 218, 660, 553, 0.12); // client → access
ref(916, 220, 720, 553, -0.08); // Mallory → access
ref(160, 470, kv.x - 2, kv.rowY(0), 0.05); // store → kv
ref(150, 666, kv.x - 2, kv.rowY(0) + 6, -0.25); // readOnly → kv
ref(760, 600, init.x - 2, init.rowY(1), -0.1); // access → init
ref(init.colX + 40, init.rowY(1), 208, 640, -0.28); // init.headerStore → readOnly
// captions: what each actor holds, what the adversary can't reach
const cap = (x, y, s, attrs = {}) => text(x, y, s, "sans", { "font-size": 14, fill: "#33536f", ...attrs });
cap(930, 118, "holds the instance id:");
cap(930, 136, "uses the cookie, can't reach store");
cap(265, 806, "shapes: keyShape \"cookie\", valueShape M.string()");
</script>
</body></html>
+46 -27
View File
@@ -1,51 +1,70 @@
--- ---
created: 2026-05-13 name: yarn-lock-separate-commit
updated: 2026-05-17 description: >-
author: liaison, gardener yarn.lock in its own commit — use when changing Yarn dependencies (package.json deps, workspace renames, yarn install), when organizing or reviewing commits that touch yarn.lock, when a rebase conflicts on yarn.lock, or during a retcon. Every lockfile change ships alone as `chore: Update yarn.lock`, after the package.json commit that caused it.
--- ---
# Skill: yarn-lock-separate-commit # yarn.lock in a separate commit
Adopted from `references/endo-but-for-bots/skills/yarn-lock-separate-commit.md`. Every change that touches `yarn.lock` ships in its own commit, with the
message `chore: Update yarn.lock`. It stays separate from the
Every change that touches `yarn.lock` ships in its own commit, separate from the `package.json` change that caused the update, with the message `chore: Update yarn.lock`. `package.json` change that caused it.
## Why ## Why
A separate lockfile commit can be dropped and regenerated cleanly on rebase. A combined commit turns lockfile churn into merge conflicts that have to be resolved by re-running `yarn install` and hand-stitching, which is error-prone and pollutes history. A separate lockfile commit can be dropped on a rebase and cleanly
regenerated. A combined commit turns lockfile churn into merge
conflicts, and those get resolved by re-running `yarn install` and
stitching by hand. That's error-prone and clutters the history.
## Procedure ## Procedure
After making the dep change in `package.json`: After the dependency change in `package.json`:
```sh ```sh
git add packages/foo/package.json git add packages/foo/package.json
git commit -m "feat(foo): add bar dependency (#NNNN)" git commit -m "feat(foo): add bar dependency"
npx corepack yarn install npx corepack yarn install
git add yarn.lock git add yarn.lock
git commit -m "chore: Update yarn.lock" git commit -m "chore: Update yarn.lock"
``` ```
When force-pushing a rebased branch, the order of the two commits matters: put the lockfile commit *after* the package.json commit, so a reviewer dropping the lockfile commit lands at a still-coherent state. Order matters. Put the lockfile commit **after** the `package.json`
commit, so dropping the lockfile commit still leaves a coherent state.
## Rebasing past a lockfile conflict
If another PR changed dependencies in the meantime, rebasing onto the
moved base usually conflicts on `yarn.lock`. Don't hand-merge it. Drop
the old lockfile commit and regenerate the lockfile:
```sh
git rebase origin/<base>
# conflict on yarn.lock
git rebase --skip # drop the old lockfile commit
# ... any other fixes on top ...
npx corepack yarn install # regenerate against the new base
git add yarn.lock
git commit -m "chore: Update yarn.lock"
```
## Pitfalls ## Pitfalls
- **`git commit --amend` on the package.json commit silently drags the lockfile in if staged.** Stage and commit the lockfile last. - **`git commit --amend` on the `package.json` commit** quietly pulls
- **Accidental combining.** If caught on review, `git reset HEAD~1`, re-stage selectively, and recommit as two. the lockfile in if it's staged. Stage and commit the lockfile last.
- **Package renames invalidate the lockfile.** Renaming a workspace's directory or `package.json:name` changes the workspace key (`"@scope/<name>@workspace:packages/<dir>"`) inside `yarn.lock`. Still ships in its own `chore: Update yarn.lock` commit, regenerated by re-running `npx corepack yarn install` after all renames are in place. Don't hand-edit the workspace key; the install produces the canonical form. - **Combined by accident:** run `git reset HEAD~1`, restage selectively,
and recommit as two commits.
- **Package renames change the lockfile.** Renaming a workspace's
directory or its `package.json` `name` changes the workspace key
(`"@scope/<name>@workspace:packages/<dir>"`) in `yarn.lock`. Once all
the renames are in, regenerate with `npx corepack yarn install`.
Commit the result on its own as usual, and don't hand-edit the key.
## Notes from the field ## See also
- _2026-05-13_: adopted from the reference. Session example from PR 71 lives in the journal, not here. - [Retcon](../retcon/SKILL.md) applies this rule to a whole branch's
- _2026-05-17_: a PR carrying `chore: Update yarn.lock` typically conflicts on the lockfile when rebased onto a moved base (any other PR that touched dependencies has landed in the interim). The workflow that resolved it cleanly: skip the old lockfile commit during rebase, apply any review-feedback fixes on top, then regenerate the lockfile against the new base as its own commit. Source: PR #238 fixer, `journal/entries/2026/05/17/200805Z-message-fixer-3e9fd0.md`. history after the fact.
- Adapted from
```sh [the yarn-lock-separate-commit skill in kriscendobot/garden](https://github.com/kriscendobot/garden/blob/f6fd8db6abe012366aa4c843874c07adae46e7f7/skills/yarn-lock-separate-commit/SKILL.md)
git rebase origin/<base> (May 2026).
# conflict on yarn.lock
git rebase --skip # drop the old lockfile commit
# apply review-feedback fixes on top
npx corepack yarn install # regenerate lockfile against new base
git add yarn.lock
git commit -m "chore: Update yarn.lock"
```