diff --git a/README.md b/README.md index 5a84f60f..e9d39a9f 100644 --- a/README.md +++ b/README.md @@ -461,7 +461,10 @@ hyp daemon uninstall # remove the service and detach clients (config + record `hyp daemon install --dry-run --json` prints the rendered plist or unit content and target paths without touching the filesystem, useful for -verifying what `hyp init` will install. +verifying what `hyp init` will install. Add `--platform darwin` or +`--platform linux` to render the *other* platform's unit; it is an +inspection flag and requires `--dry-run`, because an install always +targets the host it runs on. ## Troubleshooting diff --git a/llp/0009-cli-registry.spec.md b/llp/0009-cli-registry.spec.md index ab6d8f9c..e361fcaa 100644 --- a/llp/0009-cli-registry.spec.md +++ b/llp/0009-cli-registry.spec.md @@ -6,6 +6,7 @@ **Author:** Phil / Claude **Date:** 2026-06-01 **Related:** LLP 0003, LLP 0015 +**Extended-by:** [LLP 0270](./0270-help-metadata-is-part-of-the-contract.decision.md) - `#layered-help` and `#central-help-interception`: a registration's help strings are tested claims about its handler, `hyp help ` renders that command's help, and top-level help names the global options and aliases that own no command row. > Command registration, dispatch, and help. Decomposed from > `hypaware-design.md` (CLI Registry). diff --git a/llp/0270-help-metadata-is-part-of-the-contract.decision.md b/llp/0270-help-metadata-is-part-of-the-contract.decision.md new file mode 100644 index 00000000..3b0762e8 --- /dev/null +++ b/llp/0270-help-metadata-is-part-of-the-contract.decision.md @@ -0,0 +1,109 @@ +# LLP 0270: Help metadata is part of the command contract + +**Type:** Decision +**Status:** Accepted +**Systems:** CLI +**Author:** neutral / Claude +**Date:** 2026-08-18 +**Related:** LLP 0009 (#layered-help, #central-help-interception: the help system this extends), LLP 0062 (#bare-remote: the optional target name `remote login --help` has to admit), LLP 0137 (#pathway-defaults: the retention question `init --help` still described), LLP 0214 (#d1, #d2: the previous extension of the same help system) + +> Extends [LLP 0009 #layered-help](./0009-cli-registry.spec.md) and +> [#central-help-interception](./0009-cli-registry.spec.md). Dispatch renders +> `summary` / `usage` / `help` straight off the registration, which makes the +> registration the *only* description a user gets, and nothing has been +> checking that it still matches the handler. Four registrations had drifted. +> This settles what a registration owes its handler, adds `hyp help `, +> and gives the working-but-unlisted surfaces (`--version`, `-V`, `unattach`) a +> place in top-level help. + +## Context {#context} + +LLP 0009 #central-help-interception makes core the sole renderer of per-command +help: a command body never prints its own usage, so the `usage` and `help` +strings on the registration are the entire published interface of that command. +That is a good arrangement precisely because it is centralized, and a bad one +when nothing pins the strings to the code: a handler can grow a flag, drop a +question, or make a positional optional without a single test noticing that the +registration now lies. + +The audit that produced issue #835 found four such lies at once (an `init` +walkthrough that no longer asks for retention, a `remote login` whose target +name became optional, a `daemon restart` that restarts rather than stops, a +`skills install` whose `--client` defaults to `all`), plus three surfaces that +work but appear nowhere: `--version`, `-V`, and the `unattach` alias. The +common cause is that help was treated as prose attached to a command rather +than as an assertion about it. + +## Decision + +**A command's `summary`, `usage`, and +`help` are claims about its handler, and are tested as claims.** Every accepted +flag and positional appears in `usage`; an optional positional is spelled +`[name]`, not ``; a summary describes the path the handler actually takes +first, with the fallback path (if any) explained in `help` and qualified as a +fallback. Where a command's shape is settled by an LLP, the registration +carries a `@ref` to it, so the next reader of the help text can see what +constrains it. + +Tests assert the *semantic* claim, not the presence of a flag token: "the help +does not say the walkthrough asks for a retention window" is checkable and was +the defect; "the help mentions `--retention-days`" was already true while the +surrounding sentence was false. + +**`hyp daemon install --platform +darwin|linux` is public, and requires `--dry-run`.** It was accepted by the +parser, omitted from public usage, and used in-repo only to render the other +platform's unit for inspection. Rendering another platform's plist or unit file +is a genuinely useful thing to be able to ask for and there is no reason to +hide it. Performing an *install* for another platform is not: the install +writes into this host's service directory and then calls this host's service +manager, so a cross-platform install can only produce a broken one. So the flag +is documented, and refused outside a dry run, in the same shape as the +pre-existing `--json requires --dry-run` rule. + +**`hyp help ` renders that command's help.** +It is rewritten to `hyp --help` and routed through ordinary +dispatch rather than answered separately. Routing is what makes it reach +plugin-contributed commands: top-level help renders before `bootKernel` +(LLP 0009 #top-level-help-lists-plugin-commands-without-booting) and so cannot +see them, while the rewritten argv boots normally and hits the same central +`--help` interception every other command help goes through. Bare `hyp help` +keeps its existing meaning, the top-level table, and `hyp help --version` (a +flag, not a command) is left alone. + +Answering `hyp help query` with the top-level table was the worst available +behavior: it is indistinguishable from success, so the user reads the wrong +page believing they asked for it. + +**Top-level help names the global options and the +command aliases.** LLP 0009 #layered-help settles the *command* listing as one +row per top-level token; that is unchanged. `--help/-h` and `--version/-V` are +not commands and never had a row, and an alias routes without one by design, so +under a rule that only lists commands they were undiscoverable by construction. +They get two short sections after the command table instead: + +``` +Global options: + --help, -h show this list, or a command's help (`hyp help ` too) + --version, -V print the version (`hyp version` adds runtime detail) + +Aliases: + unattach detach +``` + +The alias rows are read off the registry, so a new alias lists itself. Only +core aliases appear: top-level help lists plugin commands from their manifests, +which declare no aliases, and booting to find out is exactly the cost LLP 0009 +refused to pay for help. + +## Consequences {#consequences} + +A registration change that contradicts its handler is now a test failure rather +than a documentation bug found in an audit. The cost is that `usage` strings +grow: `remote login` and `daemon install` both gained a flag list they had been +eliding. That is the intended trade; the alternative was a shorter line that +was not true. + +`--platform` outside `--dry-run` now exits 2 where it previously attempted an +install. No caller in this repo used it that way, and the ones that would have +were wrong. diff --git a/src/core/cli/core_commands.js b/src/core/cli/core_commands.js index e3109da1..80120b9a 100644 --- a/src/core/cli/core_commands.js +++ b/src/core/cli/core_commands.js @@ -232,10 +232,16 @@ function buildCoreCommands(registry) { name: 'init', summary: 'Initialize a new HypAware install (interactive walkthrough or preset)', usage: 'hyp init [preset] [flags]', + // @ref LLP 0137#pathway-defaults [constrained-by]: the walkthrough has no retention question to describe, so the help states the pathway defaults instead help: [ 'With no arguments, runs the interactive walkthrough: pick which clients', - 'and sources to capture, an export strategy, and a retention window, then', - 'write the config, install the daemon, and attach the selected clients.', + 'and sources to capture and an export strategy, then write the config,', + 'install the daemon, and attach the selected clients.', + '', + 'The walkthrough never asks how long to keep cached rows: the pathway', + 'sets it (90 days when you join a team, 120 days for a local-only', + 'install). --retention-days below is the override, and the written', + 'query.cache.retention.default_days stays editable afterwards.', '', 'Pass a preset name to skip the walkthrough. Passing any flag below also', 'skips it: the non-interactive path is chosen by the presence of a flag,', @@ -472,7 +478,8 @@ function buildCoreCommands(registry) { // kinds of client asset; no separate agents verb. name: 'skills install', summary: 'Install registered skills and subagents into AI client directories', - usage: 'hyp skills install [--client ]', + usage: 'hyp skills install [--client |all]', + help: 'Without --client the target is all, so every configured client gets the copies.', run: runSkillsInstall, }, makeGroupCommand({ @@ -483,7 +490,18 @@ function buildCoreCommands(registry) { { name: 'daemon install', summary: 'Install the persistent user service (launchd / systemd)', - usage: 'hyp daemon install [--config ] [--bin ] [--dry-run [--json]]', + usage: 'hyp daemon install [--config ] [--bin ] [--dry-run [--json] [--platform darwin|linux]]', + // @ref LLP 0270#platform-is-a-render-override [implements]: --platform is public but dry-run only, so it can never install the wrong platform's unit + help: [ + ' --config config the unit should load (default: the resolved one)', + ' --bin hyp binary to record in the unit', + ' --dry-run render the plist / unit without touching disk', + ' --json machine shape for --dry-run', + ' --platform darwin|linux', + ' render the other platform\'s unit. Inspection only:', + ' it requires --dry-run, because an install always', + ' targets the host it runs on.', + ].join('\n'), run: runDaemonInstall, }, { @@ -523,8 +541,16 @@ function buildCoreCommands(registry) { }, { name: 'daemon restart', - summary: 'Stop the daemon (and direct the operator to relaunch)', + summary: 'Restart the installed daemon service (launchd / systemd)', usage: 'hyp daemon restart', + help: [ + 'Restarts the installed launchd / systemd service in place, so a config', + 'change takes effect without an uninstall.', + '', + 'With no installed service there is nothing to bring back up: the running', + 'foreground daemon is stopped and the command prints how to relaunch it', + '(hyp daemon run --foreground) or how to install the service instead.', + ].join('\n'), run: runDaemonRestart, }, { @@ -574,13 +600,23 @@ function buildCoreCommands(registry) { { name: 'remote login', summary: 'Store the query-scoped token for a remote target (0600)', - usage: 'hyp remote login [--token-file ] [--no-forward] [--no-daemon]', + // @ref LLP 0062#bare-remote [constrained-by]: the name is optional here because a bare login resolves the default target + usage: + 'hyp remote login [name] [--org ] [--host