aristo_cli/lib.rs
1//! Library form of the Aristo CLI. The `aristo` binary (`src/main.rs`) is
2//! a thin wrapper that calls [`run`] and exits with its return code.
3//!
4//! Splitting the CLI into a lib + tiny bin lets integration tests exercise
5//! `dispatch` directly without spawning a child process for every case
6//! (the `binary_smoke` test still spawns one, on purpose, as the canary
7//! for the binary's own glue).
8
9mod commands;
10mod data_plane;
11mod error;
12mod filter;
13/// The nudge/progress engine (Phase 18 #9). Public so the as-yet-unwired
14/// scorer is part of the lib API surface; the `aristo nudge` emitter (S0d)
15/// is its in-crate consumer.
16pub mod nudge;
17mod pipeline;
18mod session;
19mod skills;
20mod update_notify;
21mod workspace;
22
23pub use error::{CliError, CliResult};
24pub use filter::{Filter, FilterParseError};
25pub use workspace::{Workspace, WorkspaceError};
26
27use clap::{Parser, Subcommand};
28use std::path::PathBuf;
29use std::process::ExitCode;
30
31/// Aristo annotation SDK CLI.
32///
33/// Each subcommand handles one stage of the annotation lifecycle:
34/// authoring (`init`, `lang`, `install-skills`), indexing and stamping
35/// (`index`, `stamp`, `rename`), inspection (`show`, `list`, `status`,
36/// `graph`, `doc`, `badge`), quality gates (`lint`, `verify`,
37/// `critique`), review-session management (`session`), and canon
38/// binding against the Aretta server (`auth`, `canon`).
39#[derive(Parser, Debug)]
40#[command(
41 name = "aristo",
42 version,
43 about = "Aristo annotation SDK — write, verify, and document intent.",
44 long_about = None,
45)]
46struct Cli {
47 #[command(subcommand)]
48 command: Commands,
49}
50
51#[derive(Subcommand, Debug)]
52enum Commands {
53 /// Set up Aristo in this repo (creates `aristo.toml`, `.aristo/`,
54 /// pre-commit hook). Pass `--ci` / `--ci-verify` to also add CI workflows.
55 Init {
56 /// Modify Cargo.toml to add `aristo` as a dependency. Default
57 /// behavior just prints the dep line for the user to paste in.
58 #[arg(short, long)]
59 force: bool,
60
61 /// Also write a lite PR-gate workflow (`.github/workflows/aristo.yml`)
62 /// that runs audit/lint/doc via the shared aristo-action. No token needed.
63 #[arg(long)]
64 ci: bool,
65
66 /// Also write the nightly/manual verify workflow
67 /// (`.github/workflows/aristo-verify.yml`). Needs an `ARETTA_TOKEN` repo
68 /// secret (paid tier). Implies `--ci`.
69 #[arg(long)]
70 ci_verify: bool,
71
72 /// Install a local pre-commit hook (DEPRECATED). Off by default and no
73 /// longer auto-installed: the index is a gitignored cache, so CI
74 /// (`aristo verify --audit`) is the enforcement point. The hook only
75 /// runs `aristo doc` + `lint` locally as a convenience.
76 #[arg(long)]
77 hook: bool,
78 },
79
80 /// Print a syntax cheat sheet for the detected language.
81 Lang {
82 /// Detect language for a specific file. Currently only Rust
83 /// (`.rs`) is supported; other extensions error out.
84 #[arg(long)]
85 file: Option<PathBuf>,
86 },
87
88 /// Install Aristo skills for a coding agent (claude-code, cursor, codex, opencode, antigravity).
89 InstallSkills {
90 /// Target agent. Required unless `--list-agents` is used.
91 #[arg(long, value_name = "name")]
92 agent: Option<String>,
93 /// List supported agents and exit.
94 #[arg(long)]
95 list_agents: bool,
96 /// Install at user-level (e.g. ~/.claude/skills/) instead of project-level.
97 #[arg(long)]
98 user: bool,
99 /// Re-pin installed skills to this binary's version. With `--agent`,
100 /// re-installs that agent; without it, heals every already-installed
101 /// agent in place (both scopes, or just `--user`).
102 #[arg(long)]
103 update: bool,
104 /// Report whether installed skills are stale relative to this binary
105 /// (read-only). Exits non-zero if any are out of date.
106 #[arg(long)]
107 check: bool,
108 /// Internal: emit a SessionStart hook `additionalContext` block when
109 /// installed skills are stale (used by the hook this command installs).
110 #[arg(long, hide = true)]
111 hook_format: bool,
112 },
113
114 /// Reverse `install-skills`: remove SDK-bundled skills.
115 UninstallSkills {
116 /// Target agent. Required.
117 #[arg(long, value_name = "name")]
118 agent: String,
119 /// Uninstall from user-level instead of project-level.
120 #[arg(long)]
121 user: bool,
122 /// Override the "skip locally-modified" safety check.
123 #[arg(long)]
124 force: bool,
125 },
126
127 /// Scan source for annotations and write the index (`.aristo/index.toml`).
128 Index {
129 /// Force a full re-walk, ignoring the per-file mtime cache.
130 #[arg(long)]
131 all: bool,
132 },
133
134 /// Refresh the annotation index — pick up new annotations, detect
135 /// drift, and (when signed in) match against the Aretta canon.
136 Stamp {
137 /// CI mode: report whether `stamp` would change the index or
138 /// `.aristo/canon-matches.toml`, without writing either.
139 /// Exits non-zero if it would. Skips the canon-match step
140 /// too (no outbound network calls in this mode).
141 #[arg(long)]
142 check: bool,
143 /// Skip the canon-match step for this run. Doesn't disable
144 /// canon globally — set `[canon] enabled = false` in
145 /// `aristo.toml` for that. Useful when you're offline or want
146 /// a fast local stamp.
147 #[arg(long = "skip-canon")]
148 skip_canon: bool,
149 /// Invalidate the local canon-match cache and re-query every
150 /// annotation on this run. Equivalent to
151 /// `aristo canon refresh && aristo stamp`.
152 #[arg(long = "refresh-canon", conflicts_with = "skip_canon")]
153 refresh_canon: bool,
154 /// Garbage-collect archived orphan proofs after stamping. Removed
155 /// annotations' `.proof` files are normally *archived* (moved to
156 /// `.aristo/archive/proofs/`) so a stray stamp never loses a verdict;
157 /// `--gc` is the only path that hard-deletes them. No-op under
158 /// `--check` (CI must not mutate the workspace).
159 #[arg(long = "gc")]
160 gc: bool,
161 },
162
163 /// Look up an annotation by id, fn / mod / struct name, or file:line.
164 Show {
165 /// Selector: bare id, `fn <name>`, `mod <name>`, `struct <name>`,
166 /// `enum <name>`, `trait <name>`, or `<file>:<line>`.
167 selector: String,
168 /// Emit the entry as JSON instead of human-readable text.
169 #[arg(long, conflicts_with = "toml_out")]
170 json: bool,
171 /// Emit the entry as TOML (mirrors the on-disk index schema).
172 #[arg(long = "toml", conflicts_with = "json")]
173 toml_out: bool,
174 },
175
176 /// List every annotation in the index.
177 List {
178 /// Unified filter clause (`id=<id>`, `file=<path>`,
179 /// `parent=<id>`, `status=<state>`). Repeatable; multiple
180 /// `--filter` flags AND together.
181 #[arg(long = "filter", value_name = "key=value")]
182 filters: Vec<String>,
183 /// Emit a JSON array of records instead of human-readable text.
184 #[arg(long)]
185 json: bool,
186 },
187
188 /// Project-level summary (tier, counts, freshness).
189 Status,
190
191 /// Machine-readable project metrics (counts, unverified backlog, tier).
192 /// The same `Metrics` value the nudge engine computes internally.
193 Metrics {
194 /// Emit the metrics as a JSON object instead of a human summary.
195 #[arg(long)]
196 json: bool,
197 },
198
199 /// The nudge/progress engine. With no `--event`, prints what the engine
200 /// would surface right now (human introspection). With `--event`, runs as
201 /// a Claude Code hook emitter (`post-tool-use` / `user-prompt-submit` /
202 /// `session-start`) — always exits 0 so a nudge can never break the
203 /// agent's workflow. Agent-facing output uses `additionalContext` (the
204 /// only channel that reaches the model); `Stop` is intentionally not
205 /// served (its output never reaches the agent).
206 Nudge {
207 /// Hook event to serve (omit for the human readout).
208 #[arg(long)]
209 event: Option<String>,
210 },
211
212 /// Ambient one-line status segment for the Claude Code `statusLine`
213 /// (e.g. `aristo 3 review · 2 unverified · Apprentice`). Read-only;
214 /// prints nothing when there's no workspace or nudges are off.
215 Statusline,
216
217 /// Review newly-authored intents (#7). With no flags, lists what awaits
218 /// review (split into new-this-session vs backlog when a baseline exists).
219 /// `--mark <id>` records intents as reviewed once you've looked at them.
220 Review {
221 /// Mark these authored intents reviewed (repeatable; comma-lists OK).
222 /// The whole batch is validated before anything is written.
223 #[arg(long = "mark", value_name = "ID")]
224 mark: Vec<String>,
225 /// Emit the review snapshot (or mark result) as JSON instead of a
226 /// human summary.
227 #[arg(long)]
228 json: bool,
229 },
230
231 /// Check annotation prose for quality issues (rule-based; no LLM).
232 Lint {
233 /// Read-only mode: exit non-zero on `error` findings (or
234 /// `warn` with `--strict`); never modifies source.
235 #[arg(long)]
236 check: bool,
237 /// Apply auto-fixable lint rules (whitespace only) to source
238 /// files in place.
239 #[arg(long, conflicts_with = "check")]
240 fix: bool,
241 /// Treat `warn` severity as failure too (only meaningful with `--check`).
242 #[arg(long)]
243 strict: bool,
244 },
245
246 /// Run verification for every annotation that opted in.
247 Verify {
248 /// Unified filter clause (`id=<id>`, `file=<path>`,
249 /// `parent=<id>`, `status=<state>`). Repeatable; multiple
250 /// `--filter` flags AND together.
251 #[arg(long = "filter", value_name = "key=value")]
252 filters: Vec<String>,
253 /// Re-verify entries that are already in a clean verified
254 /// state. By default they're skipped; pass `--rerun` to force
255 /// a re-check.
256 #[arg(long)]
257 rerun: bool,
258 /// CI mode: report whether any status would change, without
259 /// writing the index. Exits non-zero if any change is needed.
260 #[arg(long)]
261 check: bool,
262 /// Treat warn-severity verification outcomes as failure too.
263 #[arg(long)]
264 strict: bool,
265 /// Audit mode: regenerate the index in memory and validate every
266 /// `.aristo/proofs/<id>.proof` against current source. Reports
267 /// fresh / stale / refuted / orphan counts; with `--strict`, exits
268 /// non-zero on any stale, counterexample, or orphan proof. The CI
269 /// freshness gate (replaces `aristo stamp --check`) — no dispatch,
270 /// no network, no token.
271 #[arg(long = "audit")]
272 audit: bool,
273 /// Apply pending verdict files in `.aristo/proofs/` to the
274 /// index. Reads every `<id>.proof`, runs the mechanical
275 /// validator, and (if it passes) flips the entry's status.
276 /// Skips dispatch of new verifications when set.
277 #[arg(long = "apply-verdicts", conflicts_with = "submit_verdict")]
278 apply_verdicts: bool,
279 /// Migration only: ignore any agent-stamped ground hashes in
280 /// the `.proof` files and recompute them from the cited file
281 /// ranges and index entries. Use this once when migrating
282 /// from older proof files that recorded hashes the SDK now
283 /// fills in itself. Without this flag, a stamped hash that
284 /// mismatches the current source is reported as staleness
285 /// and the proof is rejected. Only meaningful with
286 /// `--apply-verdicts`.
287 #[arg(long = "rewrite-hashes", requires = "apply_verdicts")]
288 rewrite_hashes: bool,
289 /// **Internal — invoked by the verification skill.** Submit
290 /// a single verdict: parse the JSON payload, validate it,
291 /// and (on pass) atomically write `.aristo/proofs/<id>.proof`.
292 /// Prints `accepted: sha256:<hex>` on success; structured
293 /// errors on reject. Agents never write `.proof` files
294 /// directly — the SDK is the sole writer.
295 #[arg(long = "submit-verdict", requires = "id", requires = "json")]
296 submit_verdict: bool,
297 /// Annotation id this verdict is about. Required with
298 /// `--submit-verdict`. The `.proof` file lands at
299 /// `.aristo/proofs/<id>.proof` (with `:` rewritten to `__`).
300 #[arg(long = "id", requires = "submit_verdict")]
301 id: Option<String>,
302 /// JSON-serialized ProofFile body. Required with
303 /// `--submit-verdict`. Pass as a single-quoted shell string;
304 /// the SDK parses it into a ProofFile and rejects anything
305 /// the validator would reject. Same schema as the TOML body
306 /// written on accept.
307 #[arg(long = "json", requires = "submit_verdict")]
308 json: Option<String>,
309 /// **Internal — invoked by the verification skill.**
310 /// Atomically claim one task from the pending queue and
311 /// print its TOML body to stdout. Empty stdout means the
312 /// queue is drained (exit 0 either way). Verify workers are
313 /// single-shot — call once, process the task, exit — so
314 /// context doesn't carry between verifications. The
315 /// orchestrator runs N workers in parallel and uses
316 /// `--queue-status` to decide when to spawn the next wave.
317 #[arg(long = "pop-next", conflicts_with_all = ["apply_verdicts", "submit_verdict", "queue_status"])]
318 pop_next: bool,
319 /// Peek at queue state without claiming. Prints `pending: N`,
320 /// `claimed: M` to stdout, exit 0. Used by the orchestrator
321 /// to decide whether to dispatch another wave of workers.
322 /// Safe to call concurrently.
323 #[arg(long = "queue-status", conflicts_with_all = ["apply_verdicts", "submit_verdict"])]
324 queue_status: bool,
325 /// Block until the canon-verify session reaches a terminal
326 /// state, rendering a snapshot at each long-poll return and
327 /// emitting a `still running…` heartbeat every 60s. Exit code
328 /// is derived from the final summary: `0` iff every
329 /// annotation is `verified` or `no_coverage`. Without
330 /// `--wait` the SDK detaches after dispatch (prints session
331 /// id and exits 0). Combine with `--view <id>` to attach to
332 /// a session another invocation started.
333 #[arg(long = "wait")]
334 wait: bool,
335 /// Re-attach to a previously-dispatched canon-verify session
336 /// by id. Skips the source eligibility scan, push-first
337 /// precheck, and POST — just GETs the session state and
338 /// renders. Combine with `--wait` to block until terminal.
339 #[arg(long = "view", value_name = "SESSION_ID")]
340 view: Option<String>,
341 /// Subset the canon-verify dispatch to the listed annotation
342 /// ids. Comma-separated; each id must be a canon-bound entry
343 /// (`aristos:foo` or `kanon:bar`) in the workspace's index.
344 /// Bare canon-id suffixes (e.g. `foo`) are accepted as a
345 /// shorthand. `arta_*` (server-side opaque) ids are rejected
346 /// — those are not user-facing.
347 #[arg(long = "tags", value_name = "id1,id2,...", value_delimiter = ',')]
348 tags: Vec<String>,
349 /// Phase 16 (c): record a user-side known-failure waiver for a
350 /// canon-bound property your code currently violates. Write-only
351 /// — does NOT dispatch a verification. Requires `--because`. The
352 /// gap lands in `.aristo/expectations.toml` (commit it); at
353 /// verify time it renders as a "known gap (accepted)" instead of
354 /// a failure, and the strict ratchet flips it back to red if the
355 /// property ever starts passing — so stale waivers can't rot.
356 /// Accepts a bare canon-id suffix (e.g. `foo`) as shorthand.
357 #[arg(
358 long = "accept",
359 value_name = "CANON_ID",
360 requires = "because",
361 conflicts_with_all = [
362 "view", "wait", "tags", "rerun", "check", "strict", "filters",
363 "apply_verdicts", "rewrite_hashes", "submit_verdict", "id",
364 "json", "pop_next", "queue_status"
365 ]
366 )]
367 accept: Option<String>,
368 /// Reason the gap is accepted (mandatory with `--accept`).
369 /// Recorded verbatim and shown on the verify card. A reasonless
370 /// waiver is how baselines rot, so it is required.
371 #[arg(long = "because", value_name = "REASON", requires = "accept")]
372 because: Option<String>,
373 /// Optional tracking reference (issue URL, ticket id) for the
374 /// accepted gap. Only meaningful with `--accept`.
375 #[arg(long = "tracking", value_name = "REF", requires = "accept")]
376 tracking: Option<String>,
377 },
378
379 /// Run the critique skill against annotation prose — opinionated
380 /// suggestions, severity-tagged findings.
381 Critique {
382 /// Unified filter clause (`id=<id>[,<id>,...]`,
383 /// `file=<path>`, `parent=<id>`, `status=<state>`). Repeatable;
384 /// multiple `--filter` flags AND together; values may be
385 /// comma-separated. **REQUIRED** — `aristo critique` with no
386 /// filter errors with usage. To sweep every annotation in the
387 /// index, opt in explicitly with `--all --yes`.
388 #[arg(long = "filter", value_name = "key=value")]
389 filters: Vec<String>,
390 /// Apply pending critique files in `.aristo/critiques/` —
391 /// re-validate every `<id>.critique` and print a summary
392 /// grouped by id. Defaults to listing only findings whose
393 /// `disposition` is `None` (open / not yet reviewed); pass
394 /// `--include-closed` for the full view including findings
395 /// already triaged via `aristo session decide`.
396 #[arg(long = "apply-findings", conflicts_with_all = ["submit_findings", "pop_next", "queue_status"])]
397 apply_findings: bool,
398 /// Include findings whose `disposition` has been set (Accepted /
399 /// Rejected / Deferred) in the `--apply-findings` summary.
400 /// By default only open findings are listed — closed ones
401 /// stop re-surfacing on every apply, which is how a review
402 /// closes the loop. Only meaningful with `--apply-findings`.
403 #[arg(long = "include-closed", requires = "apply_findings")]
404 include_closed: bool,
405 /// Force re-enqueue of every matched annotation, bypassing the
406 /// `last_critiqued_at_text_hash` cache. Default behavior skips
407 /// annotations whose text hasn't drifted since the cached
408 /// critique was produced (so re-runs of `aristo critique
409 /// --filter id=X` are free when X is unchanged).
410 #[arg(long = "rerun")]
411 rerun: bool,
412 /// Restrict scope to annotations in files git-staged for the
413 /// next commit (`git diff --cached --name-only`). Useful for
414 /// pre-commit hook integration. Satisfies the filter-required
415 /// guard on its own; composes with explicit `--filter`
416 /// clauses via intersection (annotations must match BOTH
417 /// `--filter` and appear in the staged set).
418 #[arg(long = "staged")]
419 staged: bool,
420 /// Sweep every intent annotation that has a real `verify`
421 /// method (skips documentation-only `verify = false`). Loud
422 /// on purpose: prints `(this will enqueue N annotations;
423 /// ~$X cost — proceed with --all --yes?)` and exits 2 unless
424 /// you also pass `--yes`. Without the confirmation, an agent
425 /// could accidentally fire hundreds of LLM calls in one go.
426 #[arg(long = "all", conflicts_with_all = ["filters", "staged"])]
427 all: bool,
428 /// Skip the confirmation prompt for `--all`. Required
429 /// alongside `--all` to actually enqueue the sweep; without
430 /// it `--all` just prints the cost estimate and exits 2.
431 #[arg(long = "yes", requires = "all")]
432 yes: bool,
433 /// **Internal — invoked by the critique skill.** Atomically
434 /// claim one task from the critique queue and print its TOML
435 /// body to stdout. Empty stdout means the queue is drained
436 /// (exit 0 either way). Unlike verify, critique workers loop
437 /// on this call — the tasks are shallow and vocabulary stays
438 /// consistent when one worker handles several.
439 #[arg(long = "pop-next", conflicts_with_all = ["apply_findings", "submit_findings", "queue_status"])]
440 pop_next: bool,
441 /// Peek at queue state without claiming. Prints `pending: N`
442 /// + `claimed: M` to stdout, exit 0.
443 #[arg(long = "queue-status", conflicts_with_all = ["apply_findings", "submit_findings"])]
444 queue_status: bool,
445 /// **Internal — invoked by the critique skill.** Submit a
446 /// single critique: parse the JSON payload, validate it, and
447 /// (on accept) atomically write
448 /// `.aristo/critiques/<id>.critique`. Prints
449 /// `accepted: sha256:<hex>` on success.
450 #[arg(long = "submit-findings", requires = "id", requires = "json")]
451 submit_findings: bool,
452 /// Annotation id this submission is about. Required with
453 /// `--submit-findings`.
454 #[arg(long = "id", requires = "submit_findings")]
455 id: Option<String>,
456 /// JSON-serialized CritiqueFile body. Required with
457 /// `--submit-findings`.
458 #[arg(long = "json", requires = "submit_findings")]
459 json: Option<String>,
460 },
461
462 /// Generate per-annotation markdown to .aristo/doc/.
463 Doc {
464 /// Write only the crate-root summary (`_summary.md`); skip the
465 /// per-annotation pass.
466 #[arg(long)]
467 summary: bool,
468 /// Include each annotation's current verification status in
469 /// the rendered markdown. Status is a build-time snapshot
470 /// that drifts as code evolves; the default omits it so doc
471 /// artifacts stay reproducible on a clean checkout.
472 #[arg(long = "include-status")]
473 include_status: bool,
474 /// CI mode: recompute expected per-annotation MD from the index,
475 /// compare against `.aristo/doc/`, exit non-zero on drift. Never
476 /// writes.
477 #[arg(long)]
478 check: bool,
479 /// Composite: also generate the annotation graph (Mermaid)
480 /// and embed it inline in `_summary.md`. Implies `--summary`.
481 /// Conflicts with `--check` (read-only mode can't write the
482 /// graph block).
483 #[arg(long = "include-graph", conflicts_with = "check")]
484 include_graph: bool,
485 },
486
487 /// Generate the annotation graph (Mermaid / DOT / SVG).
488 Graph {
489 /// Output format. `mermaid` (default) emits a fenced
490 /// flowchart TD block; `dot` emits Graphviz DOT; `svg`
491 /// requires `dot` on PATH and shells out to render.
492 #[arg(long, default_value = "mermaid")]
493 format: String,
494 /// Write to this path instead of stdout. Atomic via
495 /// temp-file + rename. Relative paths resolve against the
496 /// invoking directory.
497 #[arg(long)]
498 out: Option<PathBuf>,
499 /// Unified filter clause (`id=<id>`, `file=<path>[:<LO>-<HI>]`,
500 /// `parent=<id>`, `status=<state>`). Repeatable; multiple
501 /// `--filter` flags AND together. With no filter, the scope
502 /// is the whole index.
503 #[arg(long = "filter", value_name = "key=value")]
504 filters: Vec<String>,
505 /// Drop `assume` nodes from the rendered graph. They're
506 /// included by default because assumes describe the
507 /// background facts your intents rely on — dropping them by
508 /// default would hide those.
509 #[arg(long = "exclude-assumes")]
510 exclude_assumes: bool,
511 /// Walk N hops from each filter-matched node in both
512 /// directions (ancestors + descendants) and include them in
513 /// the rendered graph. Useful for "show me this annotation
514 /// plus some context". Only meaningful with `--filter`;
515 /// without a filter, the scope is already the whole index.
516 #[arg(long, value_name = "N")]
517 depth: Option<u32>,
518 /// Include intent nodes that have no parent and no children.
519 /// They're omitted by default — usually they're standalone
520 /// claims that don't add structure to the rendered graph.
521 /// Assumes are always included (see `--exclude-assumes` for
522 /// that opt-out).
523 #[arg(long = "include-orphans")]
524 include_orphans: bool,
525 /// Color nodes by their current verification status instead
526 /// of by `verify` level (verified=green / tested=blue /
527 /// neural=yellow / stale=orange / orphan=purple /
528 /// forged=red+border / unknown=gray /
529 /// counterexample=red+border / inconclusive=red+border /
530 /// pending-deepen=gray). The `verify` level moves to the
531 /// in-node label. Use when you want to see what's still
532 /// unverified.
533 #[arg(long = "include-status")]
534 include_status: bool,
535 },
536
537 /// Generate a shareable SVG verification badge for README / docs.
538 Badge {
539 /// Write SVG to this path (relative to workspace root, or absolute).
540 /// Default: stdout.
541 #[arg(long)]
542 out: Option<PathBuf>,
543 /// Badge style: `flat-square` (default), `flat`, or `plastic`.
544 #[arg(long, default_value = "flat-square")]
545 style: String,
546 /// Which metric the SVG value half displays. `tier` (default,
547 /// the locked D7 score → D8 tier) is the headline signal;
548 /// `count` and `rate` preserve the slice-31 surfaces for
549 /// projects that prefer the simpler counters.
550 #[arg(long, default_value = "tier")]
551 metric: String,
552 },
553
554 /// Rename an annotation id everywhere it appears — source files,
555 /// index, and doc artifacts. Either every change lands or none do.
556 ///
557 /// Supported renames: bare → bare, and stamp-assigned opaque
558 /// (`aret_*`) → bare. Canon-bound prefixes (`aristos:` / `kanon:`)
559 /// are rejected in either direction — those prefixes are applied
560 /// by `aristo canon accept` and removed by `aristo canon unbind`.
561 /// The new id cannot itself be an opaque `aret_*` id (those are
562 /// stamp-assigned only).
563 Rename {
564 /// Annotation id to rename FROM. Must exist in the current
565 /// `.aristo/index.toml`.
566 old_id: String,
567 /// Annotation id to rename TO. Must not already exist and must
568 /// not use the reserved `aret_*` / `aristos:` / `kanon:`
569 /// prefixes.
570 new_id: String,
571 /// Compute and print the rename plan (source edits + per-id
572 /// artifact moves + index updates) without writing anything.
573 #[arg(long = "dry-run")]
574 dry_run: bool,
575 },
576
577 /// Run a review session over a pipeline's open artifacts —
578 /// critique findings, proof verdicts, and so on. Start it,
579 /// inspect bucket counts, record decisions, and close out.
580 Session {
581 #[command(subcommand)]
582 action: SessionAction,
583 },
584
585 /// Sign in to the Aretta canon API. Required for `aristo stamp`
586 /// and `aristo critique` to see canon matches on the Pro /
587 /// Enterprise tiers.
588 Auth {
589 #[command(subcommand)]
590 action: AuthAction,
591 },
592
593 /// Manage canon bindings: accept or reject pending matches,
594 /// inspect or refresh the local cache, unbind bound ids, and
595 /// request a verifier for a canon entry.
596 Canon {
597 #[command(subcommand)]
598 action: CanonAction,
599 },
600}
601
602/// Subcommands under `aristo auth`. Each operates on the persistent
603/// credentials store under `$XDG_CONFIG_HOME/aristo/credentials`
604/// (or the platform default per `aristo_core::auth`).
605#[derive(clap::Subcommand, Debug)]
606pub(crate) enum AuthAction {
607 /// Authenticate against the Aretta proxy.
608 ///
609 /// **Default mode (GitHub OAuth):** the CLI fetches the GitHub
610 /// authorization URL from the proxy, tries to open it in your
611 /// browser, and prompts you to paste the code shown on the
612 /// proxy's callback page. The proxy then mints an `arta_*`
613 /// token scoped to your `(user, repo)` pair.
614 ///
615 /// **Bypass modes (for CI / scripting):**
616 ///
617 /// - **`--stdin`** — read the raw token from stdin
618 /// (`echo "$TOKEN" | aristo auth login --stdin`).
619 /// - **`--token=<T>`** — use the literal token value.
620 ///
621 /// The token is persisted to `$XDG_CONFIG_HOME/aristo/credentials`
622 /// with `0600` Unix permissions.
623 Login {
624 /// Read the token from stdin (consumes entire stdin). Skips
625 /// the OAuth flow.
626 #[arg(long, conflicts_with = "token")]
627 stdin: bool,
628 /// Use this token directly. Skips the OAuth flow.
629 #[arg(long, value_name = "TOKEN")]
630 token: Option<String>,
631 /// Aretta server to authenticate against. Accepts:
632 /// `prod` / `production` (= https://code.aretta.ai, default),
633 /// `dev` / `development` / `staging` (= https://dev.aretta.ai),
634 /// or a full URL for self-hosted deployments
635 /// (`https://aretta.example.com`).
636 #[arg(long, default_value = "prod")]
637 server: String,
638 /// Repo to scope the OAuth-minted token to (`owner/repo`).
639 /// Defaults to auto-deriving from `<cwd>/.git/config`'s
640 /// `remote.origin.url`. Required for non-git directories or
641 /// when the remote isn't a GitHub URL. Ignored in `--stdin` /
642 /// `--token` bypass modes (where the token is supplied
643 /// directly with its server-side scope already set).
644 #[arg(long, value_name = "OWNER/REPO")]
645 repo: Option<String>,
646 },
647 /// Show the current authentication state. Never prints the token
648 /// itself — only its source (env var, credentials file, or none).
649 /// Handy for sanity-checking before running `aristo stamp`.
650 Status,
651 /// Print the resolved `arta_*` token to stdout — the `ARETTA_TOKEN`
652 /// env var if set, else the on-disk credentials file. Nothing else is
653 /// printed, so it pipes cleanly to your clipboard, e.g.
654 /// `aristo auth token | pbcopy` (macOS) or
655 /// `aristo auth token | xclip -selection clipboard` (Linux). Handy for
656 /// setting the `ARETTA_TOKEN` CI secret. Errors if not authenticated.
657 Token,
658 /// Remove the stored credentials file. Idempotent — running
659 /// `logout` when not logged in is not an error.
660 Logout,
661}
662
663/// Subcommands under `aristo canon`.
664#[derive(clap::Subcommand, Debug)]
665pub(crate) enum CanonAction {
666 /// Accept a pending canon match: rewrite source to use the
667 /// canonical text + apply the `aristos:` / `kanon:` prefix to
668 /// the annotation id, update the index entry's binding state
669 /// to `Bound`, and move the cache entry from `pending_matches`
670 /// to `accepted_matches`.
671 ///
672 /// Both arguments are required: the bare annotation id as it
673 /// appears in `.aristo/index.toml` (NOT prefixed; the prefix
674 /// is applied by accept) and the bare canon id from the
675 /// pending match (e.g. `cell_written_exactly_once_per_page_edit`).
676 Accept {
677 /// Annotation id whose pending match you're accepting. Use
678 /// the bare form (no `aristos:` / `kanon:` prefix); the
679 /// prefix is applied by the accept itself based on the
680 /// pending match's `prefix_tier`.
681 annotation_id: String,
682 /// Canon id from the pending match (also bare — no
683 /// prefix). The pair `(annotation_id, canon_id)` locates
684 /// the exact pending match in `.aristo/canon-matches.toml`.
685 canon_id: String,
686 },
687
688 /// Reject a pending canon match: move the entry from
689 /// `pending_matches` to `rejected_matches`, pinned to the
690 /// current annotation `text_hash`. The rejection keeps the same
691 /// `(canon_id, text_hash)` pair from re-surfacing on future
692 /// `aristo stamp` runs; once the annotation text changes, the
693 /// rejection no longer applies and the match is re-evaluated.
694 /// Source and index are not touched — rejection is a cache-only
695 /// operation.
696 Reject {
697 /// Annotation id whose pending match you're rejecting.
698 annotation_id: String,
699 /// Canon id from the pending match.
700 canon_id: String,
701 /// Optional note recorded with the rejection. Useful for
702 /// capturing the *why* (e.g. "this canon entry is too broad",
703 /// "wrong category") for whoever revisits it later.
704 #[arg(long = "reason")]
705 reason: Option<String>,
706 },
707
708 /// List the current canon match state: one line per annotation
709 /// with pending / accepted / rejected counts, plus per-bucket
710 /// detail lines for each match. Reads `.aristo/canon-matches.toml`;
711 /// does not call the canon API.
712 List,
713
714 /// Fetch the canon entry detail for `<canon_id>` via the canon
715 /// API and render the longer description + example + references.
716 /// For the full trust card (server description + local binding
717 /// state combined), use `aristo show <bound_id>` instead.
718 Show {
719 /// Bare canon id (no `aristos:` / `kanon:` prefix). The
720 /// server's `GET /canon/entry/<canon_id>` endpoint returns
721 /// the same entry regardless of which tier you'd bind into;
722 /// the prefix is a per-user, per-scope attribute.
723 canon_id: String,
724 /// Optional explicit version (`v<minor>.<patch>`). Omit to
725 /// get the catalog's currently active version.
726 #[arg(long = "version")]
727 version: Option<String>,
728 },
729
730 /// Re-query the canon API for every annotation in the index,
731 /// bypassing the local match cache. Equivalent to
732 /// `aristo stamp --refresh-canon` without the rest of the stamp
733 /// pipeline — no source walk, no drift check, no index rewrite.
734 /// Useful when you know a new catalog version has shipped and
735 /// want fresh matches without a full stamp.
736 Refresh,
737
738 /// Reverse of `aristo canon accept`: strip the `aristos:` /
739 /// `kanon:` prefix from a canon-bound annotation, revert its
740 /// binding to `Local`, and drop the accepted_matches cache
741 /// entry. Source is rewritten in place (only the `id =` value
742 /// changes; canonical text + verify + parent are preserved).
743 /// The next `aristo stamp` may re-pull a fresh pending match
744 /// against the same annotation text.
745 ///
746 /// Unbind is for LIVE annotations. If the annotation was deleted
747 /// from source, no unbind is needed: the next `aristo stamp`
748 /// prunes its `.aristo/canon-matches.toml` entry automatically.
749 Unbind {
750 /// Canon-bound annotation id including the prefix (e.g.
751 /// `aristos:cell_written_exactly_once_per_page_edit`).
752 prefixed_id: String,
753 },
754
755 /// Record a verification-demand signal against a canon entry.
756 /// Idempotent on `(canon_id, repo, user)` — repeated calls don't
757 /// pile up. Use when an annotation is bound at the `kanon:` tier
758 /// and you'd like Aretta to invest in a verifier for that canon
759 /// entry.
760 RequestVerify {
761 /// Canon id (no prefix). The same id the trust card shows,
762 /// or that `aristo canon list` reports.
763 canon_id: String,
764 /// Optional note to attach to the demand signal (e.g.
765 /// "critical for our financial-tx audit"). A repeat call
766 /// with a new note replaces the previous one server-side.
767 #[arg(long = "notes")]
768 notes: Option<String>,
769 },
770
771 /// Report per-binding version drift between the local cache and
772 /// the canon API. Reports three classes: `current` (no change),
773 /// `patch-bump` (same canon_id, newer version — recommended
774 /// action: `aristo canon refresh`), and `minor-bump` (canon_id
775 /// retired — recommended action: `aristo canon unbind <id>` then
776 /// re-stamp). Currently diagnostic-only; automatic patch-bump
777 /// application is planned.
778 Migrate,
779
780 /// Download the canon catalogue (the full list of available canon
781 /// entries) to `.aristo/catalogue.json` — a gitignored local
782 /// snapshot — and print a summary. Requires authentication; served
783 /// by the per-repo conductor addressed via `[instance] url`.
784 Catalogue,
785
786 /// Run the S2 presence probe against a local SUT checkout: union
787 /// the instrumentation bundles carried by this repo's accepted
788 /// canon matches, generate an ephemeral probe crate (one
789 /// type-checked accessor call per record), `[patch]` the SUT
790 /// package to `--sut-path`, and `cargo check` it twice — with the
791 /// bundle's instrumentation features on (presence) and off (the
792 /// accessors must vanish; gating check). Failures are classified
793 /// per accessor — missing (rustc E0599/E0609) vs
794 /// SUT_FEATURE_UNDECLARED (cargo feature resolution) — and
795 /// rendered as escalation cards naming the conformance catch that
796 /// needs the accessor. Reuses the SUT's Cargo.lock + target/ so a
797 /// warm probe compile takes seconds. Fully offline: reads
798 /// `.aristo/canon-matches.toml`; never calls the canon API.
799 Probe {
800 /// Path to the local SUT checkout root (e.g. your
801 /// aretta-ai/turso fork clone). The probe `[patch]`es the SUT
802 /// package to this path and docks into its build graph
803 /// (Cargo.lock, target/, toolchain file).
804 #[arg(long = "sut-path", value_name = "DIR")]
805 sut_path: PathBuf,
806 /// The escalation card's `[ flag-broken ]` decision: run the
807 /// probe, verify this accessor still fails, and append the
808 /// rustc evidence to `.aristo/instrumentation-debt.jsonl`
809 /// (committed, append-only). Flagged accessors are re-surfaced
810 /// as `SKIPPED(instr-debt: <id>)` on later runs — visible
811 /// debt, never silently green.
812 #[arg(
813 long = "flag-broken",
814 value_name = "ACCESSOR_ID",
815 conflicts_with = "gen_only"
816 )]
817 flag_broken: Option<String>,
818 /// Keep the generated probe crate directory (under the SUT
819 /// target dir) instead of removing it after the run.
820 #[arg(long = "keep-probe")]
821 keep_probe: bool,
822 /// Generate and write the probe crate, then stop before
823 /// compiling (implies --keep-probe). Useful for inspecting
824 /// exactly what the probe will check.
825 #[arg(long = "gen-only")]
826 gen_only: bool,
827 },
828
829 /// List or inspect queued §17 proof-tree suggestions (the related
830 /// canon entries dragged in alongside a primary match). Read-only:
831 /// it does not open a review session or mutate the queue. To review
832 /// and adopt suggestions, run `aristo session start intent-review`.
833 ///
834 /// With no argument, lists every queued cluster. With an
835 /// `<objective>` (the cluster's objective canon_id, or the seeding
836 /// primary id for siblings-only clusters), shows that cluster's
837 /// detail. With `--counts`, emits a machine-readable JSON summary
838 /// `{matches:{new,pending}, suggestions:{new,pending}}` for the
839 /// menu / skill entry Q&A.
840 Suggestions {
841 /// Objective canon_id (or seeding primary id) of the cluster to
842 /// show. Omit to list all queued clusters.
843 objective: Option<String>,
844 /// Emit counts only (`{matches:{...}, suggestions:{...}}`) as
845 /// JSON; no per-cluster detail. Mutually exclusive with
846 /// passing an `<objective>`.
847 #[arg(long = "counts", conflicts_with = "objective")]
848 counts: bool,
849 /// Scope the listing with the shared `--filter` grammar
850 /// (`id=`, `file=[:LO-HI]`, `parent=`, `status=`). The
851 /// `cluster <objective>` mode (§6B) is `--filter
852 /// parent=<kanon:objective>` — it scopes the list to one proof
853 /// cluster (leaves carry `parent=`). Multiple `--filter` flags
854 /// AND together. No new grammar — reuses `filter.rs` verbatim.
855 #[arg(
856 long = "filter",
857 value_name = "KEY=VALUE",
858 conflicts_with = "objective"
859 )]
860 filter: Vec<Filter>,
861 },
862}
863
864/// Subcommands under `aristo session`. Each maps to one substrate
865/// operation; per-kind side effects (e.g. mutating a `.critique`
866/// file on accept) plug in via the `SessionKind` trait wired in
867/// step 5.
868#[derive(clap::Subcommand, Debug)]
869pub(crate) enum SessionAction {
870 /// Begin a new review session of the given kind. Fails if a
871 /// session is already active — pass `--allow-nesting` to override
872 /// (currently no kind allows nesting).
873 Start {
874 /// Session kind (`critique-review`, `proof-review`).
875 kind: String,
876 /// Display label for the artifact under review (e.g.
877 /// `src/critique/pending.rs` or
878 /// `proof:balance_no_duplicate_cells`).
879 #[arg(long = "subject")]
880 subject: String,
881 /// Override the kind's default nesting policy. Currently only
882 /// `Disallow` is implemented; the flag is reserved for future
883 /// per-kind opt-ins.
884 #[arg(long = "allow-nesting", default_value_t = false)]
885 allow_nesting: bool,
886 },
887 /// Print the active session id (or empty stdout if none).
888 /// Exit 0 either way.
889 Active {
890 /// Emit the full `<system-reminder>` block instead of just
891 /// the id — for the `UserPromptSubmit` hook installed by
892 /// `aristo install-skills`. Empty stdout when no session
893 /// is active (the hook then injects nothing).
894 #[arg(long = "hook-format", default_value_t = false)]
895 hook_format: bool,
896 },
897 /// Print bucket counts + open items for the active session.
898 /// Exit 0; errors out if no session is active.
899 Status,
900 /// Record a decision on one item in the active session.
901 Decide {
902 /// Item reference (`<id>#<index>` for indexed items, or any
903 /// opaque per-kind string).
904 #[arg(long = "item")]
905 item: String,
906 /// Which bucket the item lands in.
907 #[arg(long = "bucket", value_enum)]
908 bucket: BucketArg,
909 /// Optional note recorded with the decision.
910 #[arg(long = "note")]
911 note: Option<String>,
912 },
913 /// Close the active session. Strict by default — errors out if
914 /// any items are still in the open bucket.
915 Exit {
916 /// Move open items to the per-kind backlog instead of
917 /// erroring. Items are never silently dropped; the next
918 /// session of this kind surfaces them via the backlog menu.
919 #[arg(long = "defer-undecided", default_value_t = false)]
920 defer_undecided: bool,
921 },
922 /// Cancel the session and discard every decision recorded so far.
923 /// Requires `--yes` to skip the confirmation prompt.
924 Abort {
925 /// Skip the confirmation prompt.
926 #[arg(long = "yes", default_value_t = false)]
927 yes: bool,
928 },
929 /// List the active session and the most recent N closed sessions.
930 List {
931 /// Maximum number of closed-session rows to include.
932 #[arg(long = "limit", default_value_t = 10)]
933 limit: usize,
934 },
935}
936
937/// User-facing bucket choices for `aristo session decide`. Maps to
938/// the substrate's [`session::types::ItemStatus`] (minus `Open`,
939/// which is the implicit pre-decision state).
940#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
941pub(crate) enum BucketArg {
942 Accepted,
943 Rejected,
944 Pending,
945}
946
947/// Process entry point. Parses `argv`, dispatches to the chosen subcommand,
948/// and returns the exit code. Prints `error: <msg>` to stderr on any
949/// `CliError`.
950pub fn run() -> ExitCode {
951 let cli = Cli::parse();
952 let code = match dispatch(cli.command) {
953 Ok(()) => ExitCode::SUCCESS,
954 Err(e) => {
955 if !e.is_silent() {
956 eprintln!("error: {e}");
957 }
958 ExitCode::from(e.exit_code())
959 }
960 };
961 // Best-effort "newer aristo available" notice, printed after the
962 // command's own output and never affecting its exit code. Silent in
963 // CI / pipes / on opt-out (see `update_notify`).
964 update_notify::maybe_notify();
965 code
966}
967
968/// Maps a parsed `Commands` variant to its handler.
969fn dispatch(cmd: Commands) -> CliResult<()> {
970 match cmd {
971 Commands::Init {
972 force,
973 ci,
974 ci_verify,
975 hook,
976 } => commands::init::run(force, ci, ci_verify, hook),
977 Commands::Lang { file } => commands::lang::run(file),
978 Commands::InstallSkills {
979 agent,
980 list_agents,
981 user,
982 update,
983 check,
984 hook_format,
985 } => {
986 commands::install_skills::install(agent, list_agents, user, update, check, hook_format)
987 }
988 Commands::UninstallSkills { agent, user, force } => {
989 commands::install_skills::uninstall(agent, user, force)
990 }
991 Commands::Index { all } => commands::index::run(all),
992 Commands::Stamp {
993 check,
994 skip_canon,
995 refresh_canon,
996 gc,
997 } => commands::stamp::run(check, skip_canon, refresh_canon, gc),
998 Commands::Show {
999 selector,
1000 json,
1001 toml_out,
1002 } => commands::show::run(&selector, output_mode(json, toml_out)),
1003 Commands::List { filters, json } => commands::list::run(&filters, json),
1004 Commands::Status => commands::status::run(),
1005 Commands::Metrics { json } => commands::metrics::run(json),
1006 Commands::Nudge { event } => commands::nudge::run(event),
1007 Commands::Statusline => commands::statusline::run(),
1008 Commands::Review { mark, json } => commands::review::run(json, mark),
1009 Commands::Lint { check, fix, strict } => commands::lint::run(check, fix, strict),
1010 Commands::Verify {
1011 filters,
1012 rerun,
1013 check,
1014 strict,
1015 audit,
1016 apply_verdicts,
1017 rewrite_hashes,
1018 submit_verdict,
1019 id,
1020 json,
1021 pop_next,
1022 queue_status,
1023 wait,
1024 view,
1025 tags,
1026 accept,
1027 because,
1028 tracking,
1029 } => commands::verify::run(
1030 &filters,
1031 rerun,
1032 check,
1033 strict,
1034 audit,
1035 apply_verdicts,
1036 rewrite_hashes,
1037 submit_verdict,
1038 pop_next,
1039 queue_status,
1040 id,
1041 json,
1042 wait,
1043 view,
1044 &tags,
1045 accept,
1046 because,
1047 tracking,
1048 ),
1049 Commands::Critique {
1050 filters,
1051 apply_findings,
1052 include_closed,
1053 rerun,
1054 staged,
1055 all,
1056 yes,
1057 pop_next,
1058 queue_status,
1059 submit_findings,
1060 id,
1061 json,
1062 } => commands::critique::run(
1063 &filters,
1064 submit_findings,
1065 pop_next,
1066 queue_status,
1067 apply_findings,
1068 include_closed,
1069 rerun,
1070 staged,
1071 all,
1072 yes,
1073 id,
1074 json,
1075 ),
1076 Commands::Doc {
1077 summary,
1078 include_status,
1079 check,
1080 include_graph,
1081 } => commands::doc::run(summary, include_status, check, include_graph),
1082 Commands::Graph {
1083 format,
1084 out,
1085 filters,
1086 exclude_assumes,
1087 depth,
1088 include_orphans,
1089 include_status,
1090 } => commands::graph::run(
1091 &format,
1092 out,
1093 &filters,
1094 exclude_assumes,
1095 depth,
1096 include_orphans,
1097 include_status,
1098 ),
1099 Commands::Badge { out, style, metric } => {
1100 let style =
1101 commands::badge::Style::parse(&style).map_err(|message| CliError::Other {
1102 message,
1103 exit_code: 2,
1104 })?;
1105 let metric =
1106 commands::badge::Metric::parse(&metric).map_err(|message| CliError::Other {
1107 message,
1108 exit_code: 2,
1109 })?;
1110 commands::badge::run(out, style, metric)
1111 }
1112 Commands::Rename {
1113 old_id,
1114 new_id,
1115 dry_run,
1116 } => commands::rename::run(&old_id, &new_id, dry_run),
1117 Commands::Session { action } => commands::session::run(action),
1118 Commands::Auth { action } => commands::auth::run(action),
1119 Commands::Canon { action } => match action {
1120 CanonAction::Accept {
1121 annotation_id,
1122 canon_id,
1123 } => commands::canon::accept::run(&annotation_id, &canon_id),
1124 CanonAction::Reject {
1125 annotation_id,
1126 canon_id,
1127 reason,
1128 } => commands::canon::reject::run(&annotation_id, &canon_id, reason),
1129 CanonAction::List => commands::canon::list::run(),
1130 CanonAction::Show { canon_id, version } => {
1131 commands::canon::show::run(&canon_id, version)
1132 }
1133 CanonAction::Refresh => commands::canon::refresh::run(),
1134 CanonAction::Unbind { prefixed_id } => commands::canon::unbind::run(&prefixed_id),
1135 CanonAction::RequestVerify { canon_id, notes } => {
1136 commands::canon::request_verify::run(&canon_id, notes)
1137 }
1138 CanonAction::Migrate => commands::canon::migrate::run(),
1139 CanonAction::Catalogue => commands::canon::catalogue::run(),
1140 CanonAction::Probe {
1141 sut_path,
1142 flag_broken,
1143 keep_probe,
1144 gen_only,
1145 } => {
1146 commands::canon::probe::run(&sut_path, flag_broken.as_deref(), keep_probe, gen_only)
1147 }
1148 CanonAction::Suggestions {
1149 objective,
1150 counts,
1151 filter,
1152 } => commands::canon::suggestions::run(objective, counts, filter),
1153 },
1154 }
1155}
1156
1157fn output_mode(json: bool, toml_out: bool) -> commands::show::OutputMode {
1158 if json {
1159 commands::show::OutputMode::Json
1160 } else if toml_out {
1161 commands::show::OutputMode::Toml
1162 } else {
1163 commands::show::OutputMode::Text
1164 }
1165}
1166
1167#[cfg(test)]
1168mod tests {
1169 use super::*;
1170 use clap::CommandFactory;
1171
1172 #[test]
1173 fn cli_parser_construction_is_valid() {
1174 // clap performs a structural sanity check (e.g. no duplicate
1175 // subcommand names) when CommandFactory::command() runs. We assert
1176 // it succeeds rather than panicking at runtime when a user types
1177 // `--help`. Cheap canary against future enum-shape mistakes.
1178 Cli::command().debug_assert();
1179 }
1180
1181 // Note: slice 32 removed the last `not_yet(...)` stub (Rename now
1182 // has a real implementation). The `CliError::NotImplemented` variant
1183 // is kept for future stubs but is no longer reachable from dispatch.
1184}