aristo-cli 0.2.4

Aristo CLI binary (the `aristo` command).
Documentation
---
name: aristo-intent-suggestions
description: Orchestrates the §17 proof-tree review loop. A choice-based explorer that (1) initiates the canon match, (2) reviews pending PRIMARY matches (your own annotations), and (3) reviews dragged-in SUGGESTIONS (the sibling invariants of the same proof objective). Drives the user through `AskUserQuestion` menus, finds placement sites, confirms each write, and adopts via the existing `stamp → canon accept` pipeline. All state lands in an `intent-review` session; the SDK is the sole writer.
sdk_version: {{SDK_VERSION}}
---

# Aristo intent-suggestions orchestrator

When the user invokes this skill (typically by typing `/aristo-intent-suggestions`, or
asking to "review canon matches / suggestions", "adopt the related invariants", or "what's
in the suggestions backlog?"), follow this orchestration exactly.

This skill is a **choice-based explorer**: you drive the user through `AskUserQuestion` menus
rather than free-form prose. The structure below was playtested and approved (§6A). You never
write `.aristo/` state files directly — every decision goes through `aristo session decide` and
every adoption goes through `agent writes #[aristo::intent] → aristo stamp → aristo canon accept`.
The CLI is the single write path; the agent writes source, the SDK manages state.

The skill is the **orchestrator for the whole loop**, not just a suggestions consumer (D9):
1. **initiate** the canon match (run the stamp/critique match step),
2. review **pending PRIMARY matches** — your own annotations the match flagged (new + carried-over),
3. review **SUGGESTIONS** — the proof-objective cluster dragged in alongside each primary.

## Two channels, one session

- **PRIMARY match** = a pending match for an annotation *you already wrote*. **Accepting it
  REWRITES** your wording to the canonical text and binds. The item ref is
  `match:<annotation-id>#<canon-id>`.
- **SUGGESTION cluster** = the *rest of the proof* the matched entry belongs to — the
  objective (parent) plus its sibling leaf invariants. **Adopting one ADDS a new annotation**
  (pure addition; nothing of yours is replaced). Item refs are `cluster:<key>` for the parent
  and `sibling:<key>#<canon-id>` for each dragged-in sibling.

Both are reviewed inside a single **`intent-review`** session, gated by the single-active-session
guard.

## Step 0 — check for an active review session (FIRST, BEFORE ANY WORK)

Run `aristo session active`. Three cases:

- **Empty stdout** → no active session; proceed normally.
- **Stdout shows a session id with `kind: intent-review`** → a prior intent-review session is
  in flight (e.g., a previous turn that didn't reach exit). Resume it: jump to the ROOT menu
  (Step 3) with that session id; do NOT re-run the match — the user has unfinished triage first.
- **Stdout shows a session id with a different `kind` (e.g., `critique-review`, `proof-review`)**
  → REFUSE. Print: "an aristo `<kind>` session is currently active (id=<id>, subject=<subject>).
  Exit it first with `aristo session exit` (or `aristo session exit --defer-undecided` to park
  open items), then re-invoke `/aristo-intent-suggestions`." Stop here. Do not run the match,
  start a session, or touch state.

This check is Layer 3 of the three-layer enforcement (Layer 1 is the SDK pre-check that refuses
mutations while a session is active; Layer 2 is the `UserPromptSubmit` hook). Always run it first.

## Step 1 — route to a mode (NL or menu) — §6B

The skill is a **zero-argument slash command** (`/aristo-intent-suggestions`), exactly like
`/aristo-critique`. There is no `/skill --flag`. A **mode** is reached two ways: (a) the user
states intent when invoking and you route, or (b) the ROOT menu (Step 3) offers it. Each mode is
just a **recipe of existing CLI flags** — no new machinery, no new grammar.

| Mode | When the user says… | Recipe (existing primitives) |
|---|---|---|
| *(default)* full | (nothing special) | initiate match → Stage A → Stage B |
| **backlog** | "just the backlog", "carried-over only", "don't re-match" | `aristo stamp --skip-canon` (no match) → seed session from cached/backlog items only |
| **status** | "what's pending?", "just the counts" | `aristo canon suggestions --counts` → print the table, **no session, no writes**, done |
| **matches** | "only the matches", "review my own annotations" | run **Stage A only** (skip Stage B) |
| **suggestions** | "only the suggestions" | run **Stage B only** (skip Stage A) |
| **for `<file>`** | "for src/storage/wal.rs" | add `--filter file=<path>[:LO-HI]` across both stages |
| **cluster `<objective>`** | "only the WAL cluster" | add `--filter parent=<kanon:objective>` across both stages (free — leaves carry `parent=`) |

Modes are **orthogonal and compose** (e.g. "backlog suggestions for src/storage/wal.rs"
= `--skip-canon` + Stage B only + `--filter file=core/storage/wal.rs`). Reuse the **existing
`--filter` grammar verbatim** (`id=`, `file=[:LO-HI]`, `parent=`, `status=`) — it already exists
for `critique`/`canon`; wire it through, don't reinvent.

- **status** short-circuits before any session: print the counts table from
  `aristo canon suggestions --counts`, then stop.
- **backlog** sets `--skip-canon` so Step 2 is skipped; the session seeds purely from
  carried-over items, and the entry Q&A then only offers the "carried-over" bucket.
- **matches** / **suggestions** drop the other stage from the run.

*(Deferred fast-follow modes — `staged`, `new`, `triage`, `resume` — are not in v1. If the user
asks for one, say it's not yet available and offer the closest v1 mode.)*

## Step 2 — initiate the match (skipped in backlog/status modes)

Unless the user chose **status** (short-circuit) or **backlog** (`--skip-canon`), run the match
step so the cache + suggestions queue are fresh:

```bash
aristo stamp            # default; matches new/changed annotations, fills the cache + queue
aristo stamp --skip-canon   # backlog mode: NO match — review only what's already queued
```

`aristo stamp` (or `aristo critique`) runs `POST /canon/match`; primaries land in
`canon-matches.toml`, dragged-in clusters land in the `.aristo/canon-suggestions-queue/`
(after the client-side dedup: drop members already bound / pending / rejected, and collapse
clusters sharing one objective into one task).

## Step 3 — open the session and show the ROOT menu

Read the at-a-glance counts (these gate the entry Q&A — new vs carried-over, D9):

```bash
aristo canon suggestions --counts
# → {"matches":{"new":N,"pending":N},"suggestions":{"new":N,"pending":N}}
```

If both channels are empty, report "nothing to review" and stop (no session needed).

Otherwise start the session (skip if Step 0 found a resumable one):

```bash
aristo session start intent-review --subject "<short description of the scope being reviewed>"
```

Then present the ROOT menu. **Every menu shows new vs pending counts in the option sub-line —
never silently hide capped items.**

```
ROOT — "Aristo Intent Suggestions — what would you like to do next?"
  1. Review canon matches        · "X new · Y pending"   → STAGE A
  2. Review canon suggestions     · "X new · Y pending"   → STAGE B
  3. Exit — leave everything pending
```

### The batch / multi-Q convention (used by both stages)

Same-kind items (N pending matches, N siblings) are presented as a **navigable batch** — one
`AskUserQuestion` call with multiple questions, where the user arrows left/right between items
and decides each, then you act on the whole batch. The picker caps at **4 questions per page**,
so larger sets **paginate** (e.g. 5 siblings = a page of 4 + a page of 1); say "page 1 of 2".

- Each **option** is an action; its **description** is the gray sub-line (counts, target site,
  confidence).
- An option's **preview** (monospace) carries the rich context: the `aristo canon show` card for
  decisions, or the code snippet for placements. Putting alternate sites on sibling options lets
  the user **compare candidate sites side-by-side** by arrowing between options.
- **Reuse, don't reinvent:** the decision card is literally the output of `aristo canon show <id>`
  — no bespoke card layout.

### The snippet rule (locked) — −/+ vs pure-+

- **Stage A accept = REWRITE** an annotation that already exists → render a `−`/`+` diff
  (your text out, canonical text in).
- **Stage B adopt = ADD** a new annotation → render a **pure `+`** addition, with surrounding
  lines shown as plain context (nothing is replaced).
- **The canonical text is fixed by canon.** "Edit text/site first" edits the *site* (or hands the
  site choice to the agent); it does **not** reword the canonical phrasing.

## Stage A — pending PRIMARY matches

Read the open primaries from the match cache:

```bash
aristo canon list      # the primary-match cache (your own annotations); NOT the suggestions queue
```

Present them as a **navigable batch** (≤4/page, paginate). For each match the menu offers:

```
per match:  Accept (rewrite + bind) · Reject (keep your wording) · Skip · Back
  preview on Accept = the source REWRITE as a −/+ diff (your text out, canonical in)
  preview on the card = aristo canon show <canon_id>
```

Record each decision against the session:

- **Accept**`aristo session decide --item match:<annotation-id>#<canon-id> --bucket accepted`.
  The kind's `on_accept` rewrites the annotation to canonical + binds (reuse of `aristo canon
  accept`). The annotation already exists — accept rewrites it.
- **Reject**`aristo session decide --item match:<annotation-id>#<canon-id> --bucket rejected
  [--note "..."]`. Fingerprints the `canon_id` so it's suppressed on future runs.
- **Skip / defer**`aristo session decide --item match:<annotation-id>#<canon-id> --bucket
  pending` (parks it; surfaces in the next session's backlog).

## Stage B — SUGGESTIONS, per cluster (parent-first, D6)

List the queued clusters (honor any `--filter` from the chosen mode):

```bash
aristo canon suggestions                                   # all queued clusters
aristo canon suggestions --filter parent=<kanon:objective> # cluster <objective> mode
aristo canon suggestions <objective>                       # one cluster's detail
```

**⟳ For each cluster task**, present the cluster gate. The **parent (objective) is decided once
per cluster** — this is the gate at the top of each cluster's review, asked once (there is no
global "re-decide the parent" loop):

```
cluster gate:  Explore cluster · Reject cluster · Skip · Back
  preview on Explore        = the cluster tree (✓ asserted / ☐ to consider), via aristo canon show
  preview on Reject cluster = "DISCARD dragged-in only / KEEP your independent match" (D6)
```

- **Reject cluster**`aristo session decide --item cluster:<key> --bucket rejected
  [--note "..."]`. This runs the **D6 cascade**: discard the cluster's DRAGGED-IN siblings only,
  but **KEEP any member that independently matched on its own** (already bound in the index, or
  pending/accepted in the cache). The seeding primary is never discarded. Discarded members are
  fingerprinted so they don't re-surface. Go to the next cluster.
- **Explore cluster**`aristo session decide --item cluster:<key> --bucket accepted`, then ADOPT
  the objective (see ADOPT below; if the objective already exists in source, skip it) and proceed
  to the three-phase sibling flow.

### The three-phase sibling flow (for an explored cluster)

**PHASE 1 — batch-DECIDE siblings** (multi-Q, navigable, ≤4/page):

```
per sibling:  Adopt · Reject · Skip      preview = aristo canon show <canon_id> card
```

Record each: Adopt is buffered (don't write yet); Reject →
`aristo session decide --item sibling:<key>#<canon-id> --bucket rejected [--note "..."]`
(fingerprints the canon_id). Siblings matching a prior rejection are auto-suppressed (dedup ④,
shared rejection log) — surface them in a separate "auto-rejected" group, don't clutter the main
list.

**PHASE 2 — agent batch-FINDS sites** for every "Adopt." For each adopted entry, the SDK exposes
its `applies_to` + `canonical_text` + `description` (via `aristo canon show <canon_id>`).
**Site-finding is YOUR job** (the agent): locate the load-bearing site(s) where the invariant
should be asserted. Propose a HIGH/MEDIUM-confidence primary site plus alternates.

**PHASE 3 — batch-CONFIRM placements** (multi-Q, navigable, ≤4/page):

```
per adopt:  Confirm — write here · Try a different site · Edit text/site first · Skip
  preview on Confirm         = surrounding code with the new intent as a PURE + addition
  preview on Different site   = the next candidate site (compare by arrowing)
  each candidate carries a confidence (HIGH/MEDIUM) in its sub-line
```

### ADOPT(entry) — the shared write micro-flow (D4)

Used for the parent objective AND each confirmed sibling. **Confirm → write → stamp → accept:**

1. `agent finds a candidate site (applies_to + canonical_text + description)`
2. **AskUserQuestion: confirm this site?***no* → re-find another site and re-ask; *yes*3. `agent writes #[aristo::intent(text=<canonical_text>, parent="<kanon:objective>")]` at the site
   (the canonical text verbatim — do NOT reword it).
4. `aristo stamp` — re-matches; the new annotation becomes a pending primary.
5. `aristo canon accept` — binds it.
6. Record `aristo session decide --item sibling:<key>#<canon-id> --bucket accepted --note "adopted"`.

Adoption funnels back through the **existing `stamp → /canon/match → canon accept` pipeline** —
there is no new source-mutation command; the agent writes code, the CLI manages state (the same
seam as the `aristo-authoring` skill).

## EXIT — close the session

When every item is decided, or the user picks "Exit / Stop":

- **All decided**`aristo session exit` (strict close; succeeds because every item has a bucket).
- **Stopped early**`aristo session exit --defer-undecided` (open items move to the per-kind
  **backlog**; the next session surfaces them — never silently dropped).
- **Abort entirely**`aristo session abort --yes` (destructive; discards every decision).

Run `aristo stamp` for any adopted/applied edits **after** the session closes — stamp is a
mutation and the session guard blocks it while a session is open.

## What this skill does NOT do

- It does NOT write `.aristo/` state files directly. Decisions go through `aristo session decide`;
  the suggestions queue and rejection log are SDK-owned.
- It does NOT mutate source on its own for an adoption — the agent writes the annotation, then
  the CLI's `stamp` + `canon accept` bind it.
- It does NOT reword canonical text. Adopt uses the canon's phrasing verbatim; "edit" edits the
  *site*, never the wording.
- It does NOT re-decide a cluster's parent in a loop. The parent is decided **once per cluster**.

## Anti-patterns

- ❌ Skipping Step 0's `aristo session active` check. Layer-3 enforcement is the last line of
  defense; some agents jump straight to a decision and bypass the SDK pre-check.
- ❌ Rendering a Stage B adopt as a ``/`+` diff. Adopt is a **pure `+`** addition — nothing of
  the user's is replaced. Only Stage A accept (rewrite of an existing annotation) is a diff.
- ❌ Inventing a bespoke decision card. The card is `aristo canon show <id>` output, reused.
- ❌ Cramming more than 4 questions onto one page. The picker caps at 4 — paginate and say
  "page 1 of 2".
- ❌ Writing an adopted annotation without the **confirm** AskUserQuestion. Every source edit needs
  explicit per-site confirmation before it lands.
- ❌ Rejecting a cluster and silently dropping a sibling the user already asserted. Reject-parent
  discards **dragged-in only** (D6); the SDK keeps any independently-matched member — trust it.
- ❌ Re-running the match in **backlog** mode. Backlog means `--skip-canon`: review only what's
  already queued.
- ❌ Opening a session for **status** mode. Status is `aristo canon suggestions --counts` only —
  no session, no writes.
- ❌ Starting an intent-review session and walking away without `exit` / `exit --defer-undecided`
  / `abort`. Every other aristo mutation refuses until the session closes; always reach a close.