9 Commits

Author SHA1 Message Date
Michał Pierzchała 4f5a87b6b3 refactor(command-registry): move CLI flag grammar, text and command aliases down (#2561)
Move the vocabulary that both the CLI and commands read but no command's runtime
depends on into the package below both: flag types, registry, groups and the four
flag-definitions files, command-text, and cli-command-aliases. These are pure moves;
only their import specifiers change.

No compat re-export at the old paths — every consumer switches to the owning
subpath. The per-command defaults stay where they are for now (the daemon's edge into
the facet resolver is the harder cut and belongs with the daemon-closure work).

Part of #2545 / #2543.
2026-09-14 11:27:22 +02:00
Michał Pierzchała da76aa4f1e refactor(commands): declare project-config admission and recorder sanitization on the flag declaration (#2453)
* refactor(commands): declare project-config admission and recorder sanitization on the flag declaration

Move the two fail-closed flag properties — may a key be set from a project
`agent-device.json`, and does the session recorder copy it into `SessionAction.flags`
— off the hand-maintained allowlists and onto each `FlagDefinition` as required
`projectConfig` / `recorded` fields. Omitting either is now a type error, so the
compiler holds the fail-closed property a list held by omission.

- 156 declarations carry both fields; the 6 screenshot-specific definitions carry them
  too. Populated to match the old sets exactly (one-off diff empty: 85 project-config
  and 39 recorded keys, byte-for-byte).
- `cli-config.ts` and `session-action-recorder.ts` derive their sets from the registry
  and no longer list keys; `RECORDED`/`PROJECT_CONFIG` derivations recomputed per call so
  a consumer builds its set at its own module load. Recorder reaches the derivation
  through the `cli-schema/command-schema.ts` seam (daemon may not import `commands/`).
- Planted-divergence tests, per #2421: flipping one declaration's field moves the
  admission/sanitization outcome through the production derivation, plus a compile-time
  pin that an incomplete declaration does not build.
- `docs/agents/cli-flags.md` now points at the declaration fields, not the allowlist.

Refs #2445

* refactor(commands): return the recorded keys as a set, matching project-config

Both derivations answer the same question — the set of flag keys a surface admits —
so both return ReadonlySet<FlagKey>. Drops a needless set-then-spread on the
recorder path; consumers already iterate the value.

Refs #2445

* fix(commands): keep the CommandFlags guard on recorded flag declarations

The deleted `SANITIZED_FLAG_KEYS` was `satisfies readonly (keyof CommandFlags)[]`,
so every recorded key had to be a `CommandFlags` key. The derived set returns
`FlagKey` and the recorder indexed it through a cast, so `recorded: true` on a
CLI-only key (`daemonAuthToken`, `help`, …) compiled and could leak an uncarrable
value into a recorded action.

State the constraint on the declaration: `FlagDefinition` is a union that locks
`recorded` to `false` for a `NonRecordableFlagKey = Exclude<FlagKey, keyof CommandFlags>`.
`recordedFlagKeys()` returns `ReadonlySet<RecordableFlagKey>` via a narrowing
predicate, so `sanitizeFlags` drops its cast. Adds a `@ts-expect-error` test that a
CLI-only key cannot opt into recording.

Refs #2445
2026-09-10 17:04:32 +02:00
Michał Pierzchała dbfebd4e8f refactor(cli-schema): derive the flag tail of usage synopses (#2456)
A command that hand-wrote its synopsis had to restate every option it
accepts inside that string, which is the last restatement left on the help
surface after #2421 made the flag declaration own the option itself.

A synopsis is now grammar plus a generated `[label]` tail, and the two
rendering rules live on the declaration rather than per command:

- the tail names an option with its declared `usageLabel`, alias included,
  the token the `Command flags:` section already shows;
- `usageHidden: true` keeps a cross-cutting opt-in out of every synopsis;
  `--record` is the one today, and it stays under `Command flags:`.

`usageFlags` is where a command states that its synopsis names fewer options
than it accepts: `[]` for a synopsis that is pure grammar or writes its own
mutually-exclusive brackets, otherwise the subset it names. `Command flags:`
still documents everything in `allowedFlags`. Adding an option to a command
therefore updates `--help` on its own, except where the command said its
synopsis stays short.

`snapshot` and `proxy` drop their override; `daemon`, `device`, `doctor`,
`prepare`, `tv-remote`, `scroll` and `artifacts` drop the flag brackets from
theirs. Guards fail a tail that names an option the command does not accept,
or one the hand-written grammar already wrote.

Every synopsis except `snapshot` and `is` is byte-identical; those two move
exactly per the rules above, and the canonical `snapshot` docs line follows
the generator.

Closes #2444
2026-09-10 16:31:08 +02:00
Michał Pierzchała 0da105e3c3 docs: simplify agent context and resolve conflicting guidance (#2287)
* docs: clarify agent task scope and validation

* docs: remove redundant and conflicting agent guidance
2026-09-05 20:04:40 +02:00
Michał Pierzchała bf26ab14d6 refactor(commands): one audience table for common input fields (#2074)
"Who may write this input field, on which surface" was expressed three times,
each a separate name-keyed mechanism: `retiredField()` in the command field
maps, `ALWAYS_HIDDEN_FIELDS` in the AI SDK adapter, and
`OPERATOR_INPUT_GUIDANCE` / `CONFIG_LOADER_GUIDANCE` at the MCP admission
boundary -- twelve hand-written refusal sentences keyed by name, far from the
fields they govern.

The root cause was that the ~19 shared common fields existed only as parallel
enumerations by name -- `commonProperties()`, `readCommonInput()`,
`commonToClientOptions()`, and the `CommonCommandInput` type -- carrying no
metadata, so any policy about a field forced a new name-keyed map elsewhere.

Declare each common field once, in `commands/common-input-fields.ts`, keyed by
its input key and carrying `{ schema?, read?, clientKey?, audience? }`. The JSON
schema, the readers, the client-options projection, and the model-facing
audience boundary all derive from that one table, and `satisfies Record<keyof
CommonCommandInput | 'target', ...>` makes a row without a field, or a field
without a row, a type error in both directions.

`audience` is the unified vocabulary (`commands/input-audience.ts`): `operator`
keys stay in the CLI and Node schemas but are hidden from and refused by every
model-facing tool schema; `retired` keys are absent from every schema yet still
recognized, so they answer with migration guidance. `retiredField()` now sets
`audience: 'retired'`, metro's `bearerToken`/`proxyBaseUrl` declare
`audience: 'operator'` at the field, and `stateDir` declares it in the new
`mcp/tool-control-fields.ts` beside the other MCP-only tool arguments. Refusal
guidance is rendered from each declaration's operator path -- env var names via
`buildPrimaryEnvVarName`, the operator config file, or an explicit sentence --
rather than hand-written per key, and `OperatorInputSource` is shaped so a
declaration naming no path at all does not typecheck.

`#2076`'s nested-step admission recurses through the same derived
`findInadmissibleInput`, so a batch step's refusals come from this audience map
rather than a second filter; its suite passes against this unchanged.

A field-level audience only reaches the boundaries through its command's
metadata, so that wiring is closed structurally rather than by convention:
`inputAudience` is required on `CommandMetadata`, and
`defineFieldCommandMetadata` -- which now takes an optional custom reader, so
`batch` and `gesture` go through it too -- is the only construction path for a
field-map command. At the boundary, a command's own audiences merge before the
global operator classifications, so an `operator` key outranks a colliding
per-command `retired` one and a name collision fails closed.

`command-input.ts` was 705 lines and over the 300-line target; the record
readers move to `commands/input-readers.ts` so the table can use them without an
import cycle. `click`/`press`/`fill` move onto `defineFieldCommandMetadata` --
they were that helper inlined.

`COMMON_COMMAND_SUPPORTED_FLAG_KEYS` stays hand-maintained: it is the CLI
parser's axis, and 25 of its 42 keys never become structured command input while
the table's `cwd` and `debug` are not flags. The reasoning is recorded above the
constant.

Purely internal: `listCommandTools()`, the CLI command schemas, and every
command `inputSchema` are byte-identical, verified by diffing the serialized
surfaces before and after.

Refs #2027
2026-08-27 14:03:09 +02:00
Michał Pierzchała 6a8beb653e feat(mcp): compact server instructions in both eras + MCP-only help tool (#1839)
* feat(mcp): compact server instructions in both eras + MCP-only help tool (#1833)

MCP-only clients got no workflow guidance: server/discover carried two
sentences, legacy initialize carried nothing, and the CLI guides
(agent-device --help, help <topic>) were unreachable over MCP.

- MCP_SERVER_INSTRUCTIONS: one MCP-phrased workflow card (<2 KB, the
  Claude Code truncation limit) returned by server/discover and legacy
  initialize alike.
- help tool, router-owned (not a command descriptor): no topic -> the CLI
  decision card; topic -> agent-device help <topic|command> text, prefixed
  with the one-line CLI->tool-property mapping; unknown topic -> isError
  listing the topics. listCommandTools() stays descriptor-only for the AI
  SDK; the router composes descriptors + help.
- Move src/cli/parser/cli-help{,-overview}.ts to src/cli-schema/ so
  src/mcp (rank 3) can import the renderers without a layering back-edge
  into src/cli (rank 6).

* fix(mcp): name terminal-only commands in help guides; colocate cli-help tests with their sources

- The MCP guide preamble claimed every `agent-device <command>` line is a
  tool of that name; `help web` tells the reader to run `web setup` /
  `web doctor` and no `web` tool exists. The preamble now lists the exact
  CLI-only set (listCliCommandNames minus listMcpExposedCommandNames) —
  derived, not scanned out of prose where `device`/`web` are ordinary
  words. Regression: help web names `web` as terminal-only, and the listed
  set equals the registry difference.
- cli-help-*.test.ts move from src/cli/parser/__tests__ to src/cli-schema/
  to mirror the moved sources.

* perf(mcp): tighten the guide card, tool description, and preamble

Instructions card 1572 -> 1378 bytes (paid every session), tool
description and preamble trimmed, HELP_TOOL built once as a const.
Bundle delta vs main 3189 -> 2715 bytes; the remainder is the guide text
itself, which the bundle carried in no MCP-phrased form before.
2026-08-18 17:48:36 +02:00
Michał Pierzchała 99967c7f01 fix: restrict project config trust (#1565)
* fix: restrict project config trust

* fix: preserve daemon auth transport context

* refactor: simplify project config trust

* fix: restrict project config write sinks
2026-08-03 15:48:45 +02:00
Michał Pierzchała 0ee2a86129 refactor: extract contracts workspace package (#1499)
* refactor: extract contracts workspace package

* fix: preserve screenshot diff result contract

* test: stabilize Android keyboard smoke
2026-07-30 17:07:46 +02:00
Michał Pierzchała 1a76344685 docs: restructure AGENTS.md and CONTEXT.md for progressive disclosure (#1402)
* docs: restructure AGENTS.md and CONTEXT.md for progressive disclosure

Apply the Claude 5 context-engineering guidance to the repo's agent docs:
keep the always-loaded file to gotchas and invariants, and move situational
guidance one hop away behind a routing table.

AGENTS.md 315 -> 229 lines. Cut generic agent-behavior boilerplate, three-way
duplication (Common Mistakes restated Hard Rules; Finding Source Owners
restated the registry section), and facts visible from the repo itself.
Kept verbatim: the expensive-lessons principles, enforcement gates, Hard
Rules, and environment traps.

Split out docs/agents/{cli-flags,pull-requests,device-verification}.md and
folded the Testing Matrix into docs/agents/testing.md, reframed around
pnpm check:affected so the prose stops duplicating the selector.

CONTEXT.md keeps all 50 terms, now grouped under a section index so a task
loads one section instead of the whole glossary.

* fix(check-affected): move the selector-owning sentinel to the Testing Matrix

The Testing Matrix moved from AGENTS.md to docs/agents/testing.md, but the
affected-check selector still treated only AGENTS.md as selector-owning. A
later matrix edit would have been classified as inert docs and skipped the
fail-open, so the selector could keep deriving gates from a spec that had
changed underneath it.

Move the sentinel with the prose, as a named SELECTOR_OWNING_DOCS set so the
next move is one line, and fix the two in-code comments plus the testing.md
paragraph that still pointed at the AGENTS.md matrix.

* docs: restore two rules dropped by the AGENTS.md split

Review caught two repo-specific rules that did not survive the move. Both are
prose without any backticked identifier, so the identifier-diff used to verify
the split could not see them.

- "Test through public interfaces; do not add unrelated production exports
  solely to enable tests" returns next to the behavioral-tests rule in
  docs/agents/testing.md, with the reason it exists.
- The guidance-ownership rule (decide whether new guidance/schema/metadata
  belongs to the command surface, CLI grammar, CLI help, MCP projection, or
  daemon runtime) returns to the always-loaded Docs & skills section, since it
  governs all command-surface work and not just the flag case.

Also point the ADR routing row at docs/adr/README.md, which is already the
"read when you touch…" index, rather than at the bare directory.
2026-07-25 12:13:09 +02:00