Skip to content

Add your own intent

An intent of yours lives in your profile and needs no rebuild: the daemon and the CLI read it again when a file under the profile changes. This one asks the author of someone else’s pull request one question, after you approve it, and publishes nothing.

<profile>/intents/ask-author/intent.toml:

#:schema ../intent.schema.json
label = "Ask author"
summary = "Ask the author one question"
workspace = "review"
session = { strategy = "new", title = "ask" }
shortcut = "Q"
start_line = "asking the author"
[inputs.topic]
type = "text"
flag = "--topic"
help = "what to ask about"
max = 200
persist = true
[action]
surfaces = ["row", "detail", "tui", "cli"]
when = "row.open && !pr.mine"
note = "Ask {{.pr.author}} one question"
[headline]
resumed = "ask round on {{.pr.slug}}"
[[headline.new]]
line = 'Ask the author of {{.pr.slug}} one question, {{template "started_by" .}}.'
[record]
states = ["done", "asked", "failed"]

The directory name is the intent’s name: a lower-case letter, then 1 to 23 lower-case letters, digits or -. It may not repeat a CLI command; a bundled intent’s name overrides that intent, as Change a bundled intent shows.

The session shows in Orca and Claude Desktop as its session.name, a template over the name facts: session = { ..., name = '{{.session.title}} {{.unit.label}} {{.session.started.Format "02/01 15:04"}}' } names it ask acme/api#7 26/10 14:05. Without it, the session takes session_name from the profile’s defaults.toml, which names it ask acme/api#7.

<profile>/intents/ask-author/instructions.md.tmpl is a Go template over the turn:

# {{.Headline}}
Read {{.PR.URL}} and write the one question its author most needs to answer.
{{- with .Inputs.topic}}
About: {{.}}{{end}}
Publish nothing: no comment, no review, no reply, no push. The user decides
whether the question goes out; you ask the user.
{{if .Resume}}{{template "resume" .}}{{end}}
{{- if .Guidance}}
## Guidance
{{.Guidance}}
{{end}}
## Steps
1. Read the pull request: `gh pr view {{.PR.URL}}` and `gh pr diff {{.PR.URL}}`.
2. Write one question for the author: short, specific, and not already
answered in the description or the threads.
3. Record `asked` with a `free` question whose `text` is the question you
would send, and whose `recommend` says why it matters.
4. When the user answers, record `done` with the question as they approved
or rewrote it as the whole `summary`.
{{template "history" .}}
{{template "record" .}}

An intent without a [record] table owes no record: its turns are stored, but the Stop hook never blocks their sessions for one, as for the bundled attach. Include {{template "history" .}} and {{template "record" .}} in any intent that records, and {{if .Resume}}{{template "resume" .}}{{end}} in any that can be answered.

Terminal window
wloop profile check
wloop --help | grep ask-author
wloop profile show acme/api --show

profile check loads the manifest and renders the templates against sample turns, so a template that would fail at run time fails here, as file:line:col: field: message. --help now lists ask-author <url> [--topic <topic>] [guidance...] under your intents, and profile show --show renders its instructions for a placeholder pull request.

A [help] table shapes the command’s help: target says what the target is, summary replaces the summary in the main help, group moves the command into a group of the main help such as "pull requests", and examples and docs give its examples and documentation page:

[help]
target = "a pull request URL"
group = "pull requests"
examples = ["https://github.com/acme/api/pull/4610 --topic 'the retry cap'"]
Terminal window
wloop ask-author https://github.com/acme/api/pull/4610 --topic 'the retry cap'

The turn goes to a new session of the pull request’s review workspace. The cockpit and the terminal client offer it as Q on open pull requests that are not yours. The agent records asked with its question; your answer reaches its session, or a resume turn that keeps --topic.

A user intent runs only when you ask. To start it on its own, write a workflow whose turn.start step names it.

Terminal window
wloop profile schema > <profile>/intents/intent.schema.json

With the #:schema ../intent.schema.json line at the top of each manifest, editors with Taplo (the “Even Better TOML” extension in VS Code, taplo lsp elsewhere) complete keys, show each field’s description and flag unknown keys as you type. profile check remains the full check.

A broken intent of yours disables only itself: its command exits 1 with its problems, its action stays disabled with the reason, and every other intent keeps working. The intent manifest lists every field, and the profile language every reserved word on one page.

A directory of your profile named as a bundled intent overrides it file by file. To rewrite what an address round says and keep the rest of address:

Terminal window
wloop profile show acme/api --show # the bundled instructions, rendered
mkdir -p <profile>/intents/address
# write <profile>/intents/address/instructions.md.tmpl
wloop profile check

Each file you write replaces the bundled file of the same name; the files you leave out are the bundled ones. An intent.toml of yours replaces the bundled manifest whole, with no merge of keys, so start from a copy of the bundled one. Files whose name starts with . are ignored. profile check lists the intent as bundled, overridden: instructions.md.tmpl. When your files do not load, cannot be read, or break another bundled intent, the bundled intent stays in force, profile check reports the problem against your file, and wloop doctor lists the intent as running as bundled. A problem your manifest causes in a bundled file you did not override, such as a bundled template reading an input your manifest dropped, is reported against your manifest. The judgment blocks that only your override made valid, such as intent=<name>.md for a profile it added or a stage= condition for dims it added, are skipped until it loads again.

Your override yields to your own intents: when it claims a shortcut, a flag or a row-driving workspace one of your intents already holds, the override is the one that does not load.

Judgment blocks (intent=address.md) remain the way to add to what a turn looks for; override the instructions when you want to change what the turn is.

The partials are the text every intent’s instructions share: record (how a turn ends), history, resume, brief, judgment (with review-judgment in the same file), items, unit, dossier, why, artifact (the record’s sentence per artifact kind) and started_by (who started the turn, for headlines). Each is a file of the bundled profile’s partials/, and a file of yours of the same name replaces it for every intent:

Terminal window
wloop profile show acme/api --show # the instructions, rendered
mkdir -p <profile>/partials
# write <profile>/partials/record.md.tmpl, defining "record"
wloop profile check

A partial of yours with a new name adds a template your intents may call. profile check renders every intent over your partials; a partial that does not parse or breaks an intent is reported against your file, and the bundled partials load in its place. The record partial must still tell the agent the record protocol: the turn_end tool, the turn end ... --turn command that stands in for it, and the state and summary fields; one that drops any of them fails profile check.

The headless turn’s prompt around the unit’s instructions is prompts/headless.md.tmpl, which defines before and after; wloop writes the unit data’s markers, the instructions between them and the answer schema itself. A wrapper of yours that does not parse is logged and the bundled one is used.