Skip to content

chore: back-merge main → develop (reset promote ancestry after v0.6.0)#161

Merged
saadqbal merged 11 commits into
developfrom
chore/back-merge-main-to-develop-2026-07-07
Jul 7, 2026
Merged

chore: back-merge main → develop (reset promote ancestry after v0.6.0)#161
saadqbal merged 11 commits into
developfrom
chore/back-merge-main-to-develop-2026-07-07

Conversation

@saadqbal

@saadqbal saadqbal commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Zero file changes — ancestry only. Merges main into develop so develop's history includes the promote-merge commits (#148, #155). Without this, those commits stay off develop and the next develop → main promote conflicts on release drift again (exactly what #148 hit → had to re-cut as #155).

Trees are already identical (main @ v0.6.0 == develop), so this touches no files; it only realigns ancestry so future promotes fast-forward cleanly.

Durable fix for the recurring drift flagged during the v0.6.0 release. Recommend adopting "back-merge main→develop after every promote" as the release runbook's last step.


Note

Low Risk
Zero file diff — only Git ancestry; no runtime, security, or application behavior impact.

Overview
No code or config changes — this PR only updates Git history by merging main into develop after the v0.6.0 promote.

main and develop already match at v0.6.0; the merge pulls promote commits (e.g. #148, #155) onto develop so they are not missing from that branch. That avoids the recurring release drift problem where the next promote conflicts because ancestry diverged.

The intent is to make back-merge maindevelop after every promote the last step in the release runbook.

Reviewed by Cursor Bugbot for commit 9ffa8b8. Bugbot is set up for automated code reviews on this repo. Configure here.

aptracebloc and others added 11 commits June 3, 2026 12:03
Sync develop → main for v0.1.0-alpha
* feat(#26): installer-style terminal UX package (internal/ui) (#32)

* feat(#26): add internal/ui installer-style terminal UX package

Printer renders colored step/✔/⚠/· output matching the tracebloc/client installer (scripts/lib/common.sh). TTY+NO_COLOR auto-detect with a WithColor override (functional-options); fatih/color forced per-instance so it's testable on a buffer. Helpers: Banner, Step, Successf, Warnf, Infof, Errorf, Hintf, PromptHeader.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(#26): render dataset push pre-flight via internal/ui + add --plain

Adds a persistent --plain root flag and a printerFor(cmd) helper, plus ui.Section/ui.Field. dataset push pre-flight now renders through the ui.Printer (colored Section/Field rows, ⚠ for the RWO warning) instead of ad-hoc fmt.Fprintf; behavior unchanged (the pinned RendersKeyFacts test still passes).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(#26): render cluster info via internal/ui

cluster info now prints through the ui.Printer (Section/Field + a green ✔ ready line) instead of raw fmt.Fprintf, matching the dataset push pre-flight. Test updated for the *ui.Printer signature (still asserts exit-3 on a bad kubeconfig).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(#27): installer-style ingestion summary via internal/ui (#33)

Replaces the box-drawing RenderPanel with RenderSummary(p *ui.Printer, s): an outcome-colored headline (green clean / yellow skips / red failures) + Section/Field counts + a 'what's next' block. Dropping the Unicode box resolves the v0.2 plain-ASCII TODO. submit.Options gains Printer (nil → ui.New(Out)); dataset push threads its --plain-aware printer through. Parser untouched. RenderPanel tests → RenderSummary (basic shape, nil, table-driven outcome).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(#28): interactive guided dataset push — engine + core prompts (PR-a) (#34)

On a TTY (unless --no-input), dataset push now prompts for any missing core inputs (path, category, table, intent, label) before validation; flags already passed win. Introduces a prompter interface (survey-backed in prod, fake in tests) so the prompt-mapping is unit-testable without a pseudo-terminal. Args ExactArgs(1)->MaximumNArgs(1) so a bare command can prompt for the path; explicit path-required error otherwise. MLM skips the label prompt; the table prompt reuses push.ValidateTableName. PR-b adds category-specific prompts + a confirm screen.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(#28): category-specific prompts + confirm screen (PR-b) (#35)

Completes the guided dataset push. After the core prompts, asks only what each category needs: image resolution (blank=auto-detect), required keypoint count, tabular schema (blank=infer) + regression label-policy + time-column. Then a Review screen + 'Proceed?' confirm — shown only when something was actually prompted, so full-flag pushes aren't nagged. Declining or Ctrl-C (survey terminal.InterruptErr, translated to errInteractiveCancelled at the prompter boundary) exits cleanly (0). Prompter interface gains Confirm; fake-prompter tests cover keypoint, tabular-regression, and cancel.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(#29): branded home screen + cluster info banner (PR-a) (#36)

Bare 'tracebloc' (no subcommand) now renders a branded home screen — ui.Banner + a 'get started' command list — via root.RunE, instead of cobra's raw usage dump; subcommands and --help are unaffected. cluster info gains a Banner header. PR-a of #29; --output-json follows in PR-b (it needs runDatasetPush to return a result struct + JSON tags on Summary, so it's its own change).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(#29): --output-json for dataset push (PR-b) (#38)

* feat(#29): --output-json for dataset push (PR-b)

--output-json emits a machine-readable JSON result on stdout and routes all human output (preflight, logs, summary) to stderr, so 'push ... --output-json | jq' is clean. Implies non-interactive. JSON is emitted at the dry-run stop (status=dry-run) and after submit (status=succeeded/failed/detached/unknown + summary counts). The wire shape (pushJSONResult/pushJSONSummary) lives in the CLI layer so submit.Summary stays json-tag-free. Adds printerForWriter for the stderr routing; unit-tested via TestWritePushJSON.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(#29): align --output-json status with exit code + emit JSON on errors

classifyPushOutcome maps submit.Run's result to a (status, exitError) pair in lockstep: JobOutcomeSucceeded + summary.HasFailures() now yields status "completed_with_failures" + exit 9 (was "succeeded" while exiting 9). --output-json now emits exactly one result object on EVERY path — including auth/submit/watch errors (with job namespace+name when available) — instead of nothing on error. Adds TestClassifyPushOutcome. Addresses Bugbot on #38.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(#37): expand ~ in dataset path (interactive prompt + literal arg) (#41)

A leading ~ / ~/… in the dataset path now resolves under $HOME. The shell only expands an *unquoted* ~ on the command line; a path typed at the interactive prompt (#28) or a quoted/literal ~ positional arg reaches the CLI literally, and filepath.Abs would just prepend the CWD (.../cwd/~/...). Adds expandHome (mirrors cluster.expandPath) and applies it in runDatasetPush right after a.LocalPath is resolved — before any push.Discover* call — so both entry points are covered in one place. Relative/absolute/empty paths pass through untouched.

Deferred the optional in-prompt existence validator: it would couple the prompt-mapping unit tests to real directories on disk. Filed nothing — noted in the PR.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(#30): dataset rm — in-cluster teardown (table + PVC) (#40)

Adds 'tracebloc dataset rm <table>': the in-cluster teardown of a pushed dataset, packaging the manual kubectl-exec cleanup. Discovers the cluster (parent release + shared PVC), shows the teardown plan, confirms (reuses the #28 prompter; --yes to skip, --dry-run to preview), then DROPs the MySQL table (exec into the mysql pod, using its own $MYSQL_ROOT_PASSWORD so no credential transits the CLI) and rm -rf's the PVC dirs (exec into the jobs-manager pod). Exit codes mirror push (2 bad name, 3 kubeconfig/refused, 4 no release/PVC, 7 teardown failed).

Does NOT remove the central backend catalog entry — the CLI has no direct line to that backend; tracked as the cross-repo follow-up #39. The exec-into-pods mechanism (CLI-direct) vs a server-side jobs-manager delete endpoint is the open architecture question — see the DESIGN NOTE on push.Teardown. Hence: draft, pending team-lead review.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci(release): multi-arch v0.1.0 — add linux/386, linux/arm, windows/arm64 (#43)

Widen the release build matrix from 5 to 8 targets for the first stable
v0.1.0: add linux/386, linux/arm (GOARM=6, covers armv6 + armv7), and
windows/arm64. macOS amd64/arm64 and linux/windows amd64/arm64 unchanged.

- install.sh detect_arch: learn i386|i686 -> 386 and
  armv6l|armv7l|armv8l|armhf -> arm (else auto-install breaks on them).
- install.ps1 Get-Arch: return arm64 (was a hard fail) and prefer
  PROCESSOR_ARCHITEW6432 so x64-emulated PowerShell on a Windows-ARM host
  still picks the native arm64 build.
- README + RELEASE_CHECKLIST: reflect the 8-platform stable release; the
  releases/latest/download/install.sh one-liner now resolves (a plain
  v0.1.0 tag is non-prerelease per release.yml's gate, so it is `latest`).

Verified: all 8 targets cross-compile (CGO_ENABLED=0) to the right arch;
release.yml YAML + install.ps1 parse clean. No Go source changed.

Refs #42

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* feat(#31): friendlier dataset push — prose intro, guided steps, per-prompt examples (#44)

* feat(#31): friendlier dataset push — prose intro + guided 4-step flow

Replaces the terse bullet header with a plain-English explainer (grounded in docs.tracebloc.io: 'your workspace', 'the cluster your workspace was installed on', 'your data stays on that cluster'), via a new ui.Printer.Para for prose. Restructures the push into a clearly-numbered, narrated 4-step flow — Check your dataset / Connect to your cluster / Stage your files / Run the ingestion — each with a one-line how/why for medium-technical users (steps 1-2 visible in --dry-run, 3-4 live). Splits printPushPreflight into printLocalSummary + printClusterSummary so each sits under its step; the dry-run stop now names the live-only steps it skipped.

dataset rm: drop the backend-catalog clause from the runtime warning (now just 'Destructive and cannot be undone.'); --help NOTE + #39 tracking unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(#31): per-prompt guidance + examples in interactive push

Each interactive input prompt now shows a visible hint with an example above it — previously this guidance was hidden behind survey's '?' key. Covers the core prompts (path, table, intent, label column) and the category-specific ones (keypoint count, image resolution, tabular schema, regression label-policy, time column). Rendered via a new ui.PromptHint: a leading blank line for separation + cyan text, so per-field guidance stands out and reads distinctly from the dim generic hints (e.g. the 'Press Enter' meta-line). Verified by TestRunInteractive_ShowsExampleHints, which drives runInteractive with a buffer-backed Printer — no TTY needed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci(build): compile-check all 8 release targets in build.yml (#46)

release.yml builds 8 targets but build.yml (PR CI) only compiled 5, so a
break in linux/386, linux/arm, or windows/arm64 wouldn't surface until a
release tag. Bring build.yml's matrix in lock-step: add linux/386,
linux/arm (GOARM=6), windows/arm64. Compile-only (the native linux/amd64
smoke step is unchanged).

Closes #45

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* feat(#31): add dataset rm to home screen + in-run guidance (#47)

The bare-'tracebloc' home screen now lists 'dataset rm' alongside push/cluster-info/ingest-validate. The rm command itself gains the same guidance treatment as push: a plain-English Para intro after the banner (what it removes — table + files — and that it can't be undone), and a cyan PromptHint above the confirmation. Home-screen test asserts 'dataset rm' appears.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: prep README for v0.2.0 (#50)

- Status → v0.2.0 (latest stable; builds on v0.1.0 with the friendlier
  guided dataset push + dataset rm on the home screen).
- Reconcile the Roadmap: "Next (v0.2)" framed v0.2 as the cloud-sources
  milestone, but v0.2.0 ships UX polish — so cloud sources / segmentation
  / `list` move to a label-free "Next", and `rm` drops from the future
  verbs (it shipped in v0.1.0).
- Phase-5 table link pinned to the v0.1.0 tag (text said v0.1.0 but
  linked to /latest, which now resolves to v0.2.0).

Refs #48

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* fix: address Bugbot review on #49 (#51)

- dataset push --output-json: emit a JSON error object on early-failure paths (validation, discovery, staging, token, port-forward), not just on dry-run/post-submit. runDatasetPush now uses a named return + a jsonEmitted flag + a defer, so '… --output-json | jq' always gets JSON instead of empty stdout on a failure. Adds Error/ExitCode to the result shape + writePushErrorJSON; covered by TestRunDatasetPush_OutputJSONEarlyFailureEmitsJSON.

- dataset rm: on a partial teardown (DROP TABLE succeeded but the PVC file removal failed), the error now says so and points the user to re-run — both teardown ops are idempotent (DROP TABLE IF EXISTS / rm -rf), so a re-run completes cleanup rather than reporting a flat 'teardown failed'.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Asad Iqbal (Saadi) <asad.dsoft@gmail.com>
Sync develop → main — prepare v0.3.0
docs: sync v0.3.0 README to main
…#64)

* fix(install.sh): persist PATH to the shell rc (parity with install.ps1) (#61)

* fix(install.sh): persist PATH to the shell rc (parity with install.ps1)

When the binary lands in the ~/.local/bin fallback (the normal unprivileged
`curl | sh` case), the installer now appends the PATH entry to the rc file the
user's shell actually reads — instead of only printing advice.

Fixes the recurring "CLI still not in PATH after restarting the shell" ticket.
install.ps1 already persists user PATH on Windows; this brings Unix to parity.
The print-only behavior silently failed on Ubuntu: ~/.profile adds ~/.local/bin
only at login and only if it already existed, but the installer creates it
mid-session, so a new (non-login) terminal reading ~/.bashrc never saw it.

- Detects the rc per shell + OS: zsh -> .zshrc, bash -> .bashrc (Linux) /
  .bash_profile (macOS login shell), fish -> config.fish, else .profile.
- Idempotent (skips if the rc already references the dir); falls back to
  printing the line if the rc isn't writable.

Validated: sh -n + bash -n; functional + idempotency + per-shell routing tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: add install troubleshooting guide + harden rc-write fallback

Completes the install-PATH work tracked in #60. The installer change
already on this branch fixes the Ubuntu "not on PATH after restart"
report; this adds the troubleshooting doc + README link that #60 also
asked for, plus a small hardening of the rc-write fallback.

- docs/troubleshooting.md: command-not-found / PATH guide covering the
  ~/.local/bin fallback, login vs non-login shells (Linux + macOS), the
  /usr/local/bin alternative, the Windows new-window case, and SSH login
  shells.
- README: link the guide from the install section.
- install.sh: group the rc append as `{ …; } 2>/dev/null` so a read-only
  or unwritable rc no longer leaks a raw "Permission denied" line before
  the clean fallback message. Verified leak-free under dash + bash; all
  shell/OS routing and idempotency unchanged.

Refs #60.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Asad Iqbal <asad.dsoft@gmail.com>

* feat(push): infer entirely-empty columns as nullable FLOAT + warn (#66)

When InferSchema saw a column with no non-empty value in the sample it fell
back to VARCHAR(255). An entirely-empty column then fails ingest: the
data-ingestors string validator rejects the all-NULL column ("N non-string
values"). Sparse panels (e.g. a proteomics matrix where an analyte is
measured in zero samples) hit this on every empty column.

Type such columns as a nullable FLOAT instead — the FLOAT validator accepts
NULLs, so an all-empty column ingests cleanly — and return them in a new
`empty` slice so `dataset push` warns which columns were empty (the user can
--schema-override if a column is really text).

Updates the renamed test + the InferSchema callers for the new return.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* fix(install.sh): tighten PATH idempotency + persist user-local PREFIX (Bugbot on #61) (#65)

* fix(install.sh): persist user-local PREFIX even when on PATH; tighten rc idempotency

Two Cursor Bugbot findings on #61's PATH-persistence block, both of which
let the installer print a false "Added ... to your PATH" while a new shell
still couldn't find the binary — the exact failure #61 set out to kill.

1. Loose idempotency: `grep -F "$PREFIX"` matched the directory anywhere in
   the rc, including a bare comment, so a stray mention set added=1 with no
   export written. Now strip comment lines, keep only real
   PATH=/fish_add_path lines, then fixed-match the prefix.

2. Session PATH skipped the rc: the outer `case ":$PATH:"` skipped rc
   persistence whenever $PREFIX was on PATH for the current shell, so a
   one-off `export` (which won't survive a new terminal) left the rc
   untouched. Branch on the install location instead — a user-local prefix
   (under $HOME) always persists; a system prefix (e.g. /usr/local/bin) is
   already on PATH for every shell and only gets a nudge if it isn't.

Validated: sh -n / bash -n / dash -n, shellcheck clean, and a functional
matrix over the extracted block (fresh, idempotent re-run, comment-only,
unrelated PYTHONPATH= line, system-prefix nudge, session-on-PATH persist,
plus zsh/fish/macOS-bash routing) — 9/9.

Refs #61

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(install.sh): tolerate a trailing slash in $HOME for the user-local match

Bugbot on #65: `case "$PREFIX" in "$HOME"/*)` misclassifies a user-local
prefix as a system one when $HOME ends in a slash (e.g. HOME=/home/u/ with
--prefix /home/u/.local/bin -> pattern /home/u//* never matches the single-
slash path), so rc persistence is skipped. Normalize with home_dir="${HOME%/}"
(POSIX suffix strip) before matching.

Functional matrix extended with a trailing-slash-HOME case — 10/10; sh/bash/
dash -n + shellcheck clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(install.sh): accurate present/added/failed messaging; recognize PATH+= and zsh path+=()

Two more Bugbot findings on #65:

- The user-local branch's failure message claimed "$PREFIX is not on $PATH",
  which is wrong now that the branch runs regardless of session PATH (a one-off
  export could already include it). Replaced the added/else pair with explicit
  present/added/failed states so each message is accurate; the failed case no
  longer asserts PATH state.

- The idempotency filter only matched PATH=/fish_add_path, missing the PATH+=
  append idiom (and zsh's path+=() array), so a manual entry of that form wasn't
  recognized and a re-run could append a duplicate block. Broadened to
  case-insensitive (^|[^A-Za-z_])path[+]?= (still excludes PYTHONPATH=/MYPATH=).

Functional matrix now 13/13 (adds PATH+=, zsh path+=(), persist-failure message);
sh/bash/dash -n + shellcheck clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(install.sh): persist any off-PATH prefix to rc, not just $HOME-local ones

Bugbot on #65: keying rc persistence on `case "$PREFIX" in "$home_dir"/*)`
dropped writable non-$HOME targets (e.g. `--prefix /opt/tracebloc`) into the
system branch, which only nudges — so a custom off-PATH install no longer
persisted and a new terminal couldn't find tracebloc. cli#61's original code
persisted ANY off-PATH prefix.

Compute a `persist` decision instead: yes when the prefix is under $HOME
(always — survives a new terminal even if a session export already has it) or
when it's a non-$HOME prefix not on the current $PATH (e.g. /opt/...); no for a
non-$HOME prefix already on PATH (the default /usr/local/bin needs nothing). One
rc-writing block then handles every persist case.

Functional matrix 14/14 (adds off-PATH /opt prefix persists; default system
prefix on PATH stays silent); sh/bash/dash -n + shellcheck clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: lukasWuttke <54042461+LukasWodka@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
release: promote develop → main (v0.4.0 — auth + client provisioning, cluster doctor) (#106)
* Merge pull request #111 from tracebloc/sec/rfc-0001-r8-mandatory-verify

fix(security): mandatory / fail-closed cosign verification in the CLI installer (RFC-0001 R8)

* fix: validate COSIGN_VERSION and enforce TLS 1.2 on installer downloads (#114)

Bugbot (promotion PR #113), two findings in scripts/install.sh:

- Unvalidated cosign version in URL: COSIGN_VERSION (env-overridable) was
  interpolated into the Sigstore download URL without the semver/path-traversal
  gate applied to RELEASE_VERSION. Generalized validate_tag into
  validate_version_tag and apply it to COSIGN_VERSION before building the URL.
  (scripts/install.sh:45)
- Signature download curls omit TLS: the .sig/.cert fetches (and the binary,
  SHA256SUMS, and latest-redirect curls) lacked --tlsv1.2, unlike the new cosign
  bootstrap fetches. Add --tlsv1.2 to every security-sensitive download.
  (scripts/install.sh:335)

Co-authored-by: shujaat hasan <shujaathasan@shujaats-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* chore(schema): re-vendor ingest.v1.json — register seq2seq as recognize-but-unsupported (#118)

The CI "Schema drift check" (scripts/sync-schema.sh --check) was failing on
develop itself and therefore every PR — the vendored
internal/schema/ingest.v1.json had drifted from data-ingestors master, which
added the `seq2seq` task category (enum entry + an if/then requiring `texts`,
membership in the self-supervised text group, and an updated `texts`
description). The drift is purely additive — it does not change validation for
any already-supported category.

- Re-vendored the schema (sync-schema.sh) → --check now passes.
- Registered seq2seq in internal/push/category.go as recognize-but-not-yet-
  CLI-supported (CLISupported:false + UnsupportedNote), Family text: the CLI's
  discover/build for its raw-.txt / source\ttarget `texts` layout isn't
  implemented, so push reports it as pending rather than leaving a
  schema<->registry gap (the cli#74 drift class that TestRegistryCoversSchema-
  Categories pins). Updated the parity tests.

Mirrors #103, which did exactly this for causal_language_modeling. Full
seq2seq push support (discover/build, flip CLISupported) is a follow-up
feature — the sibling of cli#105.

go build / vet / test ./... green; drift check green.

Closes #117

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* feat(auth): send User-Agent version header + handle 426 Upgrade Required (cli#98) (#115)

RFC-0001 §13 / §14 R11 / Appendix C.1 — the CLI half of the
minimum-supported-CLI-version handshake (server half: backend#888, shipped
in backend#916).

- Every backend request now carries
  `User-Agent: tracebloc-cli/<ver> (<os>/<arch>)`, injected by a transport
  wrapper on the shared internal/api client so login + provisioning are both
  covered without threading the version through each command. The version is
  the ldflags build info, recorded once via api.SetUserAgent in NewRootCmd; a
  build with no version reports "dev" (unparseable server-side → fails open).
- A 426 from any endpoint is detected centrally in post/get and surfaced as a
  typed *UpgradeRequiredError carrying min_version, so every command degrades
  to the same actionable "your CLI is too old — upgrade to >= X" message and a
  clean non-zero exit (no stack trace), in both interactive and --plain modes.

Tests: UA on the wire, dev fallback, 426 → UpgradeRequiredError (GET + POST),
unparseable-426 body, message actionability.

Closes #98.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* feat(auth): logout revokes the token server-side (cli#112) (#116)

* feat(auth): logout revokes the token server-side via POST /auth/revoke (cli#112)

RFC-0001 §7.5 / R2 — the CLI half of the logout-revoke gap (server endpoint:
backend#887, shipped in backend#903). Until now `logout` was local-only: it
cleared ~/.tracebloc but a copied/leaked token kept authenticating (confirmed
in the 2026-06-25 connect-flow FR).

- New api.Client.RevokeToken → POST /auth/revoke (Bearer in, 204 out,
  idempotent); a non-2xx surfaces as *APIError.
- logout now revokes server-side before clearing local state. Best-effort by
  contract: on failure (offline / already-revoked / 5xx) it logs a hint and
  still clears local state — the user must always be able to log out locally.

Tests: RevokeToken 204→nil (Bearer + POST on the wire) and 5xx→APIError;
logout calls revoke (now routed at a stub — it was about to hit real prod)
and still clears local state when the revoke fails.

Closes #112.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(auth): address Bugbot on logout revoke — env resolution + save ordering (cli#112)

Two Medium findings on the revoke wiring:

- Resolve the revoke env consistently with authedClient via a new shared
  sessionEnv(cfg) helper (saved env → $CLIENT_ENV → prod), instead of
  hardcoding prod for an empty cfg.Env — which could revoke against the wrong
  host and leave the real session token valid after sign-out.
- Clear + persist local state BEFORE the revoke call, so a failed Save can't
  leave a token that's already been revoked server-side on disk as a broken
  "signed in" state. Revoke stays best-effort, after the local clear.

Test: TestLogout_RevokesAgainstSessionEnv pins empty cfg.Env → $CLIENT_ENV.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* feat(cli): rename dataset → data (ingest/list/delete; keep aliases) (cli#99) (#119)

Rename the `dataset` command group to `data` and its verbs:
- `push`  → `ingest`  (canonical: `tracebloc data ingest <path>`)
- `rm`    → `delete`  (canonical: `tracebloc data delete <table>`)
- `list`  stays `list`

Deprecated aliases retained for one cycle via cobra's Aliases field:
- `data` has `Aliases: []string{"dataset"}`
- `ingest` has `Aliases: []string{"push"}`
- `delete` has `Aliases: []string{"rm"}`

All user-facing text updated (Short, Long, Banner, home screen, examples).
Internal Go symbols renamed accordingly (newDataCmd, newDataIngestCmd,
newDataDeleteCmd, runDataIngestArgs, runDataIngest, runDataDelete, etc.).
Tests updated to use canonical names; alias-resolution tests added.
No behaviour change beyond naming; ingestion logic and cli#67–#77 fixes
are untouched. The separate top-level `ingest validate` command is
unmodified.

Closes #99

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* feat(cli): env-scoped config v2 — per-env profiles fix the cross-env clobber (cli#100) (#120)

RFC-0001 §7.5 / §14 R10 / Appendix C.8. The v1 ~/.tracebloc/config.json held a
single flat {env,token,active_client_id}, so `login --env X` overwrote env+token
but stranded the previous env's active_client_id — a dev/stg/prod user could
silently target the wrong client.

- Config is now v2: { version, current_env, profiles: { <env>: {email, token,
  expires_at, active_client_id} } }. One active_client_id PER env.
- `login --env X` switches current_env and writes X's profile via Profile(env)
  (which returns X's existing profile), so a re-login preserves its
  active_client_id instead of clobbering it; other envs are untouched.
- v1 files auto-migrate to v2 on first read (the single record wrapped under
  profiles[env]); the first Save rewrites the file as v2. No data loss.
- `logout` clears only the current env's profile; other envs untouched. (Still
  revokes server-side, best-effort — from cli#112.)
- File stays 0600.

Call sites (login / logout / auth status / client create·list·use) now read and
write via cfg.Current() / Profile(env) / CurrentEnv. expires_at is persisted per
profile and shown by `auth status` when present (login doesn't capture it yet).

Tests: v1→v2 migration (+ empty-env→prod), the dev→prod→dev no-clobber (R10)
guarantee, SignedIn semantics, 0600, and every login/logout/auth/client path on v2.

Closes #100.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* feat(cli): --verbose + cluster doctor auth checks + resume hint & install log (cli#101) (#121)

* feat(cli): --verbose + cluster doctor auth checks + resume hint & install log (cli#101)

RFC-0001 §8.5 — make the zero-prompt connect flow's FAILURE path stand on its
own (the installer-side streaming stays with backend#838, per the ticket).

- `--verbose` / $TRACEBLOC_LOG_LEVEL: a root persistent flag that streams the
  device-flow → provision detail via a new verbose-gated ui.Detailf. Default
  output stays quiet (~the usual handful of ✔ lines).
- `cluster doctor` now runs an "Auth & config" section FIRST — before the
  cluster checks, so it works even when no cluster is reachable (the
  failed-provision case): signed in? which env + account? active client set?
  plus a live token check (WhoAmI) — 401 → ✖ re-login, network error → ⚠.
  Its status folds into the overall verdict.
- `client create` failure prints the exact, idempotent resume command (§7.2)
  + a `tracebloc cluster doctor` pointer, so a broken headless connect isn't a
  dead end.
- Every `client create` run writes ~/.tracebloc/install-<ts>.log (0600) — a
  full trace on disk even when the terminal stayed quiet.

Tests: ui Detailf gating; doctor auth (not-signed-in / valid / 401 /
no-active-client); verbose-streams vs quiet-default; provision failure →
resume command + install log.

Closes #101.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cli): don't log a cancelled provision as "done" (cli#101, Bugbot)

The success-path defer blanket-logged "done" on err==nil, so declining the
confirm prompt (which returns nil after "Cancelled.") recorded a user abort as a
successful run in install-<ts>.log — misleading for support / post-mortems.

Log the terminal outcome at each branch instead (minted / adopted / cancelled);
the defer now only handles the failure case.

Test: TestClientCreate_CancelLogsCancelledNotDone.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cli): resume command includes prompted name/location, not just flags (cli#101, Bugbot)

resumeCommand(opts) read opts, which carries only the flag values — so a failed
interactive provision (name/location typed at a prompt) printed a resume command
missing --name/--location, defeating the copy-paste-to-retry goal. Write the
resolved values back into opts after gathering them, so the defer's resumeCommand
reflects what the user actually entered.

Test: TestClientCreate_ResumeCommandIncludesPromptedValues.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cli): doctor folds auth into the kubeconfig-exit code; install log advertises no path on open failure (cli#101, Bugbot)

Two more Bugbot findings:
- `cluster doctor` returned exit 3 on a kubeconfig load / clientset failure even
  when the auth section had failed (e.g. a 401), so automation could read a bad
  token as a kubeconfig-only problem. A failed auth section now escalates the
  exit to 2; exit 3 is kept for the auth-OK case (the documented contract).
- newInstallLog returned the intended path even when opening the file failed, so
  the failure hint could print "Full log:" for a file that was never written. It
  now returns an empty path, and the caller's guard skips the hint.

Tests: doctor kubeconfig-fail → exit 2 (auth fail) / exit 3 (auth OK); install
log returns an empty path when the file can't be opened.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cli): cluster doctor treats a 426 as a hard upgrade failure, not a transient warning (Bugbot/#113) (#122)

doctor's live token check (WhoAmI) treated every non-401 error as a transient
"couldn't verify — check your network" warning. A 426 (the server enforces a
newer CLI, surfaced as *api.UpgradeRequiredError by the #98 handling) is a
definite, actionable problem — now reported as a hard ✖ "this CLI is too old —
upgrade to >= X" failure with the min-version message.

Test: TestRunAuthChecks_426IsHardFailure.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cli): don't misread a v2-shaped config as v1 (Bugbot/#113) (#123)

Load routed any config with version < 2 to migrateV1, which reads only the flat
v1 fields — so a v2-shaped file with a missing/wrong version would be migrated to
an empty record, silently dropping its profiles. migrateV1 now fires only for a
genuine v1 record (old version AND no `profiles` object); a profiles-bearing file
is always parsed as v2.

Test: TestLoadV2ShapedWithoutVersion_NotMigrated.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Asad Iqbal (Saadi) <asad.dsoft@gmail.com>
Co-authored-by: shujaat_tracebloc <153823837+shujaatTracebloc@users.noreply.github.com>
Co-authored-by: shujaat hasan <shujaathasan@shujaats-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* Merge pull request #111 from tracebloc/sec/rfc-0001-r8-mandatory-verify

fix(security): mandatory / fail-closed cosign verification in the CLI installer (RFC-0001 R8)

* fix: validate COSIGN_VERSION and enforce TLS 1.2 on installer downloads (#114)

Bugbot (promotion PR #113), two findings in scripts/install.sh:

- Unvalidated cosign version in URL: COSIGN_VERSION (env-overridable) was
  interpolated into the Sigstore download URL without the semver/path-traversal
  gate applied to RELEASE_VERSION. Generalized validate_tag into
  validate_version_tag and apply it to COSIGN_VERSION before building the URL.
  (scripts/install.sh:45)
- Signature download curls omit TLS: the .sig/.cert fetches (and the binary,
  SHA256SUMS, and latest-redirect curls) lacked --tlsv1.2, unlike the new cosign
  bootstrap fetches. Add --tlsv1.2 to every security-sensitive download.
  (scripts/install.sh:335)

Co-authored-by: shujaat hasan <shujaathasan@shujaats-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* chore(schema): re-vendor ingest.v1.json — register seq2seq as recognize-but-unsupported (#118)

The CI "Schema drift check" (scripts/sync-schema.sh --check) was failing on
develop itself and therefore every PR — the vendored
internal/schema/ingest.v1.json had drifted from data-ingestors master, which
added the `seq2seq` task category (enum entry + an if/then requiring `texts`,
membership in the self-supervised text group, and an updated `texts`
description). The drift is purely additive — it does not change validation for
any already-supported category.

- Re-vendored the schema (sync-schema.sh) → --check now passes.
- Registered seq2seq in internal/push/category.go as recognize-but-not-yet-
  CLI-supported (CLISupported:false + UnsupportedNote), Family text: the CLI's
  discover/build for its raw-.txt / source\ttarget `texts` layout isn't
  implemented, so push reports it as pending rather than leaving a
  schema<->registry gap (the cli#74 drift class that TestRegistryCoversSchema-
  Categories pins). Updated the parity tests.

Mirrors #103, which did exactly this for causal_language_modeling. Full
seq2seq push support (discover/build, flip CLISupported) is a follow-up
feature — the sibling of cli#105.

go build / vet / test ./... green; drift check green.

Closes #117

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* feat(auth): send User-Agent version header + handle 426 Upgrade Required (cli#98) (#115)

RFC-0001 §13 / §14 R11 / Appendix C.1 — the CLI half of the
minimum-supported-CLI-version handshake (server half: backend#888, shipped
in backend#916).

- Every backend request now carries
  `User-Agent: tracebloc-cli/<ver> (<os>/<arch>)`, injected by a transport
  wrapper on the shared internal/api client so login + provisioning are both
  covered without threading the version through each command. The version is
  the ldflags build info, recorded once via api.SetUserAgent in NewRootCmd; a
  build with no version reports "dev" (unparseable server-side → fails open).
- A 426 from any endpoint is detected centrally in post/get and surfaced as a
  typed *UpgradeRequiredError carrying min_version, so every command degrades
  to the same actionable "your CLI is too old — upgrade to >= X" message and a
  clean non-zero exit (no stack trace), in both interactive and --plain modes.

Tests: UA on the wire, dev fallback, 426 → UpgradeRequiredError (GET + POST),
unparseable-426 body, message actionability.

Closes #98.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* feat(auth): logout revokes the token server-side (cli#112) (#116)

* feat(auth): logout revokes the token server-side via POST /auth/revoke (cli#112)

RFC-0001 §7.5 / R2 — the CLI half of the logout-revoke gap (server endpoint:
backend#887, shipped in backend#903). Until now `logout` was local-only: it
cleared ~/.tracebloc but a copied/leaked token kept authenticating (confirmed
in the 2026-06-25 connect-flow FR).

- New api.Client.RevokeToken → POST /auth/revoke (Bearer in, 204 out,
  idempotent); a non-2xx surfaces as *APIError.
- logout now revokes server-side before clearing local state. Best-effort by
  contract: on failure (offline / already-revoked / 5xx) it logs a hint and
  still clears local state — the user must always be able to log out locally.

Tests: RevokeToken 204→nil (Bearer + POST on the wire) and 5xx→APIError;
logout calls revoke (now routed at a stub — it was about to hit real prod)
and still clears local state when the revoke fails.

Closes #112.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(auth): address Bugbot on logout revoke — env resolution + save ordering (cli#112)

Two Medium findings on the revoke wiring:

- Resolve the revoke env consistently with authedClient via a new shared
  sessionEnv(cfg) helper (saved env → $CLIENT_ENV → prod), instead of
  hardcoding prod for an empty cfg.Env — which could revoke against the wrong
  host and leave the real session token valid after sign-out.
- Clear + persist local state BEFORE the revoke call, so a failed Save can't
  leave a token that's already been revoked server-side on disk as a broken
  "signed in" state. Revoke stays best-effort, after the local clear.

Test: TestLogout_RevokesAgainstSessionEnv pins empty cfg.Env → $CLIENT_ENV.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* feat(cli): rename dataset → data (ingest/list/delete; keep aliases) (cli#99) (#119)

Rename the `dataset` command group to `data` and its verbs:
- `push`  → `ingest`  (canonical: `tracebloc data ingest <path>`)
- `rm`    → `delete`  (canonical: `tracebloc data delete <table>`)
- `list`  stays `list`

Deprecated aliases retained for one cycle via cobra's Aliases field:
- `data` has `Aliases: []string{"dataset"}`
- `ingest` has `Aliases: []string{"push"}`
- `delete` has `Aliases: []string{"rm"}`

All user-facing text updated (Short, Long, Banner, home screen, examples).
Internal Go symbols renamed accordingly (newDataCmd, newDataIngestCmd,
newDataDeleteCmd, runDataIngestArgs, runDataIngest, runDataDelete, etc.).
Tests updated to use canonical names; alias-resolution tests added.
No behaviour change beyond naming; ingestion logic and cli#67–#77 fixes
are untouched. The separate top-level `ingest validate` command is
unmodified.

Closes #99

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* feat(cli): env-scoped config v2 — per-env profiles fix the cross-env clobber (cli#100) (#120)

RFC-0001 §7.5 / §14 R10 / Appendix C.8. The v1 ~/.tracebloc/config.json held a
single flat {env,token,active_client_id}, so `login --env X` overwrote env+token
but stranded the previous env's active_client_id — a dev/stg/prod user could
silently target the wrong client.

- Config is now v2: { version, current_env, profiles: { <env>: {email, token,
  expires_at, active_client_id} } }. One active_client_id PER env.
- `login --env X` switches current_env and writes X's profile via Profile(env)
  (which returns X's existing profile), so a re-login preserves its
  active_client_id instead of clobbering it; other envs are untouched.
- v1 files auto-migrate to v2 on first read (the single record wrapped under
  profiles[env]); the first Save rewrites the file as v2. No data loss.
- `logout` clears only the current env's profile; other envs untouched. (Still
  revokes server-side, best-effort — from cli#112.)
- File stays 0600.

Call sites (login / logout / auth status / client create·list·use) now read and
write via cfg.Current() / Profile(env) / CurrentEnv. expires_at is persisted per
profile and shown by `auth status` when present (login doesn't capture it yet).

Tests: v1→v2 migration (+ empty-env→prod), the dev→prod→dev no-clobber (R10)
guarantee, SignedIn semantics, 0600, and every login/logout/auth/client path on v2.

Closes #100.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* feat(cli): --verbose + cluster doctor auth checks + resume hint & install log (cli#101) (#121)

* feat(cli): --verbose + cluster doctor auth checks + resume hint & install log (cli#101)

RFC-0001 §8.5 — make the zero-prompt connect flow's FAILURE path stand on its
own (the installer-side streaming stays with backend#838, per the ticket).

- `--verbose` / $TRACEBLOC_LOG_LEVEL: a root persistent flag that streams the
  device-flow → provision detail via a new verbose-gated ui.Detailf. Default
  output stays quiet (~the usual handful of ✔ lines).
- `cluster doctor` now runs an "Auth & config" section FIRST — before the
  cluster checks, so it works even when no cluster is reachable (the
  failed-provision case): signed in? which env + account? active client set?
  plus a live token check (WhoAmI) — 401 → ✖ re-login, network error → ⚠.
  Its status folds into the overall verdict.
- `client create` failure prints the exact, idempotent resume command (§7.2)
  + a `tracebloc cluster doctor` pointer, so a broken headless connect isn't a
  dead end.
- Every `client create` run writes ~/.tracebloc/install-<ts>.log (0600) — a
  full trace on disk even when the terminal stayed quiet.

Tests: ui Detailf gating; doctor auth (not-signed-in / valid / 401 /
no-active-client); verbose-streams vs quiet-default; provision failure →
resume command + install log.

Closes #101.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cli): don't log a cancelled provision as "done" (cli#101, Bugbot)

The success-path defer blanket-logged "done" on err==nil, so declining the
confirm prompt (which returns nil after "Cancelled.") recorded a user abort as a
successful run in install-<ts>.log — misleading for support / post-mortems.

Log the terminal outcome at each branch instead (minted / adopted / cancelled);
the defer now only handles the failure case.

Test: TestClientCreate_CancelLogsCancelledNotDone.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cli): resume command includes prompted name/location, not just flags (cli#101, Bugbot)

resumeCommand(opts) read opts, which carries only the flag values — so a failed
interactive provision (name/location typed at a prompt) printed a resume command
missing --name/--location, defeating the copy-paste-to-retry goal. Write the
resolved values back into opts after gathering them, so the defer's resumeCommand
reflects what the user actually entered.

Test: TestClientCreate_ResumeCommandIncludesPromptedValues.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cli): doctor folds auth into the kubeconfig-exit code; install log advertises no path on open failure (cli#101, Bugbot)

Two more Bugbot findings:
- `cluster doctor` returned exit 3 on a kubeconfig load / clientset failure even
  when the auth section had failed (e.g. a 401), so automation could read a bad
  token as a kubeconfig-only problem. A failed auth section now escalates the
  exit to 2; exit 3 is kept for the auth-OK case (the documented contract).
- newInstallLog returned the intended path even when opening the file failed, so
  the failure hint could print "Full log:" for a file that was never written. It
  now returns an empty path, and the caller's guard skips the hint.

Tests: doctor kubeconfig-fail → exit 2 (auth fail) / exit 3 (auth OK); install
log returns an empty path when the file can't be opened.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cli): cluster doctor treats a 426 as a hard upgrade failure, not a transient warning (Bugbot/#113) (#122)

doctor's live token check (WhoAmI) treated every non-401 error as a transient
"couldn't verify — check your network" warning. A 426 (the server enforces a
newer CLI, surfaced as *api.UpgradeRequiredError by the #98 handling) is a
definite, actionable problem — now reported as a hard ✖ "this CLI is too old —
upgrade to >= X" failure with the min-version message.

Test: TestRunAuthChecks_426IsHardFailure.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cli): don't misread a v2-shaped config as v1 (Bugbot/#113) (#123)

Load routed any config with version < 2 to migrateV1, which reads only the flat
v1 fields — so a v2-shaped file with a missing/wrong version would be migrated to
an empty record, silently dropping its profiles. migrateV1 now fires only for a
genuine v1 record (old version AND no `profiles` object); a profiles-bearing file
is always parsed as v2.

Test: TestLoadV2ShapedWithoutVersion_NotMigrated.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cli#125): write the UUID username (not the numeric id) as TRACEBLOC_CLIENT_ID (#126)

`client create` wrote strconv.Itoa(pc.ID) — the numeric dashboard id — as
TRACEBLOC_CLIENT_ID in both credential-file paths (mint + adopt) and in the
printed credential. But TRACEBLOC_CLIENT_ID is the *auth username*: it flows
cred → helm clientId → secret CLIENT_ID → pod env → controller.py
os.getenv("CLIENT_ID"), which is POSTed to api-token-auth as the login
username. The backend authenticates an EdgeDevice by its UUID username
(str(uuid4)), never the numeric id, so a freshly-provisioned client always
failed auth ("Unable to log in with provided credentials") and crash-looped.

Write pc.Username instead. The numeric id stays display-only (shown as
"dashboard id"). Both credential-file tests asserted the buggy numeric id and
are updated to require the username; the print-path test now asserts the
username is shown as the client id.

This is what client PR #293's skip-verify masked — verify_credentials was a
correct canary. #293 will be reverted once this ships (v0.5.1).

Refs: RFC-0001 backend#830, installer credential handoff #838.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Asad Iqbal (Saadi) <asad.dsoft@gmail.com>
Co-authored-by: shujaat_tracebloc <153823837+shujaatTracebloc@users.noreply.github.com>
Co-authored-by: shujaat hasan <shujaathasan@shujaats-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
main carried no unique code — only prior promote snapshots — so develop is
authoritative; conflicts resolved in favour of develop.
chore(release): promote develop → main (2026-07-07)
main's promote-merge commits (#148, #155) aren't in develop's ancestry, so the
next develop→main promote would conflict on release drift again. Trees are
already identical (v0.6.0 == develop), so this changes no files — it only brings
main's history into develop so future promotes fast-forward cleanly.
@saadqbal saadqbal self-assigned this Jul 7, 2026
@saadqbal saadqbal merged commit 50a8289 into develop Jul 7, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants