Profile first
wloop is meant to be customized. A behavior decision (when something happens, on which units, where it runs, what it instructs, which effects it asks for) is an intent or a workflow of the profile, and every text an agent or a person reads belongs there too. Go provides facts, mechanisms and gates: it reads units, launches agents, publishes, and holds the approval, the Outbox, the records, the budgets and the guards. It does not decide what to do and does not write prose.
The embedded profiles/default/ is the lowest layer of every profile. Each enabled plugin’s layer sits above it, the Slack plugin’s embedded from plugins/slack/profile/ (Plugins). Your profile, the directory the configuration’s profile names, sits above them.
What lives in the profile
Section titled “What lives in the profile”| What | Bundled in | Your profile |
|---|---|---|
Intents: a manifest (intent.toml) and its instructions (instructions.md.tmpl) | profiles/default/intents/<name>/ | adds intents under intents/<name>/, and overrides a bundled one file by file under its name |
Workflows: provider, trigger, predicate, steps, approval, limits (workflow.toml) | profiles/default/workflows/<name>/ | adds workflows under workflows/<name>/, and overrides a bundled one file by file; workflows.<name> in the configuration sets a bundled one’s observe, approval and switch |
The words of the built-in actions (Run now, Skip, Take over, …): label, summary, note, warning, confirm, why, an input’s label and help, and the copy an action gives to paste (the review request of Copy review request) | profiles/default/actions.toml | an actions.toml whose tables replace the bundled ones |
| Judgment: what a review looks for, the voice of a reply | the bundled guides, profiles/default/intent=<name>.md | blocks whose path says when they apply, such as repo=api/intent=address.md |
| Partials: the text every intent’s instructions share, such as the record protocol, the history and the brief | profiles/default/partials/<name>.md.tmpl | replaces a partial for every intent under partials/<name>.md.tmpl, and adds partials its intents call |
Prompts of wloop‘s own calls: the headless turn’s wrapper (headless.md.tmpl), the omnibox agent call’s (omnibox.md.tmpl) | profiles/default/prompts/ | replaces a prompt under prompts/ |
| The headings of a block’s conditions in a turn’s judgment | profiles/default/labels.toml | a labels.toml whose keys replace the bundled ones |
What an agent reads about each MCP tool: its description, its parameters’ (params, by path) and what it answers (answers, by name) | profiles/default/tools.toml, and a plugin’s tools in its layer’s tools.toml | a tools.toml whose keys replace the bundled ones |
The task states and their categories, the task properties with their types and writers, the verbs, a profile’s own included, and their words (label, summary, the button’s tooltip, input, the question a verb that asks for a value puts first), where a task offers each (offer), its move (from, to), its handler and who may run it (by, gate) | profiles/default/tasks.toml | a tasks.toml whose keys replace the bundled ones and whose new states and properties come after the bundled ones |
| The gates: each one’s kind, words and the intents it is resolved with | profiles/default/gates.toml | a gates.toml whose keys replace the bundled ones and whose new gates and intents come after the bundled ones |
| The views: what each lists (its condition over a task’s or a gate’s facts), its label, key, grouping and sort; the review’s steps; the views the badge counts | profiles/default/views.toml | a views.toml whose keys replace the bundled ones and whose new views and steps come after them |
| The record kinds: the JSON schema each one’s data follows, who may write it and the task properties its fields set | profiles/default/records.toml, its schemas under profiles/default/records/ | a records.toml whose keys replace the bundled ones |
| The help line of each core key, by context, then action | profiles/default/keys.toml | a keys.toml whose keys replace the bundled ones |
| What the loop tells a session, and the person about what they wrote: the notice that the user answered a gate or wrote on the task’s chat, the reason a stop is blocked while the turn owes a record, where a reply went, the words of a notification’s text inputs | profiles/default/notices.toml | a notices.toml whose keys replace the bundled ones |
| The decision kinds a task waits on: each gate kind’s, each plugin’s situations’ and link signals’ (in its layer), each kind’s view and words, and what a queue offers first on a plugin’s unit | profiles/default/decisions.toml, a plugin’s in its layer’s decisions.toml | a decisions.toml, or plugins/<id>/decisions.toml, whose keys replace the bundled ones |
A plugin’s predicates over its units’ facts, by the names its manifest’s [[source.predicates]] declares, such as the GitHub plugin’s reviewer_required: whether a pull request waits for a reviewer before its merge | a plugin’s layer’s predicates.toml | a plugins/<id>/predicates.toml whose keys replace the bundled ones; one that does not compile leaves the bundled one in force |
| Whether a Slack thread reply also posts to its conversation when its proposer did not say: by the kind of conversation, and for a reply the person writes | the Slack plugin’s layer’s drafts.toml | a plugins/slack/drafts.toml that replaces the whole file |
| The home: the briefing’s counters and words, its sections and the capture field’s hint | profiles/default/home.toml | a home.toml whose keys replace the bundled ones, and whose list of counters or sections replaces the whole list |
| The omnibox: how a search groups its results (each group a view and its label), and the last group’s label | profiles/default/omnibox.toml | an omnibox.toml whose keys replace the bundled ones, and whose list of groups replaces the whole list |
A file of yours replaces the bundled file of the same path; a manifest of yours replaces the bundled manifest whole. An intent becomes a command, a skill and an action on rows; a workflow is everything wloop does without being asked. A broken user intent or workflow disables only itself (a broken override leaves the bundled one in force), and evaluating the plan writes nothing. See Profiles and The sweep and workflows.
What Go provides
Section titled “What Go provides”Manifests name Go through a closed vocabulary, listed in internal/intent/builtin and checked when the profile loads. Every reserved key of a manifest, of actions.toml, tasks.toml, views.toml, gates.toml, records.toml, keys.toml and of the configuration is a row of one table, builtin.Grammar (internal/intent/builtin/grammar*.go), with its kind, values, default and doc; every loader runs builtin.Check against it, and the profile language lists it:
- Facts: named, typed values a provider computes for a unit, in
builtin.Facts(internal/intent/builtin/facts.go). Predicates are CEL over the facts of a slot, compiled at load byinternal/intent/expr; an evaluation error reads as false. A fact flaggedDimis also a condition of a judgment block’s path; today’s dimension names (organization,reviewer,kind, …) are aliases of facts ininternal/profile/dims.go, and an enum field of the brief intent’sextrais one too. - Steps: the actions a workflow runs, in
builtin.Steps, each with its reach and its guards. A profile cannot lower a guard or force a step. - Roles: what binds an intent to a plugin’s mechanism, in
builtin.Roles(internal/intent/builtin/roles.go):pr.review,pr.address,pr.nudge,probeandheadless. A manifest names itsrole; Go derives what the role implies (see below). - Effects: what sending a turn or a draft applies, in
builtin.Effects(internal/intent/builtin/effects.go), each applying to a turn, a draft or both: an intent’seffectswhen its turn is sent, adraft.proposestep’seffectsand an intent’s[drafts] allowed_effectswhen a draft is sent. - Builtins by slot: what an intent’s
workspaceand an input’sbeforemay name, inbuiltin.Names. - Marks: what a unit’s triggers handled. A workflow records its own with
[marks]and reads any withhandled()in a predicate; the core stores and compares them (internal/domain/mark), and a plugin exposes the signal a mark holds as a fact. - Gates: the approval of plan items and drafts, the Outbox, the record a turn must end with, the caps and budgets, the cleanup guards.
Some decisions stay in Go as mechanism and are exposed as facts for the profile to act on:
- which tasks exist (
task.Reconcileininternal/domain/task/reconcile.go: one task per item that waits on you or that you are owed); - when a plugin’s unit has ended (
<slot>.ended, such aspr.endedandslack.ended), whichtask.link_endedreads; whether a turn attaches its results to its task is the intent’s (record.attach_url,drafts.attach_posts), and closing the task is thetask-results-endedworkflow’s; - working-day arithmetic and who owes an item;
- the GitHub round planners’ due logic (
pr.first.due,pr.review.action,pr.address.due); - injection markers, JSON schemas and limits;
- the gate guard: every decision waiting on you is a gate, which only the gates service resolves, by you, a trusted policy rule or a passed window; a draft’s delivery is what its plugin posts it as, a delivery its plugin’s manifest marks
postswaits for your approval unless a rule of yours that names that delivery releases it, and no agent or thread text resolves a gate (no rule of a draft gate, nor a rule that releases a gate, reads an untrusted fact; a workflow’swhenmay, and a run it starts releases a draft only through that draft’s gate); - the gates’ consequences, one per kind, and how a mode applies to each kind:
sendandrunrelease,delayis the window,waitholds,dry_runis a draft gate’s only; the default of each kind isgates.toml’s[approval].
The rules that keep the line
Section titled “The rules that keep the line”- Go never enumerates intents and binds none by name. It finds the intents it starts itself by their manifest’s
role(builtin.Roles,Registry.ByRole), and reads a turn’s history by the role stored with it; the files still naming an intent are listed with their reasons (TestNoIntentEnumerations). - The
rolebinds an intent to a plugin’s mechanism, and Go derives what the role implies frombuiltin.Roles: the round kind, the reconcile with the review you published, the prepare that fills a nudge’s inputs, the context and launch of a probe, the agent policy of its sessions, and whether its turns drive their workspace’s row. A manifest does not write those. Which board a turn moves follows from itsworkspace(revieworauthor, else none), and every automatedturn.starton a unit spends its daily budget, whatever its intent declares:turns.per_unit_per_dayon a unit whose plugin keeps a per-unit count,turns.per_dayon the others. A user intent that takes a bundled role takes all of it over; the loader still refuses two intents that drive one workspace’s row. - What a turn says about itself is the profile’s: its
why, its[[refuse]]conditions with theirwhy, and its action’snote,warning,confirmand[[action.refuse]], templates over facts that Go computes (pr.review.reason,pr.nudge.refusal). - A builtin’s name is spelled only in its tables:
internal/intent/builtin/builtin.go,internal/cli/builtins.go,internal/daemon/builtins.go. - Every key a profile or the configuration may write is a row of
builtin.Grammar; any other key, a removed one included, fails at load, naming a close key when one exists, and no old spelling is read. The JSON schemas and the reference pages of the manifests, the configuration and the language are generated from it (make docs-ref). - Only
internal/catalogbuilds action specs and evaluates an intent’s predicates. - The automation engine and the rest of the core name no provider.
- Go never names a draft’s kind, compares its purpose with a value, or reads its delivery outside the publishers and the Outbox (
TestNoDraftKindComparisons). - Go reads a task’s category and names no state: no Go literal is a
task.Stateor atask.Categoryoutside the profile’s loader, the closed set’s declaration and the version 7 migration, and the store’s SQL binds categories and compares the state column with no state (TestGoNamesNoState). The store keeps the category beside the state, and a profile reload recategorizes the stored tasks. - A draft’s text comes from a profile template or from whoever wrote it, and only
draft.proposeand the plugin rules its allowlist names propose drafts on their own (TestDraftTextComesFromTheProfile,TestOnlyTheDraftStepProposes). - Prose is embedded only from
profiles/; what Go still embeds is listed with its reason, and the list of what moves only shrinks (TestProseIsEmbeddedOnlyFromProfiles). - The engine plans every automatic round; the round gate it replaced stays gone (
TestNothingImportsTheGate). - The terminal client’s words are
strings.toml’s, throughtexts.Catalog, its key names (Tab, Ctrl+E) passed as values, never translated. - The cockpit’s words are
strings.toml’s, throughtandtnininternal/web/static/i18n.js, and the core’s event kinds aretimeline.toml’s; no script quotes a text a person reads outside them (TestClientsQuoteNoProseOutsideTheCatalog). - The menu bar’s words, its items, the notes on what became of an answer and the app window’s menus, are
strings.toml’s, which the daemon serves at/api/stringsin the interface language (internal/client/texts); the app reads them again when their stamp in the health moves, and words the window’s menus anew.
What Go still owns today
Section titled “What Go still owns today”These texts and decisions are still in Go, and are moving to the profile:
- the GitHub plugin’s
github-nudgerule (plugins/github/nudge), whose texts are its profile layer’sprompts/github-nudge.tmpl; - the workflow providers, pull requests, sessions, workspaces, tasks and Slack units (
builtin.Slots); - the renderings of Slack messages that
slack_readandslack_searchreturn (the Slack plugin’splugins/slack), the JSON-RPC errors of the MCP endpoint, and the error texts a mechanism returns through a tool’sfailedanswer.
Direction
Section titled “Direction”A draft is what its proposer declares: a purpose, a delivery and the effects sending it applies, each once, through the effect table and a plugin’s own (TestNoDraftKindComparisons). An agent’s draft declares only the effects its intent lists in [drafts] allowed_effects, and a workflow proposes, spaces out (repeat_after, repeat_after_sent) and withdraws ([drafts.withdraw]) its drafts with draft.propose, its text from the profile.