Compare commits

...
14 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
11 changed files with 570 additions and 6 deletions

No files matched your search

+21 -3
View File
@@ -22,8 +22,9 @@ Code](https://code.claude.com/docs/en/skills) and
failure path leaves a trace in the logs. failure path leaves a trace in the logs.
- [lazy-tdd](lazy-tdd/SKILL.md) — works the first time? fine. Goofed? - [lazy-tdd](lazy-tdd/SKILL.md) — works the first time? fine. Goofed?
You owe a test. You owe a test.
- [docs-files-api](docs-files-api/SKILL.md) — exported API docs over - [docs-files-api](docs-files-api/SKILL.md) — README sells and gets
file docs. Avoid chit-chat. you to TADA; CONTRIBUTING has the rest; AGENTS.md links to it.
Exported API docs over file docs. Avoid chit-chat.
## commits and PRs ## commits and PRs
@@ -36,22 +37,39 @@ Code](https://code.claude.com/docs/en/skills) and
## writing prose ## writing prose
- [stop-blathering](stop-blathering/SKILL.md) — omit needless words. - [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 - [hypertext-links](hypertext-links/SKILL.md) — link text says where
the link goes; no bare URLs, no "click here". the link goes; no bare URLs, no "click here".
- [citation-needed](citation-needed/SKILL.md) — back a challenged claim - [citation-needed](citation-needed/SKILL.md) — back a challenged claim
with live links, quoted and dated. 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: - [ibis-design](ibis-design/SKILL.md) — search a design space:
issues, positions, and arguments. 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 ## 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`; - [watchme](watchme/SKILL.md) — I record the session with `script -f`;
read the typescript instead of asking me what happened. read the typescript instead of asking me what happened.
- [run-confined](run-confined/SKILL.md) — run an untrusted installer in - [run-confined](run-confined/SKILL.md) — run an untrusted installer in
a throwaway systemd user unit that can't see `~/.ssh` and friends. 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 - [what-was-i-doing](what-was-i-doing/SKILL.md) — where did this file
come from? Correlate mtimes with git history across every repo. 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 ## adding one
+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.
+29 -1
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 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). 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)):
@@ -1,7 +1,7 @@
--- ---
name: stop-blathering name: grice-maxims
description: >- description: >-
Stop blathering; be concise, truthful, relevant, clear (Grice's maxims) — use when writing or copy editing prose for a reader (docs, READMEs, skills, commit messages, PR descriptions, replies); when someone says "tighter", "too wordy", "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. 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 # Be concise; optimize for reader attention
+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.
+23
View File
@@ -54,6 +54,23 @@ leading symbol on each item says what kind of element it is.
that's what keeps fallbacks easy to agree on. that's what keeps fallbacks easy to agree on.
6. **When one issue is really several choices, say so** with a `.` 6. **When one issue is really several choices, say so** with a `.`
comment and split it into sibling `?` sub-issues. 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 ## writing it in markdown
@@ -62,6 +79,12 @@ auto-bulleting. Put each symbol after the list marker, escaping where
needed (`- \- con`, `- \+ pro`), or use a fenced block with indentation needed (`- \- con`, `- \+ pro`), or use a fenced block with indentation
as in the example below. 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 ## example
``` ```
+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.
+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>