Report issues and read the fleet from the CLI (0.6.0) - #14
Merged
Merged
Conversation
The contract for POST /api/agent/issues, which the cloud side implements and `skillhook cloud report` will use: IssueReportRequestSchema (title, body, kind, severity, contact email, the job, delivery or skill it is about, and diagnostics), IssueDiagnosticsSchema (versions, platform, cloud mode, link state, runner readiness, the health summary with the failing checks; loose, bounded like MachineInfo) and IssueReportResponseSchema (issue id, number, URL, whether a confirmation email went out), with ISSUE_KINDS, ISSUE_SEVERITIES and LIMITS.max_issue_report_bytes (64 KiB). Additive: PROTOCOL_VERSION stays 1. Errors keep the agent API's shape. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`skillhook cloud report "<title>"` (and the MCP tool cloud_report_issue) sends the Skillhook team a report through POST /api/agent/issues with the machine token: the person's title and body (--body TEXT, --body - or --body-file), kind, severity, a contact address, the job, delivery or skill it is about, and unless --no-diagnostics what the machine already knows (versions, platform, cloud mode, the link's state, runner readiness, the health summary with the failing and warning checks), from the running server's cached answers or a quick local check. Every .env value is scrubbed from all of it before it leaves, as for the link; payloads, logs, prompts and job output never go. --dry-run prints the exact JSON. It needs a paired machine and refuses under SKILLHOOK_NO_CLOUD=1 or to an http URL; what the cloud answers (a refused token, a disabled machine, a rate limit, a cloud without the route) is said plainly. The shared operation lives in src/cloud/report.ts. `--body` is a switch for `deliveries show` and takes its text only after `cloud report`. The fake cloud takes reports; src/test-support/fake-server.ts stands in for a running server. AGENTS.md's outbound rule lists the command. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`skillhook cloud login --key shc_…|-` checks an organisation API key with GET /api/v1/me and keeps it in .env as SKILLHOOK_CLOUD_API_KEY (mode 600, never printed; the environment variable wins, for CI); `cloud logout` forgets it. `cloud machines`, `cloud jobs [--machine M] [--skill S] [--status ST] [--outcome O] [--waiting] [--limit N] [--before C]` and `cloud job <id>` read /api/v1 with it and print tables, or the API's JSON with --json, parsed with loose schemas since the cloud may say more. The machine token is never used for them, so pairing gives a machine no view of the rest of its organisation; keys that do not start with shc_ are refused. Requests go only to the machine's cloud URL over HTTPS, only when run, and not under SKILLHOOK_NO_CLOUD=1. The HTTP client reads the public API's RFC 9457 problems (code, detail, request_id) as well as the agent API's error and message; a refused key says to log in again and a missing scope names it. The fake cloud serves the /api/v1 reads for one key. Docs list exactly what each command reads. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cloud_report_issue takes dry_run, like `cloud report --dry-run`: it returns exactly what would be sent (scrubbed, with the diagnostics) and sends nothing, so an agent can show the person the report before it goes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The machine list is read only for the tables, and the filters are the only thing that goes with a read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The report contract gains `report_id`, a client-generated idempotency key (8 to 100 of A-Z a-z 0-9 _ -): a retry with the same id returns the original report instead of filing a second one, and the answer is the same either way. `cloud report` generates one per report and sends it with every attempt; network errors, timeouts and 5xx are retried (three attempts), a 429 only after a retry_after_ms of at most 15 seconds, another 4xx never. The fake cloud files a report once per id and can drop a connection or lose an answer after filing, which the tests use. From review: - A token or key that is not one line of printable characters is refused before it becomes a header (the runtime quotes a refused header in its error), and a token never appears in a transport error message. - API keys must look like one (shc_ and then letters, digits, - and _), and never go to the built-in placeholder cloud URL: until pairing, `cloud.url` or SKILLHOOK_CLOUD_URL names a cloud, the reads refuse. - `skillhook cloud <subcommand> --help` prints the usage and does nothing else; `--diagnostics false` (0, no, off) turns diagnostics off. - The references and the contact address are scrubbed like the rest (a contact address that is a value from .env is refused); diagnostics from a server answer of another shape fall back to the basics instead of failing the report; the client reads the cloud's answer leniently, so a field a newer cloud adds does not turn a filed report into a failure; a 404 is "the cloud does not take reports yet" only when it is not the cloud's own answer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`skillhook cloud report` and the MCP tool cloud_report_issue: problem reports to the Skillhook team from a paired machine, with scrubbed diagnostics and an idempotent report_id; the issue-report schemas in @meterapp/skillhook/protocol; and `cloud login|logout|machines|jobs|job`, the organisation's fleet read with an organisation API key. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
JOsacky
added a commit
that referenced
this pull request
Sep 29, 2026
A safety fix: most subcommands ignored `--help` / `-h` and did their work anyway. ```bash skillhook jobs prune --help # pruned jobs skillhook service install --help # installed (and started) the launchd/systemd service skillhook config set port 1 --help # wrote skillhook.json skillhook link --help # linked the current directory skillhook serve --help # started the server ``` 0.6.0 (#14) fixed this for `skillhook cloud <subcommand> --help` only, inside `cloud.ts`. ## The fix: one check, before any command runs - `COMMANDS` in `src/commands/main.ts` now pairs every command with its usage (`{ run, usage }`), and `main()` checks `--help` / `-h` right after resolving the command, before any command code runs. It prints the usage and exits 0. A new command cannot be registered without a usage, and cannot forget the check. - `skillhook help <command> [subcommand]` does the same thing (`skillhook help` alone still prints the overview). - `--json` prints `{ "ok": true, "command": "jobs", "usage": "…" }`. - The per-command checks in `cloud.ts` and `update.ts` are gone, because the central one covers them. - `serve`, `doctor`, `health`, `runners`, `mcp` and `url` had no usage text and now have one. `init`'s usage now lists `--runner shell`, which the command already accepted. - `skillhook job <subcommand> --help` follows `jobCommand`'s dispatch. The agent subcommands (`progress|ask|outcome|note|context`, or no subcommand) print the job API usage. Everything else (`job list`, `job prune`, …) is really `jobs …`, so it prints the `jobs` usage. - Each command's private `USAGE` became an exported `<NAME>_USAGE`, as `INIT_USAGE` and `UPDATE_USAGE` already were. Most of the diff in `src/commands/*.ts` is this mechanical rename. ## The test (`src/cli.test.ts`) It runs every entry of `COMMANDS` and every subcommand in three forms: `… --help`, `… -h --json` and `help …`. That is about 400 invocations, against a `tempHome()` set up so that each line has something to act on: a job to prune or answer, a delivery to replay, secrets and config to change, a linked project, a scheduled skill and a stored API key. Every invocation must: - exit 0 and print exactly the command's usage (or the JSON above), with nothing on stderr; - leave the home unchanged (a snapshot of every entry with its mtime and content, compared after each invocation); - never read stdin. The table must also cover every subcommand the usage texts document (`skillhook jobs prune …`, `skillhook projects add|remove …`), and every `COMMANDS` key (aliases reuse their command's lines). As a control, the same `jobs prune` line without `--help` does prune, and the snapshot shows it. It is hermetic even if the fix regresses. The env sets `SKILLHOOK_NO_UPDATE_CHECK=1`, unreachable registry and cloud URLs, `SKILLHOOK_NO_CLOUD=1` and fake claude/codex. `installService`, `uninstallService`, `restartService`, `enableExposure` and `disableExposure` are stubbed with `vi.mock` for this file, so a broken check can never touch the real launchd/systemd service or Tailscale Funnel. The test asserts none of them was called. I checked that the test catches the bug. With the check skipped for `jobs`, it fails on `expected 'Removed 1 old job(s)' to contain 'skillhook jobs prune [--keep N]'`. With a stray file write added to the help path, the snapshot names the line and shows the new file. With a subcommand missing from the table, it fails on `skillhook jobs prune --help: expected [...] to include 'prune'`. The whole test takes about 0.3 s. `npm run check` passes: typecheck, 280 tests, build, schema and release metadata. CHANGELOG has an `## Unreleased` entry. README (global options) and AGENTS.md (registering a command) mention the new rule. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.
Two things a person on a machine could not do from the terminal: tell the Skillhook team about a problem, and see the rest of the fleet. Released as 0.6.0.
skillhook cloud report: problem reports from a paired machinePOST {cloud.url}/api/agent/issueswith the machine token, only when a person runs it. It printsReported as #N: <url>and whether a confirmation email went out;--jsonprints the cloud's answer.--body TEXT,--body -,--body-file),--kind,--severity,--email, and references (--job,--delivery,--skill). Unless--no-diagnostics, it adds what the machine already knows: skillhook, Node, OS and architecture,cloud.mode, the link's state,{runner, ready}per runner, and the health summary with the failing and warning checks. These come from the running server's cached answers, or from a quick local check (no network probes) when no server runs. A server answer of another shape falls back to the basics rather than failing the report..envvalue is scrubbed from the title, body, references and diagnostics before anything leaves, as for the link. A contact address that is itself a.envvalue is refused. Payloads, logs, prompts and job output never go.docs/cloud.mdlists exactly what is sent.report_id(a UUID), the same on every attempt. Network errors, timeouts and 5xx are retried (three attempts); a 429 is retried only after aretry_after_msof at most 15 s; any other 4xx is never retried. The cloud files a retried report once. The client reads the answer leniently, so a field a newer cloud adds cannot turn a filed report into a failure and a second report.cloud.enabledandSKILLHOOK_CLOUD_TOKEN); otherwise it points toskillhook cloud connector the dashboard / hosted MCP. Refused underSKILLHOOK_NO_CLOUD=1and to anhttpURL. A refused token, a disabled machine, a rate limit and a cloud without the route (an HTML 404) each get a plain message.src/cloud/report.ts). The MCP toolcloud_report_issuesends the same report, withdry_runto show the person first.--bodystays a switch fordeliveries show <id> --bodyand takes its text only aftercloud report.skillhook cloud <subcommand> --helpnow prints the usage instead of running the subcommand (a report must not go out on--help).Reading the fleet with an organisation API key
login --key shc_…|-storesSKILLHOOK_CLOUD_API_KEYin.env(mode 600). The environment variable works too (CI) and wins.logoutremoves the key from.env.machines(name, status, mode, version, last seen),jobs [--machine M] [--skill S] [--status ST] [--outcome O] [--waiting] [--limit N] [--before C]andjob <id>(status, outcome, the pending question, the result excerpt) read/api/v1. Tables by default;--jsonprints the API's JSON. Answers are parsed with loose schemas, since the cloud may add fields.shc_then letters, digits,-,_), so a pasted second line or the machine token is refused before anything is sent.cloud.urlorSKILLHOOK_CLOUD_URLnames a cloud, the reads refuse. Requests are HTTPS only and are refused underSKILLHOOK_NO_CLOUD=1.src/cloud/http.tsnow reads RFC 9457 problems (code,detail,request_id) as well as the agent API'serror/message. A 401 says to runskillhook cloud login; a 403 names the missingfleet:readscope. A token that is not one line of printable characters is refused before it becomes a header (the runtime quotes a refused header in its error), and a token never appears in a transport error message.The protocol contract (additive,
PROTOCOL_VERSIONstays 1)POST /api/agent/issues,Authorization: Bearer <SKILLHOOK_CLOUD_TOKEN>, JSON body ≤ 64 KiB. A 200 answer is anIssueReportResponse, the same for a new report and for a replay of the samereport_id;acknowledgedsays whether a confirmation email went out. Errors use the agent API's shape{ok: false, error, message, retry_after_ms?}:401 invalid_token,403 machine_disabled,400 invalid_request,413 payload_too_large,429 rate_limited,500 server_error. Documented indocs/cloud-protocol.md#issue-reports.Rules and docs
AGENTS.md's "Outbound requests are opt-in and enumerated" rule now lists the person-invoked cloud commands: one request each, only when run, only to the machine's cloud URL over HTTPS.cloud reportand the API-key commands refuse underSKILLHOOK_NO_CLOUD=1.docs/cloud.md(whatcloud reportsends, what the API-key commands read),docs/cloud-protocol.md,docs/mcp.md,docs/security.md,docs/operations.md, the README command table,llms.txt, the setup skill andCHANGELOG.md.Map, and nothing writes user-supplied keys.Landing this
Merging bumps
package.jsonto 0.6.0, so the Release workflow tagsv0.6.0and Publish ships@meterapp/skillhook@0.6.0. The cloud (MeterApp/skillhook-cloud, which vendors 0.5.0 today) needs 0.6.0 forIssueReportRequestSchema. Until its/api/agent/issuesroute is deployed,cloud reportsays the cloud does not take reports yet.Tests
npm run check: 43 files, 279 tests, build, schemas and release metadata (0.6.0).report_id);src/test-support/fake-server.ts) and from a local check with the fake CLIs;.envvalues in the title, body, references and diagnostics;--json, the machine token never reaching/api/v1, and--help;dry_run./api/agent/issues(once perreport_id, with scripted failures) and the/api/v1reads for one key.--help,--diagnostics false, odd server answers, unscrubbed references, strict answer parsing); all are fixed here, with tests.🤖 Generated with Claude Code