Repository navigation
feat(cli)!: preview writes until --write, truncate raw output, and close the AXI catalog gaps - #37
Merged
Merged
Conversation
…ose the AXI catalog gaps The AXI catalog admitted this tool with an exception that named five gaps. This closes all five. Section 6, writes. `service call`, a write-method `api` request and a write `ws` command now send nothing without `--write`. Without it each prints what it would send. The `service call` preview reads the published service, resolves the target, runs the capability pre-check and lists the entities it would reach; it fails as the call would for a service that does not exist, a response mode the service does not have, or a target that reaches nothing, and reports an unpublished field as a warning. A preview is a read, so a read-only session can still see one and is told the write would be refused. `ws --list` gains an `access` column. Section 3, truncation. `api` and `ws` shorten a long response: each list to its first 25 items and each string to 1200 characters, with the number of items withheld and the full size reported. `--full` prints all of it, and the hint appears only when something was cut. Section 2, default fields. `sensor list` defaults to entity_id, name, value and unit, and `logbook get` to when, name, event and cause. The dropped columns stay reachable through `--fields`, and the help block says so. Section 7, session hooks. `setup hooks status` reports each target as installed, stale or missing and writes nothing. `setup hooks remove` takes out the entries this tool wrote, by the same ownership test an install uses, and leaves the shared Codex feature flag on. A session-end hook runs the new `context end`, which counts the commands a session ran from the transcript the agent names; names and counts only, never an argument. `context` reports the last session in the same directory. Claude Code and Codex get a SessionEnd hook; OpenCode has no such event, so its plugin records when a session goes idle. Section 8, the home view. A bare run exits 0 when nothing is configured or the installation does not answer. It prints `live_state: not available`, the `code` and `class` of the fault, the command names and the setup help. `ping` and `doctor` remain the commands whose exit code reports reachability. The README and AGENTS.md text describing the changed behaviour is corrected, and the skill is regenerated. BREAKING CHANGE: `service call`, write-method `api` requests and write `ws` commands no longer act unless `--write` is passed; without it they preview and exit 0. The no-argument home view now exits 0 when unconfigured or unreachable, reporting the fault under `live_state`, `code` and `class` instead of `error`. `sensor list` and `logbook get` print four default columns; pass `--fields` for the rest.
… reports all six rows
…status, remove, capture
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intent
Close all of the gaps the AXI catalog maintainer recorded when admitting hass-axi with an exception (kunchenguid/axi#231, pin v0.8.0), so the tool can be listed without the exception. The catalog entry's own words for the gaps:
sensor listdefaults to six fields andlogbook getto five. (Other list commands default to three fields,statistics listto four, with--fieldsselecting from the wider set.)apiandwsoutput is not size-truncated. (src/hass_axi/commands/api.pyandwscmd.pyprint the whole response.)service calland the rawapi/wswrite paths mutate without a confirmation or preview step, relying on the capability pre-check and the opt-inHASS_AXI_READ_ONLYswitch.setup hookshas no status or removal subcommand and installs no session-end hook.)HA_URL/HA_TOKENare unset or Home Assistant is unreachable.Make sure to test all command locally again my HA.
What Changed
--write(breaking).service call, a write-methodapirequest and a writewscommand send nothing without--write; without it each prints what it would send and exits 0. Theservice callpreview reads the published service, resolves the target and runs the capability pre-check, mirrors the sent call's verdicts (unknown service, wrong response mode, nothing targeted), and degrades the same way when the registries are unreadable. A preview is a read, so a read-only session can still see one and is told the write would be refused;ws --listgains anaccesscolumn.apiandwsshorten long responses — first 25 items per list, 1200 characters per string — reporting what was withheld, with--fullprinting everything.sensor listandlogbook getdefault to the AXI-standard four columns with the rest behind--fields. The no-argument home view now exits 0 when unconfigured or unreachable, reporting the fault underlive_state,codeandclassinstead of erroring;pinganddoctorremain the reachability commands.setup hooks statusreports each target installed/stale/missing without writing, andsetup hooks removetakes out the entries this tool wrote by the same ownership test an install uses (including the pre-rename shapes), leaving shared flags and unmanaged entries alone. A SessionEnd hook runs the newcontext end, which records command names and counts — never arguments — via the newsessionlog.pymodule, andcontextreports the last session in the same directory.New suites:
tests/test_write_preview.pyandtests/test_session_capture.py, with the doubles and existing sweeps extended to cover the preview gate, truncation and hook lifecycle.Risk Assessment
Testing
Full regression suite passed (ci-local: leakcheck, commits, test, skill). Then drove 20+ named scenarios against running servers: the loopback double lab, a WS-refusing lab, a throwaway real Home Assistant 2026.9.4 container, and the maintainer's live HA 2026.7.2 for every command with reads and previews only (no mutations on his installation; all --write mutations ran on the throwaway container and double lab). Preview-before-write, both review fixes, truncation, default fields, hook status/remove/session-end, and the exit-0 home view all verified with transcripts in the evidence directory. No LLM API spend; teardown removed the container, its root-owned config, and the fetched real token; worktree left clean.
Evidence: Evidence manifest: scenario-to-file map, run summary
Evidence: Real Home Assistant container: preview leaves light off, --write turns it on (2 states changed)
Evidence: Review fix 1: unavailable-only target previews at exit 0 matching --write
Evidence: Review fix 2: WebSocket upgrade refused, preview and --write both exit 0 with degraded target report
Evidence: Live HA: api/ws truncation (first 25 of 909 and 2237 items), 1200-char string cut, --full sizes, write previews
Evidence: Live HA reads: home/doctor counts, sensor list and logbook get default columns
Evidence: Real HA adversarial: UNSUPPORTED_CAPABILITY and NO_ENTITIES_TARGETED, preview verdict equals sent verdict; truncation; typed registry rename; doctor
Evidence: Hooks lifecycle: status six targets, install, remove all six rows, codex-features kept, idempotent
Evidence: Written hook files: SessionStart and SessionEnd entries with managed_by marker (Claude Code and Codex)
Evidence: Session-end capture: context end counts commands from transcript, context reports last session, names and counts only
Evidence: Home view exit 0: unconfigured (NOT_CONFIGURED/config), unreachable (UNREACHABLE/transport), live
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
src/hass_axi/commands/service.py:506-service callpreview raises NO_ENTITIES_TARGETED (exit 1) for a target that matches only unavailable entities, while the identical command with --write exits 0 ('accepted with 0 states changed' with the unavailable entity named by _report_target). Concrete sequence:hass-axi service call switch.toggle --target-entity switch.example_outlet(no --write) hits the condition at line 506 (not reached), _unreached_target sees matched non-empty and raises; add --write and the sent path's _report_target findsreachable(domain-filtered, not availability-filtered) non-empty and reports at exit 0. This contradicts the change's own documented contract ('every refusal below is the same answer with or without the flag', README and COMMAND notes). The sent path raises only when matched is empty, or under --response (bodyless 500 explained via _unreached_target); the preview should mirror those two worlds. Note lines 510-518 already build the matched-but-skipped phrasing and are unreachable for this case because the raise at 509 fires first.src/hass_axi/hooks.py:714-hooks.removeskips appending the second target row for each settings file (else: continueat line 714), sosetup hooks removeoutput reports claude-code/codex but never claude-code-session-end/codex-session-end, whileinstallandstatusreport all six JSON targets. The comment at lines 678-679 states the opposite design ('both rows report what that one rewrite did'). tests/test_hooks.py::test_remove_takes_out_what_install_wrote_and_is_idempotent pins the four-row shape via exact dict equality, so the fix must extend that test's expected statuses with the two session-end rows.🔧 Fix applied.
1 warning still open:
src/hass_axi/commands/service.py:503- For an area- or device-targetedservice call, the preview's registry resolution (live.resolved(parsed), memoized by_Live) raises the WebSocket transport fault and exits 1, while the identical command with--writesucceeds and exits 0: the sent path's_precheck(service.py:599-604) deliberately swallows the resolution failure, sends the POST over the working REST transport, and on an empty change set degrades to_report_target's "target could not be resolved" answer at exit 0 (service.py:757-763). Concrete sequence: a reverse proxy that answers REST but refuses the WebSocket upgrade (the topology AGENTS.md itself names) —hass-axi service call light.turn_off --target-area example_roomexits 1class: transport(taxonomy's "retry" can never succeed), and adding--writemutates and exits 0. This breaks the change's documented contract ("every refusal below is the same answer with or without the flag", README and COMMAND notes) in the one deployment where it is reachable, and since the preview is now the mandatory gateway to writes (Principle 6), it blocks area/device-targeted calls that would work. Remedy mirrors_report_target: catch the AxiError at the preview's resolution and answertarget: <scope> could not be resolved: <message>with the checks marked not run, exit 0 — the same verdict the sent call gives.🔧 Fix applied.
1 info still open:
src/hass_axi/commands/context.py:64-context endwrites the session-record file but declaresaccess=READand honorsHASS_AXI_READ_ONLYinside the command body (comment at context.py:61-63, test-pinned bytest_a_read_only_session_records_nothing_and_its_hook_still_exits_zero). That is deliberate and correct — a dispatch-level WRITE refusal would be reported as every read-only session failing to close — but AGENTS.md's read-only gate section still states "Enforcement is at dispatch, never in a command body" as an absolute and was not updated to record this exception, so the committed project memory contradicts the shipped code and a future session could 'fix' the body-level check back into a declaration. Noting the tradeoff; the one-paragraph AGENTS.md amendment can ride along with any later change.✅ **Test** - passed
✅ No issues found.
scripts/ci-local.sh --only leakcheck --only commits --only test --only skillscripts/ci-local.sh --only leakcheck --only commits --only test --only skill (full ~1300-test suite, lint, leak scan, generated-skill check) — PASShass-axi with empty env / unreachable HA_URL / live HA_URL (home view exit codes and live_state, code, class)hass-axi sensor list default columns vs --fields (lab double + live HA)hass-axi logbook get default columns vs --fields (lab double + live HA)hass-axi api /states and ws --raw config/entity_registry/list truncation + --full (live HA: first 25 of 909 and of 2237 items; 1200-char string cut via POST /template)hass-axi service call preview vs --write: state-unchanged proof, changed list, unavailable-only target (fix 1), WS-down degraded target (fix 2), UNSUPPORTED_CAPABILITY, NO_ENTITIES_TARGETED — all on running servershass-axi api POST preview until --write; hass-axi ws --raw config/area_registry/create preview until --write (real HA container)HASS_AXI_READ_ONLY preview note and exit-2 refusal; ws --list access columnsetup hooks install/status/remove under an isolated --home, plus status against the real home; inspected the written settings.json/hooks.json for SessionStart and SessionEnd entrieshass-axi context end with a synthetic transcript payload, then hass-axi context last_session linelive HA sweep across all 17 commands (state/sensor/history/logbook/statistics/service/template/entity/area/device/ws/api/ping/doctor/setup/context)$ hass-axi …example blocks (adding --write, changing help lines, the context document) and the README's own rule is that every block is real output re-run against a throwaway Home Assistant. This phase verified the prose against the code but could not re-run those blocks against any installation; live verification against the user's HA is the outer run's remaining acceptance step (per the intent's "test all commands locally against my HA").✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.