@tikoci/quickchr 0.4.2 → 0.4.4
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.
- package/DESIGN.md +161 -37
- package/MANUAL.md +1271 -0
- package/README.md +54 -10
- package/package.json +7 -4
- package/src/cli/index.ts +211 -48
- package/src/cli/wizard.ts +42 -12
- package/src/index.ts +7 -3
- package/src/lib/cache.ts +19 -1
- package/src/lib/channels.ts +19 -4
- package/src/lib/completions.ts +6 -3
- package/src/lib/disk.ts +5 -2
- package/src/lib/forward-spec.ts +140 -3
- package/src/lib/paths.ts +13 -0
- package/src/lib/provision.ts +279 -59
- package/src/lib/qemu.ts +52 -22
- package/src/lib/quickchr.ts +237 -48
- package/src/lib/secrets.ts +3 -7
- package/src/lib/settings.ts +373 -0
- package/src/lib/state.ts +42 -1
- package/src/lib/types.ts +237 -30
package/DESIGN.md
CHANGED
|
@@ -44,7 +44,7 @@ Modules (src/lib/) ← qemu, images, versions, network, state, ...
|
|
|
44
44
|
- **Not applied to `_launchExisting`** (the restart-existing-machine path) — that path also spawns + `waitForBoot`s but has never been observed to flake. Extend the same respawn there only with evidence.
|
|
45
45
|
- **Timeout factor left at 1.5×** (`accelTimeoutFactor`, `src/lib/platform.ts`) rather than inflated further — the respawn is the recovery mechanism, not a bigger ceiling.
|
|
46
46
|
|
|
47
|
-
How to tell it's firing: grep CI `qemu.log`/run logs for `respawning QEMU once`. A frequent occurrence, or a `BOOT_TIMEOUT` that *survives* the respawn (or appears on the `_launchExisting` path), is the signal to stop treating it as a flake and find the real cause. Tracked
|
|
47
|
+
How to tell it's firing: grep CI `qemu.log`/run logs for `respawning QEMU once`. A frequent occurrence, or a `BOOT_TIMEOUT` that *survives* the respawn (or appears on the `_launchExisting` path), is the signal to stop treating it as a flake and find the real cause. Tracked as a watch-item in [#45](https://github.com/tikoci/quickchr/issues/45).
|
|
48
48
|
|
|
49
49
|
9. **Resilient downloads: normal `fetch` first, public-DNS IPv4 as a failback** — All fetches to MikroTik's `upgrade`/`download` hosts go through `fetchResilient()` (`src/lib/net.ts`), never bare `fetch()`. The intent is narrow: a **plain `fetch` is the path** — dual-stack (happy eyeballs), on par with curl/most tools, and honoring local DNS (`/etc/hosts` pins, VPN/split-horizon, mirror redirects, IPv6-only egress) — and we layer one **failback** beneath it to ride out *DNS misconfiguration somewhere in the environment*, wherever it comes from. We deliberately do **not** invert this (public-DNS+IPv4 first) — that would override local DNS on every machine and could introduce its own failures (e.g. an IPv4-only connect on an IPv6-only network). Normal first keeps us on par with most tools; the failback only adds smarts when the normal path actually breaks.
|
|
50
50
|
|
|
@@ -66,6 +66,17 @@ Modules (src/lib/) ← qemu, images, versions, network, state, ...
|
|
|
66
66
|
| +5 | WinBox | 8291 |
|
|
67
67
|
| +6—9 | Custom | — |
|
|
68
68
|
|
|
69
|
+
On Windows, the monitor/serial/QGA channels use TCP localhost at `portBase+6/+7/+8` (Winsock can't bind `\\.\pipe\` paths), so the `+6..+8` "Custom" slots are effectively reserved there.
|
|
70
|
+
|
|
71
|
+
### Allocation rationale & open questions
|
|
72
|
+
|
|
73
|
+
Ports are assigned in fixed 10-port blocks from `DEFAULT_PORT_BASE` (9100), scanning existing machines and probe-binding to avoid collisions. Two rough edges are **maintainer decisions, not yet settled** — don't "fix" them unilaterally, since they change the persisted-state / port contract (tracked as `needs-decision` work):
|
|
74
|
+
|
|
75
|
+
1. **Base 9100 is a poor default** — collides with JetDirect/PDL (printers) and trains agents to memorize "quickchr = 9100 for REST", which only holds for the *first* instance (the second gets 9110). Open: random base in a clean high range (persisted per machine) vs caller-requested range vs both; and whether that's a breaking change or a setting with 9100 as the default. → [#56](https://github.com/tikoci/quickchr/issues/56)
|
|
76
|
+
2. **Fixed blocks are overfit** — extras bolt on at `+6..+9` and collide with the Windows IPC offsets above (one slot before spilling); manual `extraPorts` bypass collision checking. Open: dynamic variable-size blocks vs a fixed core pool + a separate extras pool. → [#57](https://github.com/tikoci/quickchr/issues/57)
|
|
77
|
+
|
|
78
|
+
Current consumers are all first-party tikoci projects (centrs, donny, restraml), so a port-scheme migration can be coordinated across them — but `@tikoci/quickchr` is published and public, so a change to the persisted port contract still needs an issue, a CHANGELOG/docs note, and (for the `ChrInstance`/`StartOptions` surface) a deprecation/migration path, not a silent break.
|
|
79
|
+
|
|
69
80
|
## Storage Layout
|
|
70
81
|
|
|
71
82
|
```text
|
|
@@ -78,15 +89,21 @@ Modules (src/lib/) ← qemu, images, versions, network, state, ...
|
|
|
78
89
|
│ └── 7.22.1-arm64-1/
|
|
79
90
|
│ ├── machine.json # Config + state
|
|
80
91
|
│ ├── disk.img # Working copy
|
|
81
|
-
│ ├── efi-vars.
|
|
92
|
+
│ ├── efi-vars.qcow2 # UEFI vars (arm64; qcow2 so savevm works — #31)
|
|
82
93
|
│ ├── monitor.sock # QEMU monitor
|
|
83
94
|
│ ├── serial.sock # Serial console
|
|
84
95
|
│ ├── qga.sock # QGA (x86 only)
|
|
85
96
|
│ ├── qemu.pid # PID file
|
|
86
97
|
│ └── qemu.log # Output log
|
|
87
|
-
└── config.json # Global config
|
|
88
98
|
```
|
|
89
99
|
|
|
100
|
+
**Global settings** live separately, under the XDG **config** tier (not the data tree
|
|
101
|
+
above): `~/.config/quickchr/quickchr.env`, dotenv-style (`QUICKCHR_KEY=value` lines).
|
|
102
|
+
Managed via `quickchr settings get|set|print|reset`. Precedence per key: CLI flag >
|
|
103
|
+
`QUICKCHR_<KEY>` env var > `quickchr.env` > built-in default. The 5 managed keys:
|
|
104
|
+
`default-channel`, `default-arch`, `cache-max-size`, `timeout-extra`, `secure-login`.
|
|
105
|
+
See MANUAL.md's CLI reference and environment-variables sections for the full surface.
|
|
106
|
+
|
|
90
107
|
## Platform Support
|
|
91
108
|
|
|
92
109
|
| Platform | x86 CHR | arm64 CHR | Notes |
|
|
@@ -105,31 +122,63 @@ Modules (src/lib/) ← qemu, images, versions, network, state, ...
|
|
|
105
122
|
|
|
106
123
|
## CI System
|
|
107
124
|
|
|
108
|
-
**Workflows**: `.github/workflows/{ci,
|
|
109
|
-
failure-diagnosis guide live in
|
|
110
|
-
the high-level rationale only.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
125
|
+
**Workflows**: `.github/workflows/{ci,main,sweep,integration,release,ros-versions,lint-powershell}.yml`.
|
|
126
|
+
Full artifact map, dispatch recipes, and failure-diagnosis guide live in
|
|
127
|
+
`.github/instructions/ci.instructions.md` — this section is the high-level rationale only.
|
|
128
|
+
|
|
129
|
+
**The 2026-07 refactor (#29)** replaced the organically-grown scheme (integration on every
|
|
130
|
+
PR push, three parallel copies of the runner logic, `continue-on-error` green-washing)
|
|
131
|
+
with a layered design:
|
|
132
|
+
|
|
133
|
+
- **`ci.yml`** — fast PR/push gate (~3-5 min, no QEMU): lint ∥ unit+coverage ∥ Windows
|
|
134
|
+
unit ∥ `Integration freshness` (PR-only, required). Integration left the PR path
|
|
135
|
+
because a 25-minute CHR suite per PR push bought little signal per minute; the quality
|
|
136
|
+
bar moved to main.
|
|
137
|
+
- **`main.yml`** — full integration suite + examples smoke (+ PowerShell lint) on
|
|
138
|
+
linux/x86_64 + linux/aarch64 (KVM when available; hosted arm64 may fall back to TCG)
|
|
139
|
+
on every push to `main`; examples are part of the
|
|
140
|
+
default flow, not a weekly extra. The **freshness gate** (`scripts/ci-freshness.ts`) makes this honest:
|
|
141
|
+
PRs merge only while the latest completed main run is green, so a red main visibly
|
|
142
|
+
blocks everything instead of rotting in the Actions list. Superseded push runs cancel;
|
|
143
|
+
cancelled runs carry no signal (the gate skips them).
|
|
144
|
+
- **`sweep.yml`** — weekly all-platform sweep (+ examples smoke). A separate file from
|
|
145
|
+
main.yml *by design*: its TCG legs (macos-x86, windows-x86 — bounded to the anchor
|
|
146
|
+
smoke subset via `tcg-smoke: true` to cap weekly cost) may red without blocking PRs,
|
|
147
|
+
but are never green-washed — red is red. On manual `integration.yml` dispatches TCG
|
|
148
|
+
legs run the FULL suite by default (`tcg-smoke` defaults off): `platforms=all` means
|
|
149
|
+
all platforms, full set — smoke is opt-in, never an implicit narrowing.
|
|
150
|
+
- **`integration.yml`** — the single reusable integration unit (`workflow_call` +
|
|
151
|
+
`workflow_dispatch`). A plan job resolves **`platforms` × `routeros-targets`** (both
|
|
152
|
+
comma lists; `gating`/`all` platform aliases) into one cross-OS matrix — one dispatch
|
|
153
|
+
can cover e.g. gating platforms × three RouterOS versions, no wrapper jobs or repeat
|
|
154
|
+
dispatches. `test-filter` narrows; `run-examples` defaults ON. Each runner boots its
|
|
155
|
+
native CHR arch — `detectAccel()` picks KVM/HVF/TCG. Agents dispatch this to ground
|
|
156
|
+
platform hypotheses without waiting for a PR cycle.
|
|
157
|
+
- **`release.yml`** — one-click release: freshness gate (no suite re-run — main is kept
|
|
158
|
+
continuously release-able) + non-empty CHANGELOG `[Unreleased]` →
|
|
159
|
+
`scripts/release-prep.ts` bump/rollover → tag + GitHub Release + `npm publish
|
|
160
|
+
--provenance` (odd minor → `next`, even → `latest`).
|
|
161
|
+
- **`ros-versions.yml`** — daily: new RouterOS versions (per channel) with no linux-x86
|
|
162
|
+
record in `ci-data/tested-versions.json` ride the `routeros-targets` matrix of a
|
|
163
|
+
single integration dispatch.
|
|
164
|
+
|
|
165
|
+
**Metrics as byproduct (#30)**: the library appends every successful boot to
|
|
166
|
+
`<dataDir>/boot-log.ndjson` (and stamps `lastAccel`/`lastBootMs` into machine.json);
|
|
167
|
+
integration jobs assemble that + per-file timing into `metrics.ndjson`
|
|
168
|
+
(`scripts/ci-metrics.ts`); collect-metrics callers push per-run files + the
|
|
169
|
+
`tested-versions.json` rollup to the orphan **`ci-data`** branch. Never a second test
|
|
170
|
+
run; never affects pass/fail — the aggregate job is best-effort by contract (fold/push
|
|
171
|
+
failures warn and stay green, because a red there would trip the PR freshness gate over
|
|
172
|
+
side-band data). A full run marks exactly **its target's resolved version** tested —
|
|
173
|
+
never every version it happened to boot (upgrade/pinned-channel tests boot others
|
|
174
|
+
incidentally; crediting those suppressed the scheduler for versions no full suite ever
|
|
175
|
+
targeted). The per-run files are the source of truth; `ci-metrics refold` rebuilds the
|
|
176
|
+
rollup after fold-logic changes.
|
|
177
|
+
|
|
178
|
+
**Merge policy**: squash-only, PR title → main commit subject (write PR titles as
|
|
179
|
+
conventional commits), PR body → commit body, branches auto-delete. Review threads must
|
|
180
|
+
be resolved before merge (`required_conversation_resolution`) and automated reviews must
|
|
181
|
+
have actually posted — see CONTRIBUTING.md "Pull Requests & Merging".
|
|
133
182
|
|
|
134
183
|
**Coverage**: `unit-tests` parses `bun test --coverage` output and compares against thresholds
|
|
135
184
|
(default 75% functions, 60% lines). Failures emit `::warning::` annotations but do NOT block
|
|
@@ -138,13 +187,14 @@ merges (`continue-on-error: true`). Thresholds are overridable via dispatch inpu
|
|
|
138
187
|
|
|
139
188
|
**Artifacts**:
|
|
140
189
|
- `coverage-report` — full per-file coverage table (14 days)
|
|
141
|
-
- `integration-logs
|
|
190
|
+
- `{integration|main|sweep}-logs-<platform>` — bun test output + timing + metrics.ndjson +
|
|
191
|
+
machine.json + qemu.log (7 days); durable timing lives on the `ci-data` branch
|
|
142
192
|
|
|
143
193
|
## Design Principles
|
|
144
194
|
|
|
145
195
|
### Scope Boundary — QEMU Expert, Not Orchestrator
|
|
146
196
|
|
|
147
|
-
quickchr manages individual CHR instances. Multi-router topologies, test matrices, and workflow orchestration are **out of scope** for the CLI and library. Provide `examples/`
|
|
197
|
+
quickchr manages individual CHR instances. Multi-router topologies, test matrices, and workflow orchestration are **out of scope** for the CLI and library. Provide `examples/` as runnable scripts (a `bun run`-able `<name>.ts` plus `.sh`/`.ps1`/`.py` siblings; `grounding/` is the one `bun:test` reference) to inspire, but don't build a framework. The example convention lives in `.github/instructions/examples.instructions.md`; Python examples prefer `uv run` over a venv. Users (and their AI agents) compose quickchr instances into whatever topology they need — we give them reliable building blocks.
|
|
148
198
|
|
|
149
199
|
### Networking — Discover, Don't Configure
|
|
150
200
|
|
|
@@ -183,6 +233,32 @@ multicast port; two CHRs on one group don't discover each other and host capture
|
|
|
183
233
|
gets nothing. mcast still works on Linux/CI. Prefer `socket-connect` for host
|
|
184
234
|
capture on any platform. Evidence: `test/lab/mndp/REPORT.md`.
|
|
185
235
|
|
|
236
|
+
#### Guest→Host UDP via the Gateway — No Forward (discovered 2026-06-25)
|
|
237
|
+
|
|
238
|
+
The dual of `hostfwd`: SLIRP's gateway `10.0.2.2` *is* the host from inside the
|
|
239
|
+
guest, so guest-originated UDP to `10.0.2.2:<port>` reaches a host socket bound on
|
|
240
|
+
loopback `<port>` with **no forward and no extra NIC**. This generalizes the TZSP
|
|
241
|
+
path (`tzspGatewayIp`/`captureInterface`) — it is not TZSP-specific and reaches an
|
|
242
|
+
ordinary bound socket, not just a `tshark`/pcap capture. The host socket **must be
|
|
243
|
+
unconnected**: SLIRP re-emits from a rewritten loopback source
|
|
244
|
+
(`127.0.0.1:<ephemeral>`), which a `connect()`-ed socket would filter. This closed
|
|
245
|
+
centrs' btest UDP-coverage gap (issue #18) with no new quickchr feature — it was a
|
|
246
|
+
discoverability gap. Recipe: `docs/networking-recipes.md`; evidence:
|
|
247
|
+
`test/lab/gateway-udp/REPORT.md`.
|
|
248
|
+
|
|
249
|
+
#### Port-Range Forwards — Explicit-Host-Only
|
|
250
|
+
|
|
251
|
+
`--forward`/`extraPorts` accept a range (`name:hostStart-hostEnd[:guest…][/proto]`)
|
|
252
|
+
that expands to one `PortMapping`/`hostfwd` per port — QEMU has no native range.
|
|
253
|
+
**Design choice:** range host ports must be *explicit*. Auto-allocation draws from
|
|
254
|
+
the per-instance 10-port block, which cannot guarantee a contiguous run; requiring
|
|
255
|
+
explicit host ports keeps the change additive (the existing
|
|
256
|
+
`validateExplicitExtraPorts` collision check covers them) and avoids reworking the
|
|
257
|
+
port-block contract. A 64-port cap bounds the generated `hostfwd` string. For
|
|
258
|
+
guest-chosen *unpredictable* ports, the gateway path (above) is the better fit for
|
|
259
|
+
the guest→host direction. `expandForwardSpec` is the range-aware entry point;
|
|
260
|
+
`parseForwardSpec` stays single-port for backward compatibility.
|
|
261
|
+
|
|
186
262
|
### Platform Priority
|
|
187
263
|
|
|
188
264
|
macOS → Linux → Windows. Mac and Linux share most code paths with minor `#ifdef`-style branches. Windows is tracked but lower priority — larger RouterOS admin audience there, but fewer recipes and harder to test. Windows CI runner planned after the existing macOS/Linux matrix is stable.
|
|
@@ -191,6 +267,13 @@ macOS → Linux → Windows. Mac and Linux share most code paths with minor `#if
|
|
|
191
267
|
|
|
192
268
|
Tighten before expanding. New subcommands (`logs`, `exec`, `console`) wait for a full command tree review. The CLI should be discoverable without paging `--help` — shell completions help more than a long command list. Use multipass and virsh as reference points for symmetry, not to copy.
|
|
193
269
|
|
|
270
|
+
**Command-surface principles** (locked — these shape every new subcommand):
|
|
271
|
+
|
|
272
|
+
- **Interactive prompts are confined to `setup`.** Every other command is non-interactive — no selectors. Without a `<name>` argument, print the list + a tip; don't prompt.
|
|
273
|
+
- **`start`/`stop` are pure operations** — no wizard, no creation. `add` creates; `setup` is the wizard.
|
|
274
|
+
- **`set`/`get` are for machine config, not re-provisioning.** After first provisioning, don't add commands that re-provision — the surface grows and each post-provision capability needs its own RouterOS edge-case testing. A drifted machine is recreated, not mutated (see *Out of Scope*).
|
|
275
|
+
- **`--json` on read commands only** — same content as console output (richer metadata OK), pipe-friendly for `jq`. No `--yaml`/`--serialize`/TSV/CSV; callers pipe `--json` through `jq`/`yq`. For `exec`, `--json` wraps the quickchr response — the RouterOS result stays a string (use `:serialize` in-script for structured RouterOS output).
|
|
276
|
+
|
|
194
277
|
### RouterOS Verification
|
|
195
278
|
|
|
196
279
|
Always read back what we write. One extra REST API call after a provisioning action (license, user, package) catches version-specific command drift early. Surface errors with actionable hints rather than silent failures.
|
|
@@ -220,13 +303,54 @@ Output formatting via `--json` flag. RouterOS trick for structured output: wrap
|
|
|
220
303
|
|
|
221
304
|
### Examples Philosophy
|
|
222
305
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
306
|
+
`examples/` is **load-bearing agent-onboarding surface** — agents (and humans) open
|
|
307
|
+
it before `src/lib/`, so a *wrong* example is worse than none: it teaches the wrong
|
|
308
|
+
lesson and costs a later code-review to unwind. Examples are therefore held to the
|
|
309
|
+
same bar as the code, and a broken one **gates** extended verification.
|
|
310
|
+
|
|
311
|
+
Each `examples/<name>/` is a **runnable artifact that does something real** against a
|
|
312
|
+
CHR, in the canonical shape (full rules in
|
|
313
|
+
[`.github/instructions/examples.instructions.md`](.github/instructions/examples.instructions.md)):
|
|
314
|
+
|
|
315
|
+
- **`<name>.ts`** — the **primary**: a `bun run`-able script using the library API,
|
|
316
|
+
with `runExample()` guaranteeing teardown (success *or* failure). Not a test.
|
|
317
|
+
- **`<name>.sh` / `<name>.ps1`** — the **CLI** mirror (POSIX `sh` + PowerShell),
|
|
318
|
+
sourcing `examples/common.{sh,ps1}`. New examples ship both; existing ones add
|
|
319
|
+
`.ps1` where the CLI flow is simple.
|
|
320
|
+
- **`<name>.py`** — optional CLI driver for a non-TS audience, run with `uv run`.
|
|
321
|
+
- **`grounding/` is the one `bun:test` example** — kept as a test on purpose, because
|
|
322
|
+
there the *assertions are the documentation* (it's the "how to write CHR
|
|
323
|
+
integration tests" reference). Everywhere else, an agent can wrap a script in
|
|
324
|
+
`test()` trivially, so a runnable script is the better teaching surface.
|
|
325
|
+
|
|
326
|
+
Makefiles were the old convention and are now **disallowed** in `examples/`
|
|
327
|
+
(`scripts/validate-examples.ts` enforces this) — they mixed CLI orchestration with
|
|
328
|
+
raw `scp`/`curl`/`ssh`, the opposite of "one example teaches one capability."
|
|
329
|
+
|
|
330
|
+
Building examples early is a form of "anchor testing" for the CLI surface — it finds
|
|
331
|
+
ergonomic issues before we commit to new commands, and each gap surfaced gets a
|
|
332
|
+
"friction found" note in the example's README plus a GitHub issue rather than
|
|
333
|
+
being papered over (this pass surfaced the missing `quickchr cp`, #23). Coverage of
|
|
334
|
+
the CLI/library surface is tracked in
|
|
335
|
+
[`examples/COVERAGE.md`](examples/COVERAGE.md).
|
|
336
|
+
|
|
337
|
+
### Agent-Friendliness — Discoverability Over Features
|
|
338
|
+
|
|
339
|
+
A recurring lesson from downstream agents (donny, centrs, restraml): the friction is usually **finding** an existing capability, not a missing one. Issue #18 (centrs) nearly rebuilt UDP forwarding, `socket-connect` L2, and the guest→host gateway — all of which already existed — because the only way to choose among them was reading `src/lib/network.ts`. Rule: **every CLI-documented capability also needs a library-facing, by-goal surface** — JSDoc at the call site, a by-goal recipe (`docs/networking-recipes.md`), and coverage in the `routeros-quickchr` skill — or agents won't find it. Connection handoff for harnesses goes through `ChrInstance.descriptor()` / `quickchr inspect` / `quickchr env` (credential-bearing by design), never by reading `machine.json`. The structured, versioned **descriptor v1** contract that centrs consumes for `--quickchr` targets — its shape, per-service semantics, scope boundaries, and staged implementation plan — is specified in [`docs/centrs-interface.md`](./docs/centrs-interface.md) (issue [#71](https://github.com/tikoci/quickchr/issues/71)).
|
|
340
|
+
|
|
341
|
+
### Out of Scope (decided)
|
|
342
|
+
|
|
343
|
+
Explicitly rejected, with rationale, so they aren't re-proposed:
|
|
344
|
+
|
|
345
|
+
- **Cloud deployment** — `tikoci/chr-armed` already does OCI + AWS; revisit only once local CHR is solid and provisioning/image layers can be reused.
|
|
346
|
+
- **Multi-CHR orchestration** — building blocks + `examples/`, not a framework (see *Scope Boundary*). Users/agents compose topologies.
|
|
347
|
+
- **`quickchr upgrade <name>` / post-provisioning mutation** — replaced by a future *config audit/verify* report (flags drift; user recreates). Re-provisioning has too many failure modes (version bumps, rotated creds, changed deps).
|
|
348
|
+
- **Packaging (Homebrew/Deb)** and **service management (launchd/systemd)** — lower priority than core; optional later.
|
|
349
|
+
- **Machine templates** — CLI flags are the template, API objects are reusable, the wizard always prompts. No separate template system.
|
|
350
|
+
- **`--no-ansi` flag** — ANSI is fine in text output (`grep`/`jq` cope); the real discipline (separate presentation from content) belongs in a centralized error-message surface instead.
|
|
351
|
+
- **`machine.json` → YAML** — staying JSON (pretty-printed) for `jq` users; YAML adds complexity without a matching benefit.
|
|
352
|
+
- **Separate multi-version/arch matrix runner** — the CI matrix + `examples/version-matrix` cover it.
|
|
229
353
|
|
|
230
354
|
### Document Maintenance
|
|
231
355
|
|
|
232
|
-
DESIGN.md
|
|
356
|
+
DESIGN.md is the living home for design decisions and rationale. At the end of any significant work session, agents should review whether new implementation details, design decisions, or discovered constraints belong here. Open/close work in **GitHub Issues** — not BACKLOG.md (see CONTRIBUTING.md "Tracking work"); record grounded RouterOS/QEMU behaviour facts in the narrowest scoped doc (`.github/instructions/*.md`, `docs/`, or `test/lab/<topic>/REPORT.md`); add a CHANGELOG.md entry for user-facing changes. Treat this as a lightweight checklist, not a gate.
|