From 18fd467488b5b1c0aa611a35adfcb15b3ed0dd09 Mon Sep 17 00:00:00 2001 From: Max Levitskiy Date: Sun, 18 Oct 2026 16:20:34 +0200 Subject: [PATCH 1/2] docs(design): C18 records the #268 decisions describe probes but writes nothing, wait --timeout takes a duration with a unit (30s by default), continue waits for a step another worker runs, and the CLI reference is generated into its own file. Claude-Session: https://claude.ai/code/session_01PvecNurNanbbbk5qLA5qpM --- docs/design/c18-cli-grammar.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/design/c18-cli-grammar.md b/docs/design/c18-cli-grammar.md index 2073043..53d11c4 100644 --- a/docs/design/c18-cli-grammar.md +++ b/docs/design/c18-cli-grammar.md @@ -50,7 +50,7 @@ The table shows each verb's targets and the flags this design renames or adds. E | `get [name…] [--unfinished]` | every kind | `list`, `list --unfinished`, `events`, `explain` (no code) | | `describe ` | every kind | `status`, `step show`, `explain `, `inspect ` | | `explain [.field…]` | every kind | — (new: field docs from the kind's schema) | -| `wait --for= [--timeout]` | run, step | — (new; replaces polling `status`) | +| `wait --for= [--timeout ]` | run, step | — (new; replaces polling `status`) | | `start workflow/ \| ` | workflow, artifact | `start` | | `execute workflow/ \| ` | workflow, artifact | `run` (L5) | | `continue [run/] [--wait]` | run | `continue`, `worker --run --until-parked` | @@ -68,7 +68,7 @@ The table shows each verb's targets and the flags this design renames or adds. E | `init` | — | `init` | | `version` | — | `capabilities` | -`get` prints a table, one row per item, with columns from the kind's entry (L3); `-o wide` adds the low-priority columns. `describe` prints everything a person needs about one item: for a run, its status, position, the arrow into it, what it needs, the outstanding invocation, the commands that answer it, and whether a worker is alive. It is the one call that answers "what is this run doing and what do I do?" (`describe run/X -o json` gives the same as an object, which kubectl lacks.) `get events --run X` replaces `events`, as `get` with a parent (L6). `get runs --unfinished` keeps today's `list --unfinished`: only unfinished runs, newest first, filtered in the store rather than by the caller. `get runs` keeps `list`'s tree in text output: a child run is indented under its parent, in sibling order, with the parent node named. `start` and `execute` take the same targets `start` and `run` take today: `workflow/` for a workflow in the project folder, or a path to a workflow folder, its `workflow.dot` or a built artifact. As today, an existing path wins: a target that names an existing file or folder is that path, and only otherwise is it a workflow name in the project folder. `workflow/` always means the name. When two folders share a workflow name, which a lookup by name refuses with `workflow.ambiguous`, `get workflows -o name` prints each of them by its path, which every workflow verb takes. +`get` prints a table, one row per item, with columns from the kind's entry (L3); `-o wide` adds the low-priority columns. `describe` prints everything a person needs about one item: for a run, its status, position, the arrow into it, what it needs, the outstanding invocation, the commands that answer it, and whether a worker is alive. It is the one call that answers "what is this run doing and what do I do?" (`describe run/X -o json` gives the same as an object, which kubectl lacks.) `describe` changes nothing and shows nothing stale (#268). At a held async step whose action has a probe, it runs the probe. On a `dead` answer it records nothing: it shows the attempt as cut off with reason `probe-dead` and what happens next, the same way `status` already shows a worker's try that was cut off. The expiry is written by the verbs that write, as today: `serve`'s sweep, `claim` and `continue`. `status --no-probe` does not carry over, since `describe` writes nothing; the alias accepts it and ignores it. `wait` reads the same live view. `continue run/X` works the run until it parks, as `worker --run --until-parked` does: it runs watches, and exits 6 at a held step with no watch, or one whose watch says the holder waits for a person. At a step another worker runs, it waits for that worker's result instead of returning exit 0, so it replaces `worker --run --until-parked` exactly (#268). `get events --run X` replaces `events`, as `get` with a parent (L6). `get runs --unfinished` keeps today's `list --unfinished`: only unfinished runs, newest first, filtered in the store rather than by the caller. `get runs` keeps `list`'s tree in text output: a child run is indented under its parent, in sibling order, with the parent node named. `start` and `execute` take the same targets `start` and `run` take today: `workflow/` for a workflow in the project folder, or a path to a workflow folder, its `workflow.dot` or a built artifact. As today, an existing path wins: a target that names an existing file or folder is that path, and only otherwise is it a workflow name in the project folder. `workflow/` always means the name. When two folders share a workflow name, which a lookup by name refuses with `workflow.ambiguous`, `get workflows -o name` prints each of them by its path, which every workflow verb takes. Why settled: these are kubectl's read verbs (`get`, `describe`, `explain`, `wait`) and Temporal's run verbs (`start`, `execute`, `signal`, `cancel`), with spore's own `resolve`, `continue`, `claim` and the step-worker verbs kept as they are. `serve` is the usual name for a long-running process that does work for others; `worker` stays the kind it serves as (`get workers`). `version` follows `kubectl version`. `version -o json` keeps the whole handshake `capabilities` gives today, `actionKinds`, `asyncKinds` and `resultChannels` included, so a caller such as `ml-workflow` checks the host in one call before it starts anything. Action kinds (`command`, `external`, `native`) are host capabilities, not kinds a verb targets, so they stay in that handshake and are never rows of `get kinds`. @@ -140,10 +140,11 @@ Why settled: kubectl's lesson is that CRDs (new kinds as data) carry most of the **Decision.** - **Flags.** `--data` is every JSON payload (`signal`, `resolve`, `complete`), `--outcome` every outcome, `--key` every repeat-safety key (`start`, `signal`, `resolve`, `cancel`), `--fail`/`--error` both become `--error`, `--run` every run scope. Worker tuning flags (`--lease-ms`, `--heartbeat-ms`, `--poll-ms`, `--grace-ms`, `--stop-grace-ms`, `--max-watches`) belong to `serve`, `execute` and `continue` only; `--db` stays global; `--native-kernel` becomes the `RUNSPORE_NATIVE_KERNEL` variable. Every flag and argument has help text and every verb has examples. - **Output.** `-o text|wide|json|jsonl|name`; `--json` stays as `-o json`. `--fields a,b` selects fields in JSON. `-o jsonl` streams one object per line and ends with `{"type":"done"}`; it is the form of `wait` and `continue --wait`. Results go to stdout, progress and warnings to stderr. +- **Durations.** `wait --timeout` takes a number with a unit (`500ms`, `30s`, `5m`, `1h`, or a compound such as `1m30s`), as kubectl does. A bare number is refused with exit 2 and an error that shows the fix: `--timeout 30 has no unit; give one, e.g. 30s or 5m (units: ms, s, m, h)`. Leaving out `--timeout` waits at most 30s, and `0s` checks once and exits. No value waits forever (#268). - **Errors.** Under `-o json` every error is JSON, including usage errors, and names the argument. Each error has a stable code, the current state, `retryable`, and the exact next commands. Writes print `kind/name verbed` and the next command. - **Exit codes.** The 0.2 table stays, plus `7` cancelled (c08), `8` timeout (`wait`), and `9` not found from 0.5. Until then anything not found, a run, a workflow or a code, stays exit 2 as in 0.2, since `start`, `validate` and other 0.2 command names are unchanged and L9 keeps them working. - **Dry run.** `--dry-run` on `signal`, `resolve`, `cancel` and `release` prints what the command would change and the resulting status, writing nothing: the event `signal`, `resolve` and `cancel` would record, and for `release`, which records no event, the quarantine it would clear. -- **One source.** `--help`, `help -o json`, shell completion and the CLI reference in `spec/cli.md` come from the registry and the clap tree. +- **One source.** `--help`, `help -o json`, shell completion and the CLI reference come from the registry and the clap tree. The reference is its own generated file, `spec/cli-reference.md`, with each verb's synopsis, flags, kinds and examples. CI fails when the committed file is out of date. `spec/cli.md` stays hand-written, keeps the prose contracts and links to the reference (#268). Why settled: gh (`--json fields`), kubectl (`-o`, `wait --for`), terraform and cargo (JSON streams with a final event), Restate's 2026 CLI revamp (typed exit codes, next steps, dry run), Cloudflare's `cf` (generated from one schema) and Arcjet's guidance all converge on these. They fix the 0.2 gaps listed in §Why. @@ -161,7 +162,7 @@ Tickets in build order, each one release of the CLI that works end to end: 2. **The rest of the core kinds.** `step`, `signal`, `event`, `invocation`, `workflow`, `artifact`, `worker`, `quarantine`, `code`; `explain`; `get all`; `-o name` round trips. About 1 day. 3. **Action verbs and aliases.** `start`, `execute`, `continue`, `signal`, `resolve`, `release`, step-worker verbs, `serve`, `validate`, `build`, `cat`, `version`; the unified flags; every 0.2 command as an alias. About 1 day. 4. **`wait`, `cancel`, `-o jsonl`, dry run, exit codes 7–9, JSON usage errors.** About 1 day; `cancel` is #74. -5. **Generated reference and completion.** `help -o json`, completion scripts, `spec/cli.md` generated. Half a day. +5. **Generated reference and completion.** `help -o json`, completion scripts, `spec/cli-reference.md` generated, with a CI check that it is current. Half a day. The `ml-workflow` plugin and `work/build-0-2/` move to the new forms with ticket 3. Plugin loading is M6 and not part of this list. From a1e5c2dc1a7413db10ce97851c14ae50b13c40ce Mon Sep 17 00:00:00 2001 From: Max Levitskiy Date: Sun, 18 Oct 2026 16:24:52 +0200 Subject: [PATCH 2/2] docs(design): C18 shows a dead probe as an observation on a held attempt Claude-Session: https://claude.ai/code/session_01PvecNurNanbbbk5qLA5qpM --- docs/design/c18-cli-grammar.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/design/c18-cli-grammar.md b/docs/design/c18-cli-grammar.md index 53d11c4..6b76c25 100644 --- a/docs/design/c18-cli-grammar.md +++ b/docs/design/c18-cli-grammar.md @@ -68,7 +68,7 @@ The table shows each verb's targets and the flags this design renames or adds. E | `init` | — | `init` | | `version` | — | `capabilities` | -`get` prints a table, one row per item, with columns from the kind's entry (L3); `-o wide` adds the low-priority columns. `describe` prints everything a person needs about one item: for a run, its status, position, the arrow into it, what it needs, the outstanding invocation, the commands that answer it, and whether a worker is alive. It is the one call that answers "what is this run doing and what do I do?" (`describe run/X -o json` gives the same as an object, which kubectl lacks.) `describe` changes nothing and shows nothing stale (#268). At a held async step whose action has a probe, it runs the probe. On a `dead` answer it records nothing: it shows the attempt as cut off with reason `probe-dead` and what happens next, the same way `status` already shows a worker's try that was cut off. The expiry is written by the verbs that write, as today: `serve`'s sweep, `claim` and `continue`. `status --no-probe` does not carry over, since `describe` writes nothing; the alias accepts it and ignores it. `wait` reads the same live view. `continue run/X` works the run until it parks, as `worker --run --until-parked` does: it runs watches, and exits 6 at a held step with no watch, or one whose watch says the holder waits for a person. At a step another worker runs, it waits for that worker's result instead of returning exit 0, so it replaces `worker --run --until-parked` exactly (#268). `get events --run X` replaces `events`, as `get` with a parent (L6). `get runs --unfinished` keeps today's `list --unfinished`: only unfinished runs, newest first, filtered in the store rather than by the caller. `get runs` keeps `list`'s tree in text output: a child run is indented under its parent, in sibling order, with the parent node named. `start` and `execute` take the same targets `start` and `run` take today: `workflow/` for a workflow in the project folder, or a path to a workflow folder, its `workflow.dot` or a built artifact. As today, an existing path wins: a target that names an existing file or folder is that path, and only otherwise is it a workflow name in the project folder. `workflow/` always means the name. When two folders share a workflow name, which a lookup by name refuses with `workflow.ambiguous`, `get workflows -o name` prints each of them by its path, which every workflow verb takes. +`get` prints a table, one row per item, with columns from the kind's entry (L3); `-o wide` adds the low-priority columns. `describe` prints everything a person needs about one item: for a run, its status, position, the arrow into it, what it needs, the outstanding invocation, the commands that answer it, and whether a worker is alive. It is the one call that answers "what is this run doing and what do I do?" (`describe run/X -o json` gives the same as an object, which kubectl lacks.) `describe` changes nothing and shows nothing stale (#268). At a held async step whose action has a probe, it runs the probe. On a `dead` answer it records nothing. The attempt is still held: its token still reports, and no effect rule has moved the run. So `describe` shows the attempt as held, next to the probe's `dead` answer and when it was checked, and says what the expiry will do once a verb that writes records it. Those verbs are `serve`'s sweep, `claim` and `continue`, as today. Its commands are only those that apply now, such as `continue run/X`, which expires the attempt and goes on, and never one that applies only after the expiry, such as `resolve` on an `unsafe` step. Through 0.4 the `status` alias keeps `--no-probe` as in 0.2, a read of the store that runs no probe (L9). `wait` reads the same live view. `continue run/X` works the run until it parks, as `worker --run --until-parked` does: it runs watches, and exits 6 at a held step with no watch, or one whose watch says the holder waits for a person. At a step another worker runs, it waits for that worker's result instead of returning exit 0, so it replaces `worker --run --until-parked` exactly (#268). `get events --run X` replaces `events`, as `get` with a parent (L6). `get runs --unfinished` keeps today's `list --unfinished`: only unfinished runs, newest first, filtered in the store rather than by the caller. `get runs` keeps `list`'s tree in text output: a child run is indented under its parent, in sibling order, with the parent node named. `start` and `execute` take the same targets `start` and `run` take today: `workflow/` for a workflow in the project folder, or a path to a workflow folder, its `workflow.dot` or a built artifact. As today, an existing path wins: a target that names an existing file or folder is that path, and only otherwise is it a workflow name in the project folder. `workflow/` always means the name. When two folders share a workflow name, which a lookup by name refuses with `workflow.ambiguous`, `get workflows -o name` prints each of them by its path, which every workflow verb takes. Why settled: these are kubectl's read verbs (`get`, `describe`, `explain`, `wait`) and Temporal's run verbs (`start`, `execute`, `signal`, `cancel`), with spore's own `resolve`, `continue`, `claim` and the step-worker verbs kept as they are. `serve` is the usual name for a long-running process that does work for others; `worker` stays the kind it serves as (`get workers`). `version` follows `kubectl version`. `version -o json` keeps the whole handshake `capabilities` gives today, `actionKinds`, `asyncKinds` and `resultChannels` included, so a caller such as `ml-workflow` checks the host in one call before it starts anything. Action kinds (`command`, `external`, `native`) are host capabilities, not kinds a verb targets, so they stay in that handshake and are never rows of `get kinds`.