@tikoci/centrs 0.1.6 → 0.1.8-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/CHANGELOG.md +342 -0
  2. package/GLOSSARY.txt +32 -0
  3. package/README.md +10 -6
  4. package/SECURITY.md +1 -1
  5. package/commands/AGENTS.md +2 -1
  6. package/commands/api/AGENTS.md +55 -30
  7. package/commands/api/README.md +135 -42
  8. package/commands/api/examples.md +275 -19
  9. package/commands/devices/README.md +21 -4
  10. package/commands/devices/examples.md +33 -2
  11. package/commands/execute/README.md +60 -18
  12. package/commands/execute/examples.md +224 -0
  13. package/commands/explain/README.md +42 -5
  14. package/commands/mcp/README.md +2 -1
  15. package/commands/retrieve/README.md +348 -4
  16. package/commands/retrieve/examples.md +442 -5
  17. package/commands/terminal/README.md +14 -4
  18. package/commands/terminal/examples.md +22 -3
  19. package/commands/transfer/README.md +55 -9
  20. package/commands/transfer/examples.md +106 -7
  21. package/docs/CLI.md +29 -21
  22. package/docs/CONSTITUTION.md +41 -16
  23. package/docs/MATRIX.md +38 -1
  24. package/docs/errors/README.md +3 -1
  25. package/docs/errors/cdb/create-failed.md +20 -0
  26. package/docs/errors/cdb/file-exists.md +23 -0
  27. package/docs/errors/input/invalid-query.md +36 -0
  28. package/docs/errors/input/invalid-target.md +25 -3
  29. package/docs/errors/input/local-destination.md +20 -0
  30. package/docs/errors/input/unsupported-query.md +19 -0
  31. package/docs/errors/internal/byte-string.md +13 -0
  32. package/docs/errors/routeros/invalid-value.md +3 -2
  33. package/docs/errors/settings/invalid-timeout.md +6 -0
  34. package/docs/errors/transport/cancel-unacknowledged.md +24 -0
  35. package/docs/errors/transport/stream-overflow.md +21 -0
  36. package/docs/errors/usage/removed-flag.md +20 -0
  37. package/docs/errors/validation/menu-unavailable.md +22 -0
  38. package/docs/errors/validation/not-followable.md +23 -0
  39. package/docs/errors/validation/package-missing.md +29 -0
  40. package/docs/errors/validation/unknown-path.md +16 -4
  41. package/docs/errors/wait/deadline-exceeded.md +14 -0
  42. package/docs/errors/wait/interrupted.md +10 -0
  43. package/package.json +14 -11
  44. package/src/absent-menu.ts +280 -0
  45. package/src/api-fanout.ts +22 -12
  46. package/src/api.ts +603 -194
  47. package/src/btest.ts +19 -17
  48. package/src/cli/api.ts +126 -34
  49. package/src/cli/devices.ts +16 -3
  50. package/src/cli/execute.ts +18 -5
  51. package/src/cli/retrieve.ts +216 -22
  52. package/src/cli/terminal.ts +10 -5
  53. package/src/cli/transfer.ts +23 -8
  54. package/src/core/envelope.ts +14 -0
  55. package/src/core/error-catalog.ts +70 -1
  56. package/src/core/execute-command.ts +313 -6
  57. package/src/core/inspect.ts +65 -27
  58. package/src/core/query.ts +508 -0
  59. package/src/core/routeros-errors.ts +67 -1
  60. package/src/data/winbox-cdb-write.ts +22 -8
  61. package/src/devices.ts +148 -37
  62. package/src/discover.ts +7 -8
  63. package/src/errors.ts +116 -7
  64. package/src/execute-fanout.ts +14 -9
  65. package/src/execute.ts +128 -116
  66. package/src/explain/catalog.ts +64 -16
  67. package/src/explain/menus.ts +27 -6
  68. package/src/explain.ts +6 -6
  69. package/src/index.ts +31 -1
  70. package/src/mcp/tools.ts +10 -15
  71. package/src/protocols/adapter.ts +235 -118
  72. package/src/protocols/btest-session.ts +3 -2
  73. package/src/protocols/endpoint.ts +10 -0
  74. package/src/protocols/mac-telnet-console.ts +12 -7
  75. package/src/protocols/native-api.ts +252 -73
  76. package/src/protocols/sftp.ts +44 -14
  77. package/src/protocols/ssh.ts +42 -14
  78. package/src/resolver/cdb.ts +28 -7
  79. package/src/resolver/index.ts +1 -0
  80. package/src/resolver/target.ts +84 -18
  81. package/src/retrieve-fanout.ts +51 -12
  82. package/src/retrieve-follow.ts +862 -0
  83. package/src/retrieve-sample.ts +244 -0
  84. package/src/retrieve-stream.ts +115 -0
  85. package/src/retrieve-wait.ts +286 -0
  86. package/src/retrieve.ts +741 -109
  87. package/src/settings.ts +15 -15
  88. package/src/transfer-fanout.ts +40 -15
  89. package/src/transfer.ts +234 -32
@@ -118,6 +118,15 @@ past 60 KB) rather than quietly switching to sftp. `scp`, `fetch`, and `ftp` are
118
118
  explicit-only; `ftp` additionally requires `ALLOW_UNSAFE_PROTOCOLS=ftp` because
119
119
  it is cleartext.
120
120
 
121
+ **File contents are bytes on `rest`/`native`.** RouterOS carries `/file`
122
+ contents as raw bytes on both wires — REST puts bytes 0x80–0xff unescaped in the
123
+ JSON string, so the body is not UTF-8 — and stores the UTF-8 encoding of every
124
+ JSON code point it receives (CHR 7.23.7 + 7.24.5, #404). So the `/file`
125
+ `get`/`read`/`set` calls run in the adapters' `binary` mode: each byte is one
126
+ string code unit (Node `Buffer` latin1 — not WHATWG `TextDecoder("latin1")`,
127
+ which is windows-1252), and a file name in those calls is sent as its UTF-8
128
+ bytes. Every other command stays UTF-8 text.
129
+
121
130
  > Future methods documented so callers know the capability exists, behind an
122
131
  > explicit `--via`: `/system/smb` (not enabled by default) and `rose-storage`
123
132
  > (`rsync`/`nfs`/`nvme-over-tcp`/`iscsi`, needs `rose-storage.npk`).
@@ -158,16 +167,25 @@ Implemented flags are generated from the CLI metadata into
158
167
  [`docs/CLI.md` → transfer](../../docs/CLI.md#transfer); this file does not
159
168
  duplicate that table. Behavior notes the generated reference cannot carry:
160
169
 
161
- - `--force`/`--overwrite` applies to the destination side (the local file for
162
- `download`, the remote file for `upload`); the default refuses an existing
163
- target with `usage/target-exists` (see *Validation*).
170
+ - `--force`/`--overwrite` skips the existing-**device**-destination check of
171
+ `upload`, `mkdir` and `copy`, which by default refuse an existing target
172
+ with `usage/target-exists` (see *Validation*). `upload` then overwrites the
173
+ file; `mkdir` and `copy` still run as RouterOS's own `/file` commands.
174
+ `download` replaces an existing **local** file without `--force`, as
175
+ `curl -o` and `scp` do (#406). The local file is written only after the
176
+ whole remote read completes and, under the default `--verify size`, matches
177
+ the device's size, so a failed read or a size mismatch leaves an existing
178
+ file untouched. The write itself is not atomic: a local write that fails
179
+ part-way can leave the file truncated.
164
180
  - `--verify` details and the sftp size caveat are in *Integrity*.
165
181
  - `--timeout`: `rest`/`native` are per-request ≤ 60000 ms (a chunked read is
166
182
  many short requests, each capped); `sftp` accepts longer for a single large
167
183
  transfer.
168
184
  - `--ssh-key` is the same `sshKey` setting as `terminal`/`execute`
169
185
  (`CENTRS_SSH_KEY`, CDB `ssh-key=`); when unset, the ssh-agent /
170
- `~/.ssh/config` is used. See `commands/terminal/README.md`.
186
+ `~/.ssh/config` is used. A selected key adds `IdentitiesOnly=yes` to exclude
187
+ unrelated agent keys; explicit OpenSSH `IdentityFile` entries still apply.
188
+ See `commands/terminal/README.md`.
171
189
  - `--insecure` adds a `transport/insecure-trust` warning (constitution:
172
190
  transport trust).
173
191
  - `--quickchr` resolves host/port/auth from the live `@tikoci/quickchr`
@@ -201,6 +219,12 @@ single-target. On the top-level `upload`/`download` alias the verb is fixed, so
201
219
  its positionals are paths — positional fan-out targets are not expressible there;
202
220
  fan-out comes from a selector flag.
203
221
 
222
+ In the verb form, the `--host` **flag** with no positional before the verb is the
223
+ single target (`transfer --host 192.0.2.10 list`), with no CDB lookup.
224
+ `CENTRS_HOST` never stands in for `<router>`. The `upload`/`download` aliases
225
+ still read their first positional as the target, since their optional second
226
+ path makes a count ambiguous.
227
+
204
228
  Two transfer-specific rules:
205
229
 
206
230
  - **`download` fan-out requires `--out-dir <dir>`.** N devices cannot share one
@@ -261,11 +285,15 @@ success, per-target results live in `data.targets`.
261
285
  ## Validation
262
286
 
263
287
  `transfer` validates what it can locally first — conflicting flags
264
- (`usage/conflicting-flags`), a missing local source on `upload`, a `--via rest`
288
+ (`usage/conflicting-flags`), a missing local source on `upload`, a `download`
289
+ destination whose parent directory is missing or that is itself a directory
290
+ (`input/local-destination`; centrs does not create directories), a `--via rest`
265
291
  upload that exceeds 60 KB (`transport/unsupported-operation`). The
266
- **refuse-overwrite** guard is a real precondition probe, not a local check:
267
- unless `--force`, centrs `stat`s the destination (SFTP `stat`, or `/file/print`
268
- for the REST family) and fails with `usage/target-exists` if it is already there.
292
+ **refuse-overwrite** guard protects device files and is a real precondition
293
+ probe, not a local check: unless `--force`, centrs `stat`s the device
294
+ destination (SFTP `stat`, or `/file/print` for the REST family) and fails with
295
+ `usage/target-exists` if it is already there. A `download` overwrites its local
296
+ destination (see *Flags*).
269
297
  This is the "validate before write" gate applied to files — see constitution:
270
298
  validation is the product.
271
299
 
@@ -274,6 +302,20 @@ gate as in `retrieve` / `execute`; RouterOS's own menu errors (missing remote
274
302
  file, permission denied) map to `routeros/*`, and SFTP/SCP/fetch errors map to
275
303
  `transport/*` and `auth/*`.
276
304
 
305
+ A failure **after** target resolution keeps what resolution found: the error
306
+ envelope carries the resolved `meta.target` (host, port, identity, sources),
307
+ `meta.via`, the setting sources in `meta.settings`, and `meta.operation`
308
+ (including `auth.username` / `auth.passwordProvided` — provenance, never the
309
+ secret). This holds for single-target runs and for each fan-out member. A
310
+ failure before resolution (bad shape, unknown target) keeps only
311
+ `meta.target.input`. An sftp `auth/failed` with a resolved password adds
312
+ `tip/sftp-password-not-used`, plus `tip/transfer-password-transport` unless the
313
+ upload is known to exceed the rest/native write cap — advice only, never a
314
+ fallback across a pinned `--via` (examples S7–S9). Only OpenSSH's pre-login
315
+ refusal is `auth/failed`: a user whose group lacks `write` logs in and gets
316
+ `routeros/command-failed` for upload/mkdir/remove, and one lacking `ftp` gets
317
+ `routeros/session-closed` (SSH disconnect reason 7) — example S9.
318
+
277
319
  **Error codes reuse existing namespaces — `transfer` adds no namespace of its
278
320
  own.** Everything specific to transfer fits `usage/*` (`usage/target-exists`),
279
321
  `transport/*` (`transport/unsupported-operation` for the REST 60 KB write cap,
@@ -342,7 +384,11 @@ done rule. The first coded pass targets `rest`/`native` (no SSH dependency);
342
384
  / [User](https://help.mikrotik.com/docs/spaces/ROS/pages/8978504/User) pages:
343
385
  **by default** a user cannot password-auth over SSH once an SSH key is set for it
344
386
  (`/ip/ssh password-authentication=yes-if-no-key`, the default — settable to
345
- `yes` to allow both), so key auth is the normal path; and `strong-crypto=yes`
387
+ `yes` to allow both; CHR-grounded on 7.23.8 and 7.24.5 by
388
+ `test/integration/ssh-auth-policy.test.ts`). centrs's batch sftp runs
389
+ `BatchMode=yes` and never sends a password at all — it authenticates with the
390
+ selected key or the host OpenSSH configuration/agent — so key auth is the
391
+ only sftp path regardless of that policy; and `strong-crypto=yes`
346
392
  disables ssh-rsa/SHA1, so the client must offer ed25519 / rsa-sha2-256 (the host
347
393
  OpenSSH negotiates this natively). The SSH file path is the SFTP **subsystem**
348
394
  (a real file protocol), not exec-driven scp; on-device `copy`
@@ -15,7 +15,10 @@ Conventions (provided by the harness):
15
15
  - `$A` is `<host>` and `$API_PORT` is the native-API port (`chr.ports.api`) for
16
16
  `--via native-api`.
17
17
  - `$SRC` is a small local text file (~2 KB) the harness creates; `$TMP` is a
18
- scratch directory for downloads. `$BIG` is a local file **larger than 60 KB**.
18
+ scratch directory for downloads. `$BIG` is a local file **larger than 60 KB**:
19
+ every byte value, then deterministic noise, with a UTF-8 `€` straddling each
20
+ 32 KiB `/file/read` chunk boundary. `$BIN` is the same shape at exactly 60 KB,
21
+ the largest the `/file/set` write cap allows.
19
22
  - Device-side paths are **bare** (a CHR has no `flash` disk, so everything
20
23
  persists for the session); the harness removes `centrs-*` test files between
21
24
  runs. The CHR free license caps throughput at 1 Mb/s, so fixtures stay small —
@@ -77,6 +80,15 @@ centrs transfer $R list --name 'centrs-*' --username $U --password $P --json
77
80
 
78
81
  Envelope: every row's `name` matches the glob; `centrs-up.txt` is present.
79
82
 
83
+ ### H1. `--host` stands in for `<router>` (#428)
84
+
85
+ ```bash
86
+ centrs transfer --host $R list --username $U --password $P --json
87
+ ```
88
+
89
+ `ok: true`, with `data` the device's file list. Nothing comes before the verb,
90
+ so `--host` is the target and there is no CDB lookup.
91
+
80
92
  ## Validate before write
81
93
 
82
94
  ### 6. Refuse overwrite without `--force`
@@ -202,8 +214,8 @@ centrs transfer $R download centrs-big.bin $TMP/big.bin --username $U --password
202
214
  ```
203
215
 
204
216
  Envelope: `ok: true`, `meta.via="rest-api"`, `data.bytes` equals the seeded size
205
- (>60 KB). `$TMP/big.bin` is byte-identical to the seed — proving the chunked read
206
- reassembles correctly across `/file/read` calls.
217
+ (>60 KB). `$TMP/big.bin` has the seed's sha256 — proving the chunked read
218
+ reassembles correctly across `/file/read` calls and keeps every byte value.
207
219
 
208
220
  ## Error / transport contract
209
221
 
@@ -284,14 +296,46 @@ centrs transfer $A upload $BIG centrs-too-big.txt --via native-api --port $API_P
284
296
 
285
297
  Envelope: `ok: false`, `error.code="transport/unsupported-operation"`.
286
298
 
299
+ ## Binary contents (#404)
300
+
301
+ A size check cannot see a same-length substitution, so these compare sha256.
302
+
303
+ ### B1. Binary round-trip over REST
304
+
305
+ ```bash
306
+ centrs transfer $R upload $BIN centrs-bin-rest.bin --username $U --password $P --json
307
+ centrs transfer $R download centrs-bin-rest.bin $TMP/bin-rest.bin --username $U --password $P --json
308
+ ```
309
+
310
+ Upload envelope: `data.bytes=60000`, `data.verified="size"`. `$TMP/bin-rest.bin`
311
+ has the sha256 of `$BIN`.
312
+
313
+ ### B2. Binary round-trip over native-api
314
+
315
+ ```bash
316
+ centrs transfer $A upload $BIN centrs-bin-nv.bin --via native-api --port $API_PORT --username $U --password $P --json
317
+ centrs transfer $A download centrs-bin-nv.bin $TMP/bin-nv.bin --via native-api --port $API_PORT --username $U --password $P --json
318
+ ```
319
+
320
+ Same as B1.
321
+
322
+ ### B3. Chunked binary read over native-api
323
+
324
+ ```bash
325
+ centrs transfer $A download centrs-big.bin $TMP/nv-big.bin --via native-api --port $API_PORT --username $U --password $P --json
326
+ ```
327
+
328
+ Runs after S3 seeds `centrs-big.bin`. `$TMP/nv-big.bin` has the sha256 of `$BIG`.
329
+
287
330
  ## ssh (`--via sftp`)
288
331
 
289
332
  The first SSH consumer: a key-authenticated SFTP round-trip over the host OpenSSH
290
333
  `sftp` subsystem. The harness generates an ed25519 keypair, imports the public
291
334
  half to the CHR user (`/user ssh-keys import`), and drives `--via sftp --ssh-key`
292
335
  against the CHR's forwarded SSH port; `--insecure` accepts the ephemeral host key
293
- (default is `accept-new` trust-on-first-use). RouterOS refuses password login once
294
- a key is set, and centrs's sftp runs `BatchMode=yes`, so key auth is the path.
336
+ (default is `accept-new` trust-on-first-use). centrs's sftp runs `BatchMode=yes`
337
+ and never sends a password, so key auth is the only path (S7); RouterOS's own
338
+ password policy is a separate fact (S8).
295
339
 
296
340
  ### S1. sftp upload (host → device)
297
341
 
@@ -315,8 +359,9 @@ centrs transfer 127.0.0.1 --via sftp --port $SSH_PORT --username $U --ssh-key $K
315
359
  centrs transfer 127.0.0.1 --via sftp --port $SSH_PORT --username $U --ssh-key $KEY --insecure upload $BIG centrs-big.bin --json
316
360
  ```
317
361
 
318
- Envelope: `ok: true`. This is also the seed for example 17 (the same file then
319
- reads back over REST via chunked `/file/read`).
362
+ Envelope: `ok: true`. An sftp download of it has the sha256 of `$BIG`. This is
363
+ also the seed for examples 17 and B3 (the same file then reads back over REST and
364
+ native-api via chunked `/file/read`).
320
365
 
321
366
  ### S4. list over sftp
322
367
 
@@ -336,6 +381,60 @@ centrs transfer 127.0.0.1 --via sftp --port $SSH_PORT --username $U --ssh-key $K
336
381
  Both `ok: true`. On-device `copy` has no SFTP primitive — `--via sftp copy` returns
337
382
  `transport/unsupported-operation` (it stays on rest/native).
338
383
 
384
+ ### S6. A rejected selected key fails despite a trusted agent identity
385
+
386
+ The shared [SSH identity example S5](../execute/examples.md#s5-an-explicit-rejected-key-cannot-fall-back-to-an-unrelated-agent-key)
387
+ runs a real SFTP list with an untrusted `--ssh-key` and a trusted key in a
388
+ private agent. It fails with `auth/failed`; selecting the trusted key or leaving
389
+ the key unset succeeds. Covered by `test/integration/ssh-identity.test.ts`.
390
+
391
+ ### S7. A resolved password does not authenticate batch sftp
392
+
393
+ `$PW_USER` has a password, no SSH key, and the default
394
+ `password-authentication=yes-if-no-key`; a plain OpenSSH password login as that
395
+ user succeeds. The host OpenSSH runs with an empty config and no agent.
396
+
397
+ ```bash
398
+ centrs transfer 127.0.0.1 --via sftp --port $SSH_PORT --username $PW_USER --password $PW_PASS --insecure list --json
399
+ ```
400
+
401
+ Exit 1. Envelope: `ok: false`, `error.code="auth/failed"`,
402
+ `error.context.sshKeySelected=false`, and the resolved meta — `meta.via="ssh"`,
403
+ `meta.target.host`/`port`, `meta.settings.password={kind:"cli",key:"password"}`,
404
+ `meta.operation.auth={username:$PW_USER,passwordProvided:true}`. `tips` is
405
+ `tip/sftp-password-not-used` then `tip/transfer-password-transport`; text output
406
+ lists the same tips. The password appears nowhere in either output.
407
+
408
+ ### S8. RouterOS password policy, and the key path that works
409
+
410
+ After `/user ssh-keys import` installs `$KEY.pub` for `$PW_USER`, the plain
411
+ OpenSSH password login is refused (`Permission denied`), and:
412
+
413
+ ```bash
414
+ centrs transfer 127.0.0.1 --via sftp --port $SSH_PORT --username $PW_USER --ssh-key $KEY --insecure list --json
415
+ ```
416
+
417
+ Envelope: `ok: true`. With `/ip ssh set password-authentication=yes`, the
418
+ password login succeeds again with the key still installed.
419
+
420
+ ### S9. Group policy refusals are not auth failures
421
+
422
+ Each user holds the installed `$KEY`, and a password is passed too:
423
+
424
+ ```bash
425
+ centrs transfer 127.0.0.1 --via sftp --port $SSH_PORT --username $USER_NO_WRITE --password $PW_PASS --ssh-key $KEY --insecure upload $SRC centrs-391.txt --json
426
+ ```
427
+
428
+ | User's group policy | Verb | `error.code` |
429
+ | ------------------- | ---- | ------------ |
430
+ | `ssh,ftp,read` (no `write`) | `upload`, `mkdir` | `routeros/command-failed`, `tips: []` |
431
+ | `ssh,read,write` (no `ftp`) | `list` | `routeros/session-closed` (SSH disconnect reason 7) |
432
+ | `ftp,read,write` (no `ssh`) | `list` | `auth/failed` |
433
+
434
+ Only OpenSSH's pre-login `Permission denied (publickey…)` is `auth/failed`; a
435
+ bare `Permission denied` after login is a policy refusal, so no password tips
436
+ are attached. S7–S9 are covered by `test/integration/ssh-auth-policy.test.ts`.
437
+
339
438
  ## Target selection (fan-out)
340
439
 
341
440
  These exercise the shared target-selection grammar
package/docs/CLI.md CHANGED
@@ -27,12 +27,12 @@ implemented flags live in each command's `commands/<name>/README.md`.
27
27
  Read RouterOS values through the shared core using the selected protocol.
28
28
 
29
29
  ```text
30
- Usage: centrs retrieve <target> <routeros-path> [flags] | centrs retrieve <target...> <routeros-path> [flags] | centrs retrieve --group <name> <routeros-path> [flags]
30
+ Usage: centrs retrieve <target> <routeros-path> [flags] | centrs retrieve <target...> <routeros-path> [flags] | centrs retrieve --host <host|url> <routeros-path> [flags] | centrs retrieve --group <name> <routeros-path> [flags]
31
31
  ```
32
32
 
33
33
  | Flag | Value | Description |
34
34
  | ---- | ----- | ----------- |
35
- | `--via` | `<protocol>` | Pin the protocol selector. Defaults to `rest-api` for retrieve. |
35
+ | `--via` | `<protocol>` | Pin the protocol selector. Defaults to `rest-api` for retrieve (`--sample` included), and to `native-api` under `--follow`. |
36
36
  | `--group` | `<name>` | Fan out across every CDB record in the group (repeatable; de-duped by record index). |
37
37
  | `--where` | `<attr>`=`<value>` | Device-class selector over CDB facts + core fields (repeatable, AND-combined). |
38
38
  | `--near` | `<lat>`,`<lon>`,`<radius>` | Geo selector: devices whose GPS is within radius (m/km/mi/ft; bare number = km). Lat-first. |
@@ -41,17 +41,25 @@ Usage: centrs retrieve <target> <routeros-path> [flags] | centrs retrieve <targe
41
41
  | `--default` | | Select the reserved `__default__` record. |
42
42
  | `--quickchr` | `<name>` | Target a running quickchr-managed CHR VM by name (repeatable; fans out when repeated). Exclusive of positional targets and CDB selectors; conflicts with direct connection overrides (--host/--port/--username/--password, and --ssh-key where the command has it). |
43
43
  | `--concurrency` | `<n>` | Max in-flight targets during fan-out (transport-aware default: rest-api 8, native-api 4). |
44
- | `--host` | &lt;host\|url&gt; | Override the resolved host or base URL for the target. |
44
+ | `--host` | &lt;host\|url&gt; | Host or base URL to dial. With no `<target>` it is the target (no CDB lookup); with one it overrides the resolved host. |
45
45
  | `--port` | `<port>` | Override the resolved management port. |
46
46
  | `--username / --user / -u` | `<name>` | RouterOS username (aliases `--user`, `-u`). Falls back to `CENTRS_USERNAME`. |
47
47
  | `--password` | `<secret>` | RouterOS password. Falls back to `CENTRS_PASSWORD`. |
48
- | `--timeout` | &lt;ms\|5s&gt; | Operation timeout. REST currently rejects values above 60s. |
48
+ | `--timeout` | &lt;ms\|5s&gt; | Per-request timeout, separate from --wait's overall deadline. REST currently rejects values above 60s. |
49
49
  | `--attribute` | `<name>` | Project one attribute. May be repeated. |
50
50
  | `--attributes` | `<a,b>` | Project a comma-separated attribute list. |
51
51
  | `--all-attributes` | | Request the RouterOS detail/all-attributes shape. |
52
52
  | `--list-attributes / --list` | | List inspect-derived attributes without running the data call. |
53
- | `--query / --filter` | `<expr>` | RouterOS-side row filter (maps to `.query`). Not implemented yet — returns `validation/not-implemented`. |
54
- | `--format` | &lt;text\|json\|yaml&gt; | Output format for the CLI response. Defaults to text; use --json or --format json for the structured envelope. |
53
+ | `--query / --filter` | `<expr>` | Row filter written as after `print where` (`disabled`, `mtu>=1500 and !dynamic`), run on the router as an API query; repeatable, AND-ed. No `~` or `in`. |
54
+ | `--follow` | | Keep the menu's rows current over native-api `listen`: snapshot frames, a `synced` line, then a line per change, then a summary (NDJSON for json/yaml/ndjson). Single target, list menus whose print takes follow-only. |
55
+ | `--sample` | `<interval>` | Read the menu again every interval (with a unit, e.g. `5s`): one line per complete read, then a summary. With --wait, sets polling cadence and returns one final envelope. Sees counters and state listen never reports. Single target; rest-api or native-api. |
56
+ | `--wait` | `<deadline>` | Wait for a successful read or a condition until this overall deadline, with a unit (e.g. `2m`). Retries refused, closed, reset and timed-out connections and a down or unreachable host; other errors fail at once. One final envelope; single target. |
57
+ | `--until` | `<expr>` | With --wait: succeed when any selected row matches this --query grammar predicate; repeatable, AND-ed. Returns the matching rows. Already true succeeds on the initial read. |
58
+ | `--until-empty` | | With --wait: succeed on a completed read with zero rows selected by --query. Silence and failed reads never satisfy it. |
59
+ | `--sweep` | `<duration>` | `--follow` only: how often a membership sweep finds removals RouterOS does not send, including silent exits from --query. Needs a unit; default `10s`; `0` turns it off. |
60
+ | `--count` | `<n>` | `--follow`: stop after N live changes (snapshot frames and `synced` do not count). `--sample`: stop after N samples. |
61
+ | `--duration` | `<duration>` | `--follow`/`--sample`: stop after this wall-clock window (e.g. `30s`), counted from the end of validation. |
62
+ | `--format` | &lt;text\|json\|yaml\|ndjson&gt; | Output format for the CLI response. Defaults to text; use --json or --format json for the structured envelope. `ndjson` prints each envelope as one compact line. |
55
63
  | `--json` | | Shortcut for `--format json`. |
56
64
  | `--max-results` | `<bytes>` | Fail instead of printing output larger than the given byte budget. |
57
65
  | `--cdb-file` | `<path>` | Read target credentials from a WinBox CDB file. |
@@ -65,7 +73,7 @@ Usage: centrs retrieve <target> <routeros-path> [flags] | centrs retrieve <targe
65
73
  Run a RouterOS read or write command via native API, REST, or mac-telnet.
66
74
 
67
75
  ```text
68
- Usage: centrs execute <target> <command> [flags] | centrs execute <target...> -- <command> [flags] | centrs execute --group <name> <command> [flags]
76
+ Usage: centrs execute <target> <command> [flags] | centrs execute <target...> -- <command> [flags] | centrs execute --host <host|url> [--] <command> [flags] | centrs execute --group <name> <command> [flags]
69
77
  ```
70
78
 
71
79
  | Flag | Value | Description |
@@ -79,7 +87,7 @@ Usage: centrs execute <target> <command> [flags] | centrs execute <target...> --
79
87
  | `--default` | | Select the reserved `__default__` record. |
80
88
  | `--quickchr` | `<name>` | Target a running quickchr-managed CHR VM by name (repeatable; fans out when repeated). Exclusive of positional targets and CDB selectors; conflicts with direct connection overrides (--host/--port/--username/--password, and --ssh-key where the command has it). |
81
89
  | `--concurrency` | `<n>` | Max in-flight targets during fan-out (transport-aware default: rest-api 8, native-api 4). |
82
- | `--host` | &lt;host\|url&gt; | Override the resolved host or base URL for the target. |
90
+ | `--host` | &lt;host\|url&gt; | Host or base URL to dial. With no `<target>` it is the target (no CDB lookup); with one it overrides the resolved host. |
83
91
  | `--port` | `<port>` | Override the resolved management port. |
84
92
  | `--username / --user / -u` | `<name>` | RouterOS username (aliases `--user`, `-u`). Falls back to `CENTRS_USERNAME`. |
85
93
  | `--password` | `<secret>` | RouterOS password. Falls back to `CENTRS_PASSWORD`. |
@@ -102,7 +110,7 @@ Usage: centrs execute <target> <command> [flags] | centrs execute <target...> --
102
110
  Structured RouterOS API passthrough (gh-api style) over REST or native API.
103
111
 
104
112
  ```text
105
- Usage: centrs api <router> <endpoint> [flags]
113
+ Usage: centrs api <router> <endpoint> [flags] | centrs api --host <host|url> <endpoint> [flags]
106
114
  ```
107
115
 
108
116
  | Flag | Value | Description |
@@ -111,13 +119,13 @@ Usage: centrs api <router> <endpoint> [flags]
111
119
  | `-f / --field` | `<key=value>` | Body field, repeatable; assembled into the JSON body (verbatim string values). |
112
120
  | `-d / --data` | `<json>` | Raw JSON request body. Conflicts with `-f` / `--input`. |
113
121
  | `--input` | &lt;file\|-&gt; | Read the raw JSON body from a file or stdin (`-`). |
114
- | `--query / --filter` | `<expr>` | RouterOS-side row filter, AND-combined, repeatable: name=value, name!=value, name>value, name<value, name. |
115
- | `--raw-query` | `<word>` | Verbatim RouterOS query word (repeatable) for OR / absence / stack expressions. |
122
+ | `--query / --filter` | `<expr>` | Row filter written as after `print where` (`disabled`, `mtu>=1500 and !dynamic`), sent as query words; repeatable, AND-ed. No `~` or `in`. |
123
+ | `--raw-query` | `<word>` | Verbatim RouterOS query word (repeatable), e.g. has-property `name`, absence `-name`, or stack ops `#\|` `#&` `#!`. |
116
124
  | `--attribute / --proplist` | `<a,b>` | Property projection → `.proplist`. |
117
125
  | `--raw` | | Strip the envelope; emit bare RouterOS JSON. Defaults `--validate` to false (an explicit `--validate=true` still runs the gate); does not imply `--yes`. |
118
126
  | `--yes` | | Confirm a mutating (non-read) request in non-interactive runs. |
119
- | `--stream / --listen` | | Follow changes as an NDJSON envelope stream (native-api only; the `/listen` endpoint infers it). Ends with a summary envelope. |
120
- | `--count` | `<n>` | Stop a `--stream` after N change frames. |
127
+ | `--stream` | | Stream the command's native replies (`!re` rows and `!empty`) as NDJSON, then a summary envelope. Never changes the command: a GET streams one print. To follow changes, request `<menu>/listen`, which implies it. |
128
+ | `--count` | `<n>` | Stop a `--stream` after N `!re` rows; `!empty` replies do not count (not the device count= argument). |
121
129
  | `--duration` | `<dur>` | Stop a `--stream` after this wall-clock window (e.g. `5s`). |
122
130
  | `--via` | &lt;rest-api\|native-api&gt; | Pin the transport; no silent downgrade. Default rest-api. |
123
131
  | `--group` | `<name>` | Fan out across every CDB record in the group (repeatable; de-duped by record index). |
@@ -128,7 +136,7 @@ Usage: centrs api <router> <endpoint> [flags]
128
136
  | `--default` | | Select the reserved `__default__` record. |
129
137
  | `--quickchr` | `<name>` | Target a running quickchr-managed CHR VM by name (repeatable; fans out when repeated). Exclusive of positional targets and CDB selectors; conflicts with direct connection overrides (--host/--port/--username/--password, and --ssh-key where the command has it). |
130
138
  | `--concurrency` | `<n>` | Max in-flight targets during fan-out (transport-aware default: rest-api 8, native-api 4). |
131
- | `--host` | &lt;host\|url&gt; | Override the resolved host or base URL for the target. |
139
+ | `--host` | &lt;host\|url&gt; | Host or base URL to dial. With no `<router>` it is the target (no CDB lookup); with one it overrides the resolved host. |
132
140
  | `--port` | `<port>` | Override the resolved management port. |
133
141
  | `--username / --user / -u` | `<name>` | RouterOS username. Falls back to `CENTRS_USERNAME`. |
134
142
  | `--password` | `<secret>` | RouterOS password. Falls back to `CENTRS_PASSWORD`. |
@@ -138,7 +146,7 @@ Usage: centrs api <router> <endpoint> [flags]
138
146
  | `--resolve` | &lt;none\|arp&gt; | Resolve a MAC-address target to an IP via the host ARP cache (default none). |
139
147
  | `--timeout` | `<duration>` | Per-request timeout (for REST, max 60s). |
140
148
  | `--validate / --no-validate` | | Run `/console/inspect` validation before the request (default true; `--validate=false` also accepted). |
141
- | `--format` | &lt;json\|yaml\|text&gt; | Output format. Defaults to json for api; `CENTRS_FORMAT` overrides. |
149
+ | `--format` | &lt;json\|ndjson\|yaml\|text&gt; | Output format. Defaults to json for api; `CENTRS_FORMAT` overrides. |
142
150
  | `--json` | | Shortcut for `--format json`. |
143
151
  | `--verbose` | | Include additional context in text output. |
144
152
 
@@ -147,7 +155,7 @@ Usage: centrs api <router> <endpoint> [flags]
147
155
  Copy files to/from a RouterOS device and manage device files (rest/native/sftp).
148
156
 
149
157
  ```text
150
- Usage: centrs transfer <router> upload <local> [remote] | download <remote> [local] | list [path] | remove <remote> | mkdir <remote> | copy <src> <dst> [flags]
158
+ Usage: centrs transfer <router>|--host <host> upload <local> [remote] | download <remote> [local] | list [path] | remove <remote> | mkdir <remote> | copy <src> <dst> [flags]
151
159
  ```
152
160
 
153
161
  | Flag | Value | Description |
@@ -155,7 +163,7 @@ Usage: centrs transfer <router> upload <local> [remote] | download <remote> [loc
155
163
  | `--via` | `<method>` | Pin the method: rest, native, or sftp (large transfers); scp/fetch/ftp are not built yet. Auto picks the cheapest by size/direction. |
156
164
  | `--ssh-key` | `<path>` | sftp only: explicit private-key path. Falls back to CENTRS_SSH_KEY / the ssh-agent. |
157
165
  | `--insecure` | | Accept a self-signed TLS cert (https/api-ssl) or a new SSH host key. Default verifies. |
158
- | `--force / --overwrite` | | Replace an existing destination. Default refuses it. |
166
+ | `--force / --overwrite` | | Skip the existing-destination check on the device for `upload`/`mkdir`/`copy` (`upload` then overwrites). Default refuses an existing target. `download` always replaces the local file. |
159
167
  | `--group` | `<name>` | Fan out across every CDB record in the group (repeatable; de-duped by record index). |
160
168
  | `--where` | `<attr>`=`<value>` | Device-class selector over CDB facts + core fields (repeatable, AND-combined). |
161
169
  | `--near` | `<lat>`,`<lon>`,`<radius>` | Geo selector: devices whose GPS is within radius (m/km/mi/ft; bare number = km). Lat-first. |
@@ -169,7 +177,7 @@ Usage: centrs transfer <router> upload <local> [remote] | download <remote> [loc
169
177
  | `--verify / --no-verify` | &lt;size\|checksum\|off&gt; | Post-transfer integrity check. Default size; `--no-verify` skips. |
170
178
  | `--type` | &lt;file\|directory\|disk\|package&gt; | list filter: RouterOS /file row type. |
171
179
  | `--name` | `<glob>` | list filter: file-name glob. |
172
- | `--host` | &lt;host\|url&gt; | Override the resolved host or base URL. |
180
+ | `--host` | &lt;host\|url&gt; | Host or base URL to dial. With no `<router>` before the verb it is the target (no CDB lookup); with one it overrides the resolved host. |
173
181
  | `--port` | `<port>` | Override the resolved management port. |
174
182
  | `--username / --user / -u` | `<name>` | RouterOS username (aliases --user, -u). Falls back to CENTRS_USERNAME. |
175
183
  | `--password` | `<secret>` | RouterOS password. Falls back to CENTRS_PASSWORD. |
@@ -187,13 +195,13 @@ Usage: centrs transfer <router> upload <local> [remote] | download <remote> [loc
187
195
  Open an interactive RouterOS console (ssh or mac-telnet).
188
196
 
189
197
  ```text
190
- Usage: centrs terminal <router> [flags]
198
+ Usage: centrs terminal <router> [flags] | centrs terminal --host <host> [flags]
191
199
  ```
192
200
 
193
201
  | Flag | Value | Description |
194
202
  | ---- | ----- | ----------- |
195
203
  | `--via` | `<method>` | Transport: `ssh` (host target, default) or `mac-telnet` (MAC target, default for a MAC). rest/native have no terminal capability. |
196
- | `--host` | `<host>` | Override the target host (ssh) / UDP delivery host (mac-telnet, default L2 broadcast). |
204
+ | `--host` | `<host>` | ssh: the host to dial (the target when no `<router>` is given; no CDB lookup). mac-telnet: the UDP delivery host (default L2 broadcast). |
197
205
  | `--port` | `<port>` | Override the port: ssh default 22, mac-telnet default 20561. |
198
206
  | `--username / --user / -u` | `<name>` | RouterOS username (aliases --user, -u). Falls back to CENTRS_USERNAME. |
199
207
  | `--password` | `<secret>` | RouterOS password. Falls back to CENTRS_PASSWORD. |
@@ -234,7 +242,7 @@ Usage: centrs explain '<input>' [flags]
234
242
  Inspect and mutate the CDB-backed device registry. `devices` is the only command that writes the CDB.
235
243
 
236
244
  ```text
237
- Usage: centrs devices <list|show|groups|add|edit|set|remove> [args] [flags]
245
+ Usage: centrs devices <init|list|show|groups|add|edit|set|remove> [args] [flags]
238
246
  ```
239
247
 
240
248
  | Flag | Value | Description |
@@ -63,6 +63,22 @@ an ABSTENTION and passes through to the device stage; only a `fail` rejects.
63
63
  that was skipped and why. A reader must always be able to tell "the device
64
64
  accepted it" from "the device was never asked".
65
65
 
66
+ `meta.validation.result` is a verdict, so it is present only when the gate
67
+ produced one. `failed` means the gate refused the request. A failure after the
68
+ gate passed keeps `passed`. When something stopped the gate before it gave a
69
+ verdict (a `transport/*` or `auth/*` failure, a device error that is not a
70
+ gate answer, or a refusal before the gate ran), `result` and the
71
+ `syntax`/`semantic` summaries are absent. Each stage the gate did not reach is
72
+ then `skipped` with `reason: "not reached: <error.code>"` (GH#435). On an
73
+ `execute` rejection, `syntax: true` survives only for a name-level refusal (an
74
+ unknown attribute in a structured command). Any other device or offline
75
+ refusal is `syntax: false`, and `semantic` is then absent (`not-applicable` for
76
+ script input), because the name-level check was never asked. `api`'s
77
+ structured requests run no `:parse`, so their `syntax: false` means "not run",
78
+ even on success. That overload is part of the review below. This is an
79
+ interim shape: the validation meta across commands is reviewed again with live
80
+ `explain` (#236).
81
+
66
82
  `--validate=false` disables **both** stages — one flag, one meaning. A bypass
67
83
  that covered only the device stage would leave a caller stranded behind a
68
84
  stage-1 defect with no escape at all, and the analyzer is demonstrably wrong in
@@ -84,8 +100,10 @@ validator, with CHR evidence.
84
100
  `expected`. Rejection forms are `(evl bad parameter <name> (line N column M)
85
101
  /path)`, `(<%% bad command name <name> (line N column M) …)`, and the
86
102
  line-anchored `expected … (line N column M)` (spellings: `expected end of
87
- command`, `expected command name`, `expected input value`) / `syntax error
88
- (line N column M)`. That text rides the **HTTP-200 `ret` value** (REST,
103
+ command`, `expected command name`, `expected input value`), `missing … (line
104
+ N column M)` (`missing closing brace`, `missing value for where`; GH#375) and
105
+ `syntax error (line N column M)`. Every bare form is anchored at a line start
106
+ with the position suffix, because an accepted return can echo those words. That text rides the **HTTP-200 `ret` value** (REST,
89
107
  `as-string`), the native `as-string` `ret`, and the console's printed output
90
108
  alike — so a single `:put [:parse ...]` covers both the syntax and the
91
109
  **unknown-attribute** gate on every transport. centrs sends `as-string` and now
@@ -276,7 +294,10 @@ token cannot spoof them), AND-combined across repeats. A call is in **fan-out
276
294
  mode** when any selector flag is present **or** more than one positional target is
277
295
  given; a plain single-positional call stays single-target. The union is
278
296
  **de-duped by CDB record index** (ad-hoc literal targets by host) and run through
279
- the same per-target pipeline.
297
+ the same per-target pipeline. With no `<router>` positional and no selector, the
298
+ `--host` **flag** stands in for the target: a single target with no CDB lookup,
299
+ so no record credential applies. The env var `CENTRS_HOST` never changes how
300
+ positionals split. Each command README states where its `<router>` slot ends.
280
301
 
281
302
  Fan-out output is the locked **`FanoutData`** envelope: `data = { summary, targets[]
282
303
  }`, where the outer `ok` reports **orchestration** success and a per-target failure
@@ -290,9 +311,9 @@ A multi-target **RouterOS write** (e.g. `execute` / `api` add/set/remove across
290
311
  selection) is gated by **`--yes`**, confirmed **once** up front; when it is missing
291
312
  the error names the blast radius (how many routers). The separate **`--force`**
292
313
  gate is scoped to destructive **`devices` CDB mutations** (e.g. `devices remove`
293
- across a group), not RouterOS writes. `terminal` and single-session `stream` /
294
- `api --stream` are not fan-out surfaces and reject N>1 with
295
- `usage/fanout-not-supported`; `api --raw` is rejected in fan-out mode with
314
+ across a group), not RouterOS writes. `terminal`, `api --stream`,
315
+ `retrieve --follow` and `retrieve --sample` are single-session, not fan-out
316
+ surfaces, and reject N>1 with `usage/fanout-not-supported`; `api --raw` is rejected in fan-out mode with
296
317
  `usage/conflicting-flags` (it strips the per-target envelope).
297
318
 
298
319
  `--where` filters *which devices* by CDB-stored facts; keep it distinct from
@@ -370,8 +391,8 @@ Per-operation preferences, downgrade order in parens:
370
391
 
371
392
  | Operation | Preferred | Downgrade order |
372
393
  | --------- | --------- | --------------- |
373
- | retrieve | rest-api, native-api; snmp for OID/MIB reads (future) | rest-api, native-api |
374
- | api | rest-api, native-api; `--stream` follow native-api only | rest-api, native-api (no silent downgrade) |
394
+ | retrieve | rest-api, native-api (`--sample` too); snmp for OID/MIB reads (future); `--follow` native-api only | rest-api, native-api (`--follow`: no downgrade) |
395
+ | api | rest-api, native-api; `--stream` and `/listen` native-api only | rest-api, native-api (no silent downgrade) |
375
396
  | execute | native-api → rest-api → mac-telnet | native-api, rest-api, mac-telnet, ssh, romon, winbox-terminal |
376
397
  | terminal | ssh | mac-telnet (L2 only when ssh fails or MAC given) |
377
398
  | transfer | size/direction-aware: rest-api/native-api for ≤60 KB writes & all reads (chunked), sftp for large uploads | sftp ⇄ rest/native by size; scp · fetch · ftp explicit-only |
@@ -398,7 +419,7 @@ Rules:
398
419
  - **`measure` (btest) is explicit-only** — never in the auto-select/downgrade
399
420
  chain, never a substitute, and no other command rides it.
400
421
  - Per-command selection **depth** lives in the command READMEs: `api`'s
401
- `--stream` follow/NDJSON contract and the REST-cannot-follow constraint
422
+ literal `--stream`/NDJSON contract and the REST-cannot-follow constraint
402
423
  (`commands/api/README.md`), `transfer`'s size/direction-aware method
403
424
  selection and the explicit-only `scp`/`ftp`/`fetch`
404
425
  (`commands/transfer/README.md`), SNMP's MIB-cache
@@ -430,9 +451,10 @@ ecosystem norm* rather than one literal value:
430
451
 
431
452
  SSH identity selection: the `sshKey` setting (`--ssh-key`/`CENTRS_SSH_KEY`/CDB
432
453
  `ssh-key=`) carries a private-key **path only** — never key material; when unset,
433
- the host SSH agent / `~/.ssh/config` is used. RouterOS refuses password login once
434
- an SSH key is set for a user (`password-authentication=yes-if-no-key`, the
435
- default), so key auth is the normal sftp path.
454
+ the host SSH agent / `~/.ssh/config` is used. Batch SSH clients (sftp, `execute
455
+ --via ssh`) run `BatchMode=yes` and never send a password, so key auth is the
456
+ only batch path. Separately, RouterOS refuses SSH password login once a user has
457
+ a key (`/ip/ssh password-authentication=yes-if-no-key`, the default).
436
458
 
437
459
  ## Done definition
438
460
 
@@ -535,8 +557,10 @@ as bibliography entries in `GLOSSARY.txt` — e.g. `netmiko`,
535
557
  ### Canonicalizer ownership
536
558
 
537
559
  centrs owns the **script-vs-structured execution gate** — the load-bearing
538
- discriminator for which validation runs and whether the write-confirmation prompt
539
- fires; widening what counts as `structured` is a product regression. The shared,
560
+ discriminator for validation and transport shape; widening what counts as
561
+ `structured` is a product regression. Write confirmation is a separate,
562
+ fail-closed safety predicate over the original CLI string: falling back to raw
563
+ script transport must never turn a write into a read. The shared,
540
564
  pure command canonicalizer that `rosetta` / `lsp-routeros-ts` publish is for
541
565
  canonicalization only, never the structured-mode predicate. The gate contract,
542
566
  its pinning test, and the parser-vendoring preconditions are documented in
@@ -546,5 +570,6 @@ its pinning test, and the parser-vendoring preconditions are documented in
546
570
  Richer schema-free analysis must distinguish **ambiguous** (multiple plausible
547
571
  readings) from **unknown** (no safe reading) and abstain in both cases rather than
548
572
  inventing a command, path, symbol class, or transport mapping. These explain-only
549
- verdicts never widen or reinterpret the execution gate's `mode` or `writeShaped`
550
- decision.
573
+ verdicts never widen or reinterpret the execution gate's `mode`. The execution
574
+ safety predicate remains deliberately conservative and independent of richer
575
+ explain-only inference.
package/docs/MATRIX.md CHANGED
@@ -60,6 +60,33 @@ and implementation sequencing in the linked issues.
60
60
  structured operation surface is `api`.
61
61
  - `api --stream` absorbs the former `stream`/`tail` surface; native-api stream
62
62
  details are in `commands/api/README.md` and `src/protocols/native-api.ts`.
63
+ #399 extends it to incremental command replies and truthful terminal outcomes.
64
+ #402 makes it literal: following is the `<menu>/listen` endpoint, every wire
65
+ reply (`!empty` included) is a line, and what RouterOS will not report
66
+ (filtered or projected change feeds) is a tip, not a rewrite.
67
+ - `retrieve --follow` (#396) is the stateful follow over native-api: A1
68
+ bootstrap, `synced`, live frames, and an `.id` sweep for removals RouterOS
69
+ does not send. Single-target; `CHR-passed` by
70
+ `test/integration/retrieve-follow.test.ts` (FL1–FL9, 7.23.7 and 7.24.5).
71
+ Filtered follow (#397) uses device-evaluated changed-id membership, guarded
72
+ against stale replies; `test/integration/retrieve-filtered-follow.test.ts`
73
+ covers FQ1–FQ5 on 7.23.8 and 7.24.5, including projected predicates, both membership directions,
74
+ minimal deletes, silent predicate exits and bootstrap/live churn.
75
+ - `retrieve --sample <interval>` repeats the one-shot read on a timer, one
76
+ line per complete read, over rest-api or native-api: the answer for counters
77
+ and state `listen` never reports. RouterOS's `print interval=` was measured
78
+ and not used (no end-of-tick marker). Single-target; `CHR-passed` by
79
+ `test/integration/retrieve-sample.test.ts` (SA1–SA6, 7.23.7 and 7.24.5).
80
+ - `retrieve --query` (#397) filters rows on the router with the `print where`
81
+ grammar, compiled to the API query; `api --query` shares it. One-shot,
82
+ `--sample` and fan-out; `CHR-passed` by
83
+ `test/integration/retrieve-query.test.ts` (QY1–QY7, QA1–QA2, 7.23.7 and
84
+ 7.24.5), whose QY1 checks the rows against RouterOS's own `find where`.
85
+ - `retrieve --wait` / `--until` / `--until-empty` (#364) return one bounded
86
+ readiness/condition result over REST or native-api, with elapsed time,
87
+ attempts and completed observations. `test/integration/retrieve-wait.test.ts`
88
+ covers WT1–WT7 on 7.23.8 and 7.24.5, including real reboot readiness and a
89
+ two-CHR OSPF adjacency.
63
90
  - `discover / mndp` is grounded by `commands/discover/README.md`,
64
91
  `src/data/mndp.ts`, `src/data/mndp-cache.ts`, and
65
92
  `test/integration/discover.test.ts`.
@@ -150,7 +177,17 @@ Wire, auth, TCP/UDP, and CI caveats live in `commands/btest/README.md` and the
150
177
 
151
178
  ## Current priority
152
179
 
153
- **Tightening validation as a whole** is the selected track, in the three-step
180
+ **First: the native-api / NDJSON line** (maintainer decision on
181
+ [#396](https://github.com/tikoci/centrs/issues/396), 2026-10-07): agents in the
182
+ field poll with `sleep` where a native follow would answer. In order:
183
+ (1) done: #402, literal `api --stream` with `!empty` frames and
184
+ `--format ndjson`; (2) done: `retrieve --follow` (#396, unfiltered);
185
+ (3) done: `retrieve --sample`; (4) #397: one-shot `retrieve --query` done,
186
+ filtered `--follow` done; #364 (`--wait`/`--until`, same grammar) done;
187
+ (5) #398 and #401. Each slice's contract lives in its command
188
+ README. Live `explain` (below) resumes after this line.
189
+
190
+ **Tightening validation as a whole** is the next track, in the three-step
154
191
  order agreed on [#236](https://github.com/tikoci/centrs/issues/236):
155
192
 
156
193
  1. **Done** — fix the offline false rejects so the analyzer can be trusted as a
@@ -59,10 +59,12 @@ Codes are slash-namespaced `family/slug`. The families and where they originate:
59
59
  - `transport/*` — connection, TLS, DNS, and timeout plumbing.
60
60
  - `usage/*` — how a command was invoked (arity, conflicting flags, confirmation).
61
61
  - `validation/*` — the canonicalize → validate gate (`:parse` + `/console/inspect`).
62
+ - `wait/*` — readiness/condition deadlines and cancellation (`retrieveWait`).
62
63
 
63
64
  The hand-enriched pages (richer trigger strings and remediation) currently are
64
65
  the `routeros/*` set produced by `src/core/routeros-errors.ts` and the
65
- `target/*` MAC-resolution pages; the rest are generated stubs to be enriched as
66
+ `target/*` MAC-resolution pages and `wait/*` deadline/cancellation pages;
67
+ the rest are generated stubs to be enriched as
66
68
  each code is grounded.
67
69
 
68
70
  ## RouterOS-fault mapping (`routeros/*`)