intents.md
c146 Phase 1: the intents catalog
Written: 2026-11-13, for the Phase 5 eval (eval.md).
What it is: twelve plain instructions a person gives their agent about config.yaml, each with the fragment that answers it. It is the eval set: an agent passes an instruction when its first proposal checks clean, its draft carries the fragment's essentials, and the Config page shows the draft ready to Apply.
How each fragment was checked
- The starting file is the
meridian-day90world's (nina-mbp, the quiet laptop), the file the eval runs against:# qcontrol configuration (this machine) # Apply behaviors to targets. First match wins. targets: - title: All harnesses if: is_harness() behaviors: [audit] # Available behaviors behaviors: # Out-of-band auditing. audit: plugins: - name: io-events - name: llm-events - name: mcp - name: sentinel - Each fragment was merged into that file and the whole file checked twice:
- the app's checker,
config-checkon the guide's MCP server (qnative main313bd7c,crates/app/src/config/check.rs), the check the eval's agents see; - qconfig itself, the loader on qcontrol
main(8677b44ed):RawRoot::readthenqconfig::load, run from a scratch example outside the repo. This is the check qcontrol will make.
- the app's checker,
- Both pass all eleven whole files, and both refuse #11's file (the one meant to be refused) on the same key. The two checkers do not always agree, though: see
eval.md§ Findings, F1. - Harness ids are qcontrol's:
claude-cli,cursor-cli,codex-cli,aider(qconfig's tests and the monitor's inventory tests). - The fragment is what changes. Everything else in the file stays as it was. "Essentials" says what a draft must carry to match; a draft may word its comments, titles and behavior names its own way.
The catalog
1. "Audit everything Claude Code does."
targets:
- title: Claude Code
if: harness.id == "claude-cli"
behaviors: [audit, commands-and-files]
- title: All harnesses
if: is_harness()
behaviors: [audit]
behaviors:
# Commands and file access, on top of audit.
commands-and-files:
exec:
enabled: true
file:
enabled: true
Essentials: a target for claude-cli before All harnesses (the first match wins), carrying audit and a behavior that turns on exec and file capture. All harnesses keeps audit. More capture (netstat, the io-log plugins) is a fair reading of "everything".
2. "Send my events to my OTel collector at collector.internal:4318."
sinks:
- url: otel://collector.internal:4318
Essentials: one sink, otel://collector.internal:4318, with no if:, so every event goes.
3. "Also log my events to a file."
sinks:
- url: file:///var/log/qcontrol/events.jsonl
Essentials: a file:// sink with an absolute path (qconfig refuses a relative one). The stock document's example path is the obvious choice.
4. "Enforce OpenShell for Cursor. My policy is at /Users/nina/.config/qcontrol/policies/cursor.yaml."
targets:
- title: Cursor
if: harness.id == "cursor-cli"
behaviors: [audit, openshell-enforce]
- title: All harnesses
if: is_harness()
behaviors: [audit]
behaviors:
# Enforce an OpenShell policy.
openshell-enforce:
plugins:
- name: openshell
config:
policy: /Users/nina/.config/qcontrol/policies/cursor.yaml
Essentials: a cursor-cli target before All harnesses, with audit kept and a behavior running the openshell plugin with config.policy set to the person's path. The instruction gives the path, because the plan's shorter wording ("enforce OpenShell for Cursor") leaves the agent nothing to enforce.
5. "Send my events to my collector at otel.example.com:4318. It needs my API key, which is OTEL_API_KEY in ~/.config/qcontrol/secrets.env. Keep the key out of the config file."
secrets:
my_secrets:
provider: dotenv
path: /Users/nina/.config/qcontrol/secrets.env
sinks:
- url: otel://otel.example.com:4318
headers:
Authorization: $__my_secrets{OTEL_API_KEY}
Essentials: a dotenv secret store whose path is absolute (qsecrets refuses ~), and the sink's header naming the key as $__<store>{OTEL_API_KEY}. The header's name is the collector's business: Authorization or an API-key header both match. The key's value appears nowhere.
6. "Turn the tray off."
ui:
tray: false
Essentials: ui.tray: false. The dashboard is left as it was. The words collide with the app's own Messages › Settings › "Show messages in the tray", so a good answer says which tray it changes.
7. "Send only file writes and the commands my agents run to my collector at localhost:4318."
behaviors:
audit:
exec:
enabled: true
file:
enabled: true
plugins: … # as before
sinks:
- url: otel://localhost:4318
if: |
starts_with(type, ['exec.'])
or type in ['file.write']
Essentials: a sink at otel://localhost:4318 whose if: admits exec.* events and file.write, and nothing else. Turning on exec and file capture makes sure those events exist; it isn't required to match, because unwritten coverage follows the plugins.
8. "Stop watching Aider."
targets:
- title: Aider (discovered, not watched)
if: harness.id == "aider"
behaviors: []
- title: All harnesses
if: is_harness()
behaviors: [audit]
Essentials: an aider target first, with behaviors: []. That means "discover only" in qconfig: Aider is still seen but not tapped, and because the first match wins, All harnesses never reaches it. Narrowing All harnesses' rule instead (is_harness() and harness.id != "aider") also matches.
9. "What is my config file doing right now?"
No fragment, and no proposal. The answer comes from config-read's effective view: one target, All harnesses, running audit (io-events, llm-events, mcp, sentinel); no sinks, so nothing leaves the machine; the tray and the dashboard on (the defaults). Passes when the agent reads before it answers, says those four things in plain words, and proposes nothing.
10. "Start me from the developer-laptop starter."
The whole of developer-laptop.yaml from config-starters: coding agents (claude-cli, cursor-cli, codex-cli) get audit with command and file capture plus egress-allowlist (OpenShell); every other harness gets audit; tray and dashboard on.
Essentials: the starter's text, unchanged. The agent says that it replaces the current file and what is lost: nothing here, since the current file only audits.
11. "Propose this config exactly as it is, don't fix anything: …" then "Why was my draft refused?"
The file is the starting file with plugins: misspelt plugin: under audit. config-propose must refuse it:
line 13 · behaviors.audit.plugin · Unknown key `plugin` in behaviors.audit. · Did you mean `plugins`?
qconfig refuses it on the same key.
Essentials: the first proposal is refused (here, refusal is the pass), nothing reaches the page, and the answer to "why" names the line, the key and the fix (plugins) in plain words and offers to propose the fixed file. "First proposal checks clean" is read the other way round for this one instruction.
12. "Give Python scripts the full IO logs."
targets:
- title: Python scripts
if: process.binary == "python3"
behaviors: [io-log]
- title: All harnesses
if: is_harness()
behaviors: [audit]
behaviors:
# Full trace-level IO logs.
io-log:
plugins:
- name: http-logger
- name: net-logger
- name: file-logger
Essentials: a target matching Python by process.binary (or process.path), and an io-log behavior with the stock document's three loggers. The trap: the starting file doesn't define io-log. Only the stock document does, and it isn't in force once a file exists, so naming io-log without defining it is refused ("not defined under behaviors"). Order doesn't matter here, because a script is not a harness.
Not in the set
- A managed machine. "Turn the dashboard off" while the company's plane manages the file should be refused with C9's sentence ("Managed by your company. Changes are made there."). This needs the
planesource and is left for Mark's walk, since C9 itself is open with Mark Peterson. - Includes,
unix://andpipe://sinks,injecttuning, proxy knobs. These are real keys that no person asked for in the plan's first dozen. The reference covers them; the catalog grows when people ask.