# Mjolnir CLI v1 Contract `mjolnirctl` — user-accepted command-line contract for Mjolnir v1. ## 1. Context and constraints - `mjolnirctl` is an unprivileged client. - All communication uses the `org.bongbetic.Mjolnir` name on the D-Bus system bus. - No direct hardware or store access. - No daemon auto-start; a missing daemon is reported immediately. ## 2. Command hierarchy Canonical hierarchy: ``` mode status telemetry battery rule config calibration validate diagnostics capabilities gpu never-touch why ``` - `mjolnirctl [args]` - Sole alias: `mjolnirctl --why ` is the required equivalent of `mjolnirctl why `. - No other aliases, short options, or inferred subcommands. ## 3. Command contract ### 3.1 `mode` - `mode get` — report the active mode plus thermal-override and external-profile annotations. - `mode set ` — wait for daemon confirmation and report the resulting mode. - Invalid enum values are rejected locally before any daemon call. - Bare `mode` (no subcommand) does nothing. ### 3.2 `status` Reports: daemon version, active mode, probe completion, thermal override, external profile, module/autoload state, degradation reason. - Capability detail stays in `capabilities`. - Current sample stays in `telemetry`. ### 3.3 `telemetry` - `telemetry get` — current sample. - `telemetry history` — time-series history. - Default window: `--since 1h`. - Alternative: `--from --to `. - Output ascending by timestamp. - Empty result is success (exit 0). - No watch in v1. ### 3.4 `battery` - `battery health` — current health record. - `battery history` — time-series; same time flags as `telemetry history`. - `battery thresholds get` — current charge thresholds. - `battery thresholds set --start --end `. - Both threshold values required; applied atomically. - Local validation: `0 <= start < end <= 100`, integers. - Daemon validates against hardware. - Preset aliases deferred to the charge-preset decision. - Dependency note: ADR-003 currently exposes `ChargeStartThreshold` and `ChargeEndThreshold` as independent readwrite properties, so the open D-Bus contract must provide or define an atomic two-value update mechanism before implementation. This CLI document does not choose that wire shape; see section 5 and section 6. ### 3.5 `rule` - `rule list` - `rule add` - `rule update ` - `rule delete ` - Canonical rule fields deferred to the per-app rule decision. - No arbitrary D-Bus dictionary input. - Delete has no confirmation prompt. ### 3.6 `config` - `config export ` - `config import ` - Export refuses to overwrite an existing file unless `--force`. - Import validates the complete document before atomic apply. - Unreadable path fails. - No prompts. - `--dry-run` validates without mutation. ### 3.7 `never-touch` - `never-touch list` - `never-touch set ...` — atomically replaces the full list. - `never-touch set --clear` — clears the list. - No add/remove variants; avoid them in v1. ### 3.8 `calibration` - `calibration start` - `calibration status` - `start` remains attached through terminal daemon state and relays progress. - No cancel, stop, or background mode. ### 3.9 `validate` - Sole validation command. - Starts interactive validation, relays progress, stays attached through terminal state. - No status or cancel subcommands. ### 3.10 `diagnostics` - `diagnostics generate` - No destination path argument. - Daemon creates a redacted bundle and returns its path. - Human output prints path and size. - JSON output: `{"path": ..., "size_bytes": ...}`. - CLI does not copy, rename, or inspect the bundle. ### 3.11 `capabilities` - `capabilities get` — normalized stable fields: - knob ID - support state - reason - range - topology ### 3.12 `gpu` - `gpu get` — dGPU power and display topology. - Unsupported hardware is data, not an error: exit 0. ### 3.13 `why` / `--why` - `why ` and `--why `; write ID is an opaque daemon-issued identifier. - Output full attribution: - `write_id` - `timestamp` - `knob_id` - `trigger_event_id` - `policy_rule` - `principal` - `prior_value` - `new_value` - `mode` - `external_profile_state` - Daemon resolves `policy_rule`; non-policy writes use `"none"`. - Unknown write ID: exit 2. ## 4. Cross-cutting decisions ### 4.1 Global options Only: - `--help` - `--version` - `--output human|json` - `--timeout ` No CSV, watch, shell completion, color/verbose/quiet flags, cancellation, background workflows, daemon start, or retries. ### 4.2 Output - Human is the default regardless of TTY. - Singleton resources use key/value blocks; lists and history use aligned tables. - Explicit units on all values. - Lowercase enums. - Status annotations included. - No required color or Unicode. JSON success is one object: ```json {"schema_version": 1, "command": "...", "data": {...}} ``` - snake_case keys, lowercase enums, UTC RFC3339 timestamps, explicit units. - Long workflows use JSON Lines with the same versioned event shape. - JSON errors: one structured object on stderr; stdout carries only successful data; no partial-success output. ### 4.3 Exit codes | Code | Meaning | | ---- | -------------------------------------------- | | 0 | Success (incl. degraded/unsupported, exit 0) | | 2 | Usage / validation / not-found | | 3 | Daemon / module unavailable | | 4 | Authorization denied | | 5 | Unsupported capability | | 6 | Policy conflict / rate limit | | 7 | Internal operation failure | | 124 | Timeout | | 130 | Ctrl-C detach | Successful degraded/unsupported-capability responses remain exit 0. D-Bus errors from `org.bongbetic.Mjolnir.Error.*` map to exit codes as follows; these are CLI mappings of settled D-Bus errors: | D-Bus error | Exit | | ----------------------- | ---- | | `UnknownKnob` | 2 | | `InvalidConfig` | 2 | | `ModuleAbsent` | 3 | | `NotAuthorized` | 4 | | `CapabilityLocked` | 5 | | `UnsupportedOperation` | 5 | | `RateLimited` | 6 | | `ThermalOverride` | 6 | | `CalibrationInProgress` | 6 | ### 4.4 Timeouts - D-Bus default deadline 5 seconds, overridable by global `--timeout`. - Daemon absence reported immediately; no retries. - Long workflows (calibration/validate) default 30 minutes and have a separate timeout override. - Ctrl-C on calibration/validate detaches, returns 130, daemon safely continues. ### 4.5 Time arguments - `--since ` with `s|m|h` suffix, or - `--from --to `. - No mixing of forms. - Output always UTC RFC3339. ### 4.6 Numeric and ID rules - Percentages: integer 0..100, no suffix. - Reject locale-dependent numbers and implicit conversions. - IDs opaque; CLI never interprets them. - Human labels units according to D-Bus fields. ### 4.7 Authorization - PolKit delegated to the active agent; CLI never prompts or embeds credentials. - No agent or denied authorization gives actionable stderr and exit 4. - JSON error stable: `authorization_denied` plus action ID. This is the stable CLI code mapped from the D-Bus error `org.bongbetic.Mjolnir.Error.NotAuthorized`. ### 4.8 Battery health record `battery health` output must include the complete record fields: - `timestamp` - `cycle_count` - `design_capacity` - `full_charge_capacity` - `wear_percent` - `charge_temperature` - `validity_status` - `staleness_status` The exact types/enums and source contract for `charge_temperature`, `validity_status`, and `staleness_status` must be settled by the data-model/D-Bus work; no enum values or wire types are invented here. No derived fade prediction. ## 5. Required and Pending D-Bus Dependencies This section records CLI prerequisites for the open D-Bus contract ticket and separates them from the surface already settled in ADR-003 and the XML. `EditNeverTouchList(as)` is already settled in ADR-003 and the current XML and is therefore **not** labelled required or `[REQUIRED]` here. ### Required additions — absent from ADR-003/XML These four additions are absent from ADR-003 and the current XML. They are **required** CLI prerequisites for the open D-Bus contract ticket; they are not settled ADR-003 interfaces. | Addition | Type / shape | Purpose | | -------------------- | ------------------------------ | ---------------------------------------- | | `NeverTouchList` | read-only property `as` | read the current never-touch list | | `StartValidationRun` | method | start validation | | `ValidationStage` | progress signal | validate progress reporting | | `ExplainWrite` | method `(s write_id) -> a{sv}` | `why` full attribution; read/audit-gated | `NeverTouchList`, `StartValidationRun`, `ValidationStage`, and `ExplainWrite` are absent from ADR-003 and the current XML. `ValidationStage`'s signature is pending: ADR-006 shows both a single-stage string form and a failed-with-reason form, so this document does not invent a wire signature. ### Pending read surfaces for `status` The accepted `status` command reports module/autoload state and degradation reason. Both are required read surfaces, but their final property names and wire types are pending; descriptive labels only are used here: - current module/autoload state — wire shape pending - overall degradation reason — wire shape pending The existing `AutoloadDisabled` signal is transition-only: it fires when autoload is disabled and cannot replace a current-state read. `status` needs read surfaces for the present module/autoload state and degradation reason; no final property names are invented here. ### Pending atomic threshold mechanism `battery thresholds set --start --end ` applies both values atomically. The current `ChargeStartThreshold`/`ChargeEndThreshold` independent readwrite properties are insufficient for atomicity. The open D-Bus contract ticket must settle a two-value atomic update mechanism; its exact wire shape is pending. This is separate from the four absent additions and the two pending read surfaces above: it concerns the existing settled threshold properties rather than a new named surface. ## 6. Command-to-D-Bus mapping Where known. `[REQUIRED]` marks one of the four absent additions from section 5 — a required CLI prerequisite for the open D-Bus contract, not a settled ADR-003 interface. `[PENDING]` marks a surface whose wire shape is unresolved: the two `status` read surfaces, the `ValidationStage` signature, and the atomic threshold mechanism. `EditNeverTouchList(as)` is current settled ADR-003/XML surface and carries no tag. | Command | D-Bus surface | | -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | | `mode get` / `mode set` | mode property / setter; thermal-override, external-profile properties | | `status` | daemon version, mode, probe, override, profile properties; module/autoload state read `[PENDING]`; degradation reason read `[PENDING]` | | `telemetry get` / `history` | telemetry property; history method | | `battery health` / `history` | battery health property; history method | | `battery thresholds get` / `set` | `ChargeStartThreshold`/`ChargeEndThreshold` properties; atomic two-value update mechanism `[PENDING]` (see 3.4) | | `rule list/add/update/delete` | rule methods (fields deferred to per-app rule decision) | | `config export/import` | `ExportConfig(s path)` / `ImportConfig(s path)` with validation and atomic semantics as CLI requirements | | `calibration start/status` | calibration start/status; progress signal | | `validate` | `StartValidationRun()` `[REQUIRED]`; `ValidationStage` signal `[REQUIRED]` (signature `[PENDING]`) | | `diagnostics generate` | diagnostics method returning bundle path | | `capabilities get` | capabilities introspection | | `gpu get` | dGPU power/topology properties | | `never-touch list` | `NeverTouchList` read-only property `as` `[REQUIRED]` | | `never-touch set` | `EditNeverTouchList(as)` — current settled ADR-003/XML surface | | `why` / `--why` | `ExplainWrite(s write_id) -> a{sv}` `[REQUIRED]`, read/audit-gated | ## 7. Scope exclusions and deferred work Excluded in v1: - CSV, watch, shell completion, color/verbose/quiet flags - Cancellation, background workflows - Daemon start, retries Deferred: - Rule field syntax — waits for _Per-app rule contract — identity, precedence, and restoration_. - Charge preset aliases/wiring — waits for _Charge preset mapping — start hysteresis for 60, 80, and 100 percent_. ## 8. Rationale - Single-command, explicit-hierarchy CLI keeps v1 small and auditable. - Unprivileged D-Bus-only client keeps the surface safe; no direct hardware paths. - Degraded/unsupported hardware is data (exit 0) so scripts can distinguish capability from failure. - Atomicity (thresholds, never-touch replace, config import) avoids partial states. - Human default regardless of TTY keeps output deterministic for users and tools alike. - JSON Lines for long workflows keeps progress streamable without breaking the single-object success shape.