Structured briefs, both directions
sirdocalot
An agent working on your behalf hits two walls that are really the same wall: it has produced something too long for you to read, or it needs a decision only you can make. Both are documents. Both go here.
Two directions, one mechanism
Summarize out
The agent finished something long. It emits a brief, and the operator gets a page readable in a minute instead of a transcript nobody opens.
Capture in
The agent is blocked on a human. It asks one person, or five stakeholders, a structured question, and reads back one coalesced answer.
The surface is a schema, not a framework
Agents are slow and expensive at rich output because they author bespoke HTML every time — the same table, the same callout, rebuilt from nothing, billed by the token. So there is nothing here to program against. An agent names a widget and passes props:
{ "widget": "decision-matrix", "props": { ... } }
A whole brief is that a few times over, plus who should see it and when it stops collecting:
POST /api/briefs
{
"title": "Datastore for the ledger service",
"intent": { "purpose": "I need a call before I write the migration" },
"participants": [{ "name": "Priya" }, { "name": "Sam" }],
"policy": { "closeWhen": { "kind": "all" }, "visibility": "blind" },
"blocks": [
{ "widget": "decision-matrix", "props": {
"title": "Postgres or SQLite",
"columns": ["Option", "Ops cost", "Ceiling"],
"rows": [
["Postgres", "We run one already", "Years away"],
["SQLite", "None", "Single node, permanently"]
],
"recommendation": "Postgres, unless single node is a promise we can keep." } },
{ "widget": "pick-one", "props": {
"title": "Your call",
"question": "Which one?",
"options": [
{ "value": "postgres", "label": "Postgres" },
{ "value": "sqlite", "label": "SQLite" }
] } }
]
}
Roughly 2.4 times fewer tokens than authoring and publishing the equivalent page, and it comes out the same every time. Measured — one session forked twice, asked to hand the same work over both ways.
What the person opens
One short link per participant:
https://sirdocalot.vteng.io/r/x7v43et5xetd
Behind it is a server-rendered page with a plain form. No account, no sign-in, no JavaScript, no Claude access, no need to know what an agent is. That is what makes it usable by the people who are not on your team: the customer, the lawyer, the person in finance who answers one question and never comes back.
The widget library
This is the whole vocabulary. An agent names one and passes props; it never describes a layout.
See every widget rendered — each one built from the example it ships with, so the page is the thing itself rather than a description of it.
There is no thirteenth without a pull request. Widgets used to be postable at runtime, which meant any agent could add one every other agent then saw, and could overwrite one somebody else was using. A shared vocabulary that anyone can extend at runtime is not shared, it is accumulated. So the list above is a reviewed file in the repository, and the escape hatch for a shape nobody anticipated is a sandboxed raw block — needing it twice is the argument for a pull request.
Three tiers
Headings, prose, callouts, lists, key-values, tables, code, fields. Compiled in. This is the only tier that costs a deploy, so it grows slowly and on purpose.
Compositions of primitives, stored as rows. The twelve builtins are ordinary widget definitions with a flag set — there is no second mechanism for them to be special by.
Sandboxed HTML, for the thing the vocabulary genuinely cannot say. Named raw so it is obvious in a brief which parts were not expressible, and so reaching for it is a signal rather than a habit.
Disagreement is surfaced, never resolved
Five people answer and two of them disagree. Nothing here picks a winner. The service has none of the context that decision needs, and quietly averaging it away would destroy the one signal worth having — that the question was contested.
{
"closed": true,
"results": {
"responded": ["p_priya", "p_sam"],
"outstanding": [],
"conflicts": ["choice"],
"fields": [
{ "field": { "id": "choice", "kind": "choice" },
"agreed": false,
"contestable": true,
"answers": [
{ "participant": "p_priya", "value": "postgres" },
{ "participant": "p_sam", "value": "sqlite" }
] }
]
}
}
Choices, booleans, ratings and numbers have an answer space, so two people landing in different parts of it means something. Free text has no such space. Two people writing different prose is what prose does, and flagging it would bury the one real disagreement under every comment box on the form.
When collection closes
Stated per brief, because all four are legitimate and the agent is the only party that knows which situation it is in.
- all
- Close once every invited participant has answered.
- quorum
- Close at n responses, whoever they came from.
- deadline
- Close at a timestamp, however many answered.
- manual
- Close when the agent says to.
- blind
- Participants cannot see each other's answers. A survey.
- open
- They can. A deliberation. The two produce genuinely different answers, which is why it is a choice and not a default.
What it does not do
It never resumes an agent. Nothing here will wake you, retry you, or hold your process open on your behalf. It guarantees three narrower things instead:
- The question and the context around it are durable.
- The answers are retrievable by handle, by whoever holds the handle, whenever they ask.
- A signal fires when collection closes.
So a long-running session long-polls /api/briefs/<id>/await and blocks on it. An ephemeral agent hands the id to whatever orchestrates it and exits. Both work, because both are how agents actually run.
Keys
An agent generates an Ed25519 keypair and registers the public half. Requests are signed over the method, the path, a timestamp and a digest of the body. The service never receives a private key, so there is no shared secret here to leak and nothing on this end to reissue if you lose yours.
Getting started
With a code in hand, setup is one line pasted into Claude:
Set yourself up on sirdocalot: read https://sirdocalot.vteng.io/start and follow it. My invite code is xxxx-xxxx-xxxx
The agent reads /start, makes a key, registers, and saves the signing helper from /cli. Both are plain text and both are meant to be read first: /cli hands back a short shell script to look at and keep, not a pipe into a shell. An agent that will run whatever a URL returns is a supply chain with one link in it.
Run it yourself
This instance is one box. The software is not tied to it — it is on GitHub at github.com/gdoteof/sirdocalot, and a copy of your own is two containers:
docker compose up
That is Postgres and the service on http://localhost:8080. The compose file carries working development credentials deliberately, so there is no setup step between cloning it and having something to open.
On your own instance you are the operator, so the invite gate becomes self-service rather than a queue: you mint your own code. The just enrol recipe generates a keypair, registers it, and prints the claude mcp add line to paste.
It is a container and a Postgres, so it deploys anywhere that runs those. To make a home server reachable from outside without forwarding a port, a Cloudflare Tunnel is the realistic answer — it is how this box is reachable.
The details that belong in a repository stayed in one: docs/SELF-HOSTING.md.
If you got here without a code, that is the whole ask: find the person running this box and tell them what you want to use it for. Or do not ask anyone, and run your own.