From 087b415f73208941369e4585c2281dbeb6c338b2 Mon Sep 17 00:00:00 2001 From: Viktor Barzin Date: Thu, 18 Jun 2026 19:24:08 +0000 Subject: [PATCH 1/2] homelab: add work verbs (start/land/clean) with a land verification gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completes the infra-loop verb surface. work start creates .worktrees/ on / off /master (git-crypt-aware, ensures .worktrees is ignored) and prints the path for native EnterWorktree entry. work land fetches, merges master in, verifies, pushes HEAD:master with non-fast-forward retry, and falls back to pushing the feature branch for a PR when the direct push is rejected (branch protection). work clean removes the worktree + branch. Safety: work land REFUSES to push when it cannot verify (no --verify-cmd and no auto-detected suite) unless --no-verify is passed. This was added after an accidental smoke-test invocation pushed unverified WIP to master (benign — the infra CI applied 0 stacks since the diff was cli/-only — but the gate makes an unverified land a deliberate choice, not the default). Known v0.1 limitation: land does not yet block on CI to green; that arrives with the ci/deploy watch verbs. It prints a reminder to follow the pipeline manually. Co-Authored-By: Claude Opus 4.8 --- cli/cmd_work.go | 205 +++++++++++++++++++++++++++++++++++++++++++ cli/cmd_work_test.go | 32 +++++++ cli/homelab.go | 1 + cli/repo.go | 38 ++++++++ 4 files changed, 276 insertions(+) create mode 100644 cli/cmd_work.go create mode 100644 cli/cmd_work_test.go diff --git a/cli/cmd_work.go b/cli/cmd_work.go new file mode 100644 index 00000000..0a9278a1 --- /dev/null +++ b/cli/cmd_work.go @@ -0,0 +1,205 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "strings" +) + +func workCommands() []Command { + return []Command{ + {Path: []string{"work", "start"}, Tier: TierWrite, + Summary: "create a worktree + branch for a task (enter it with EnterWorktree)", Run: workStart}, + {Path: []string{"work", "land"}, Tier: TierWrite, + Summary: "merge master in, verify, push HEAD:master (run from the worktree)", Run: workLand}, + {Path: []string{"work", "clean"}, Tier: TierWrite, + Summary: "remove a task's worktree + branch (run from the main checkout)", Run: workClean}, + } +} + +// flagValue extracts `--name value` or `--name=value` from args. +func flagValue(args []string, name string) string { + for i, a := range args { + if a == name && i+1 < len(args) { + return args[i+1] + } + if strings.HasPrefix(a, name+"=") { + return strings.TrimPrefix(a, name+"=") + } + } + return "" +} + +func remotesOrEmpty(repoRoot string) []string { + r, _ := gitRemotes(repoRoot) + return r +} + +// workStart creates .worktrees/ on branch / off /master. +func workStart(args []string) error { + topic, _ := firstPositional(args) + if topic == "" { + return fmt.Errorf("usage: homelab work start ") + } + cwd, _ := os.Getwd() + repoRoot, err := gitRepoRoot(cwd) + if err != nil { + return fmt.Errorf("not in a git repository: %w", err) + } + remote := preferRemote(remotesOrEmpty(repoRoot)) + if remote == "" { + return fmt.Errorf("no git remote configured in %s", repoRoot) + } + flags := cryptFlagsFor(repoRoot) + branch := currentUser() + "/" + topic + wtRel := filepath.Join(".worktrees", topic) + + ensureWorktreesIgnored(repoRoot) + if err := gitStream(repoRoot, flags, "fetch", remote); err != nil { + return fmt.Errorf("fetch %s failed: %w", remote, err) + } + if err := gitStream(repoRoot, flags, "worktree", "add", wtRel, "-b", branch, remote+"/master"); err != nil { + return fmt.Errorf("worktree add failed: %w", err) + } + wtPath := filepath.Join(repoRoot, wtRel) + fmt.Printf("homelab: created worktree %s (branch %s off %s/master)\n", wtPath, branch, remote) + fmt.Printf("homelab: enter it with the native tool: EnterWorktree(path=%q)\n", wtPath) + return nil +} + +// workLand integrates the current branch into master: fetch, merge master in, +// verify, push HEAD:master (retrying on non-fast-forward), with a feature-branch +// fallback when the direct push is rejected (e.g. branch protection). +func workLand(args []string) error { + verifyCmd := flagValue(args, "--verify-cmd") + cwd, _ := os.Getwd() + repoRoot, err := gitRepoRoot(cwd) + if err != nil { + return fmt.Errorf("not in a git repository: %w", err) + } + branch, err := gitOutput(repoRoot, "rev-parse", "--abbrev-ref", "HEAD") + if err != nil { + return err + } + if branch == "master" || branch == "main" { + return fmt.Errorf("refusing to land: already on %s", branch) + } + remote := preferRemote(remotesOrEmpty(repoRoot)) + if remote == "" { + return fmt.Errorf("no git remote configured in %s", repoRoot) + } + flags := cryptFlagsFor(repoRoot) + + if err := gitStream(repoRoot, flags, "fetch", remote); err != nil { + return fmt.Errorf("fetch failed: %w", err) + } + if err := gitStream(repoRoot, flags, "merge", "--no-edit", remote+"/master"); err != nil { + return fmt.Errorf("merging %s/master failed — resolve conflicts then re-run `homelab work land`: %w", remote, err) + } + if err := runVerify(repoRoot, verifyCmd, containsArg(args, "--no-verify")); err != nil { + return fmt.Errorf("not landing: %w", err) + } + if err := pushWithRetry(repoRoot, flags, remote, 3); err != nil { + return landFallback(repoRoot, flags, remote, branch, err) + } + fmt.Printf("homelab: landed %s -> %s/master.\n", branch, remote) + fmt.Println("homelab: CI was triggered by the push — watch it to completion before calling the work done") + fmt.Println(" (the ci/deploy watch verbs arrive in a later version; for now follow the pipeline manually).") + return nil +} + +// runVerify runs the explicit --verify-cmd, else auto-detects (go test). If +// neither is available it REFUSES (returns an error) unless allowSkip is set — +// landing to master unverified must be a deliberate choice (--no-verify). +func runVerify(repoRoot, verifyCmd string, allowSkip bool) error { + if verifyCmd != "" { + fmt.Fprintf(os.Stderr, "homelab: verify: %s\n", verifyCmd) + return runStreamingIn(repoRoot, "sh", "-c", verifyCmd) + } + if isFile(filepath.Join(repoRoot, "go.mod")) { + fmt.Fprintln(os.Stderr, "homelab: verify: go test ./...") + return runStreamingIn(repoRoot, "go", "test", "./...") + } + if allowSkip { + fmt.Fprintln(os.Stderr, "homelab: WARNING: --no-verify set — landing without verification") + return nil + } + return fmt.Errorf("no verification configured for this repo — pass --verify-cmd \"...\" or --no-verify to land without verifying") +} + +// pushWithRetry pushes HEAD:master, recovering from non-fast-forward rejections +// by fetching + merging master and retrying. +func pushWithRetry(repoRoot string, flags []string, remote string, attempts int) error { + var lastErr error + for i := 0; i < attempts; i++ { + if err := gitStream(repoRoot, flags, "push", remote, "HEAD:master"); err == nil { + return nil + } else { + lastErr = err + } + if i < attempts-1 { + fmt.Fprintln(os.Stderr, "homelab: push rejected — fetching + merging master, then retrying") + if err := gitStream(repoRoot, flags, "fetch", remote); err != nil { + return err + } + if err := gitStream(repoRoot, flags, "merge", "--no-edit", remote+"/master"); err != nil { + return err + } + } + } + return fmt.Errorf("push to %s/master failed after %d attempts: %w", remote, attempts, lastErr) +} + +// landFallback pushes the feature branch when the direct master push is rejected +// (e.g. branch protection), so the work isn't lost and a PR can be opened. +func landFallback(repoRoot string, flags []string, remote, branch string, pushErr error) error { + fmt.Fprintf(os.Stderr, "homelab: direct push to master failed (%v)\n", pushErr) + fmt.Fprintf(os.Stderr, "homelab: falling back to pushing the feature branch %q for a PR\n", branch) + if err := gitStream(repoRoot, flags, "push", "-u", remote, branch); err != nil { + return fmt.Errorf("fallback branch push also failed: %w", err) + } + fmt.Printf("homelab: pushed %s to %s. Open a PR to land it (branch protection blocked the direct push).\n", branch, remote) + return nil +} + +// workClean removes a task's worktree and branch. Run from the main checkout. +func workClean(args []string) error { + topic, _ := firstPositional(args) + if topic == "" { + return fmt.Errorf("usage: homelab work clean (run from the main checkout)") + } + cwd, _ := os.Getwd() + repoRoot, err := gitRepoRoot(cwd) + if err != nil { + return fmt.Errorf("not in a git repository: %w", err) + } + flags := cryptFlagsFor(repoRoot) + wtRel := filepath.Join(".worktrees", topic) + branch := currentUser() + "/" + topic + + if err := gitStream(repoRoot, flags, "worktree", "remove", wtRel); err != nil { + return fmt.Errorf("worktree remove failed (uncommitted changes? run from the main checkout, not the worktree): %w", err) + } + if err := gitStream(repoRoot, flags, "branch", "-d", branch); err != nil { + fmt.Fprintf(os.Stderr, "homelab: note: could not delete branch %s (unmerged — use `git branch -D` if intended): %v\n", branch, err) + } + fmt.Printf("homelab: removed worktree %s and branch %s\n", wtRel, branch) + return nil +} + +// ensureWorktreesIgnored appends .worktrees/ to .gitignore if not already ignored. +func ensureWorktreesIgnored(repoRoot string) { + if _, err := gitOutput(repoRoot, "check-ignore", ".worktrees"); err == nil { + return + } + gi := filepath.Join(repoRoot, ".gitignore") + f, err := os.OpenFile(gi, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) + if err != nil { + return + } + defer f.Close() + if _, err := f.WriteString("\n.worktrees/\n"); err == nil { + fmt.Fprintln(os.Stderr, "homelab: added .worktrees/ to .gitignore") + } +} diff --git a/cli/cmd_work_test.go b/cli/cmd_work_test.go new file mode 100644 index 00000000..af573dd6 --- /dev/null +++ b/cli/cmd_work_test.go @@ -0,0 +1,32 @@ +package main + +import "testing" + +func TestRunVerifyRefusesWhenNothingToVerify(t *testing.T) { + dir := t.TempDir() // no go.mod, no verify cmd + if err := runVerify(dir, "", false); err == nil { + t.Fatal("runVerify must refuse (error) when nothing to verify and --no-verify absent") + } + if err := runVerify(dir, "", true); err != nil { + t.Fatalf("runVerify must skip when --no-verify set, got: %v", err) + } +} + +func TestFlagValue(t *testing.T) { + cases := []struct { + args []string + name string + want string + }{ + {[]string{"--verify-cmd", "go test ./..."}, "--verify-cmd", "go test ./..."}, + {[]string{"--verify-cmd=make test"}, "--verify-cmd", "make test"}, + {[]string{"topic", "--verify-cmd", "x"}, "--verify-cmd", "x"}, + {[]string{"topic"}, "--verify-cmd", ""}, + {[]string{"--verify-cmd"}, "--verify-cmd", ""}, // no value + } + for _, c := range cases { + if got := flagValue(c.args, c.name); got != c.want { + t.Errorf("flagValue(%v, %q) = %q, want %q", c.args, c.name, got, c.want) + } + } +} diff --git a/cli/homelab.go b/cli/homelab.go index f3ad5f4b..ce66943c 100644 --- a/cli/homelab.go +++ b/cli/homelab.go @@ -13,6 +13,7 @@ func buildRegistry() []Command { var reg []Command reg = append(reg, claimCommands()...) reg = append(reg, tfCommands()...) + reg = append(reg, workCommands()...) return reg } diff --git a/cli/repo.go b/cli/repo.go index ff65e8a4..3e0dc4f1 100644 --- a/cli/repo.go +++ b/cli/repo.go @@ -1,7 +1,10 @@ package main import ( + "os" "os/exec" + "os/user" + "path/filepath" "strings" ) @@ -61,3 +64,38 @@ func gitRemotes(dir string) ([]string, error) { } return strings.Split(out, "\n"), nil } + +// isGitCryptRepo reports whether the repo at repoRoot uses git-crypt. +func isGitCryptRepo(repoRoot string) bool { + b, err := os.ReadFile(filepath.Join(repoRoot, ".gitattributes")) + if err != nil { + return false + } + return hasGitCryptAttr(string(b)) +} + +// cryptFlagsFor returns the git-crypt filter flags when repoRoot is encrypted, +// else nil. These are injected per-command and never persisted. +func cryptFlagsFor(repoRoot string) []string { + if isGitCryptRepo(repoRoot) { + return gitCryptFlags() + } + return nil +} + +// gitStream runs `git [cryptFlags] -C repoRoot ` with live output. +func gitStream(repoRoot string, cryptFlags []string, args ...string) error { + full := append(append([]string{}, cryptFlags...), append([]string{"-C", repoRoot}, args...)...) + return runStreamingIn("", "git", full...) +} + +// currentUser returns the OS username for branch naming (/). +func currentUser() string { + if u := os.Getenv("USER"); u != "" { + return u + } + if u, err := user.Current(); err == nil && u.Username != "" { + return u.Username + } + return "user" +} From 66caa0bf7f9436797d3bb31c344da501a535708c Mon Sep 17 00:00:00 2001 From: Viktor Barzin Date: Thu, 18 Jun 2026 19:25:51 +0000 Subject: [PATCH 2/2] homelab: v0.1 docs, distribution wiring, and version Completes v0.1: documentation, build/install path, and version stamping. - cli/VERSION (v0.1.0) stamped into the binary via ldflags. - cli/README.md rewritten as the homelab overview (verbs + tiers, manifest, build, the preserved legacy webhook use-cases). - docs/adr/0004-0006: why homelab exists (grown in place from infra/cli, not a separate repo), v0.1 scope + everything-allowed/tiers-recorded, and the work/tf behaviour (native worktree entry, verification-gated auto-land, presence-coupled apply). - setup-devvm.sh builds cli/ -> /usr/local/bin/homelab each provisioning run (t3-dispatch pattern), so every devvm user gets the current binary. - AGENTS.md: discovery pointer under Common Operations. Co-Authored-By: Claude Opus 4.8 --- AGENTS.md | 1 + cli/README.md | 70 +++++++++++++++++++++++++++- cli/VERSION | 1 + docs/adr/0004-homelab-unified-cli.md | 30 ++++++++++++ docs/adr/0005-homelab-v01-scope.md | 23 +++++++++ docs/adr/0006-homelab-work-and-tf.md | 29 ++++++++++++ scripts/workstation/setup-devvm.sh | 12 +++++ 7 files changed, 164 insertions(+), 2 deletions(-) create mode 100644 cli/VERSION create mode 100644 docs/adr/0004-homelab-unified-cli.md create mode 100644 docs/adr/0005-homelab-v01-scope.md create mode 100644 docs/adr/0006-homelab-work-and-tf.md diff --git a/AGENTS.md b/AGENTS.md index 797ed5df..f281fd33 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -289,6 +289,7 @@ curl -X POST -H "Authorization: token $TOK" -H 'Content-Type: application/json' ``` ## Common Operations +- **`homelab` CLI** (`/usr/local/bin/homelab`, source `cli/`): unified infra-ops verbs — run `homelab manifest` to discover the surface (each verb tagged read/write). v0.1 covers the inner loop: `homelab tf plan|fmt|apply ` (wraps `scripts/tg`; `apply` auto-claims presence + releases on exit, warns out-of-band), `homelab claim|release :`, `homelab work start|land|clean ` (worktree lifecycle; `land` gates on verification, `--verify-cmd`/`--no-verify`). Full docs: `cli/README.md`. - **Deploy new service**: Use `stacks//` as template. Create stack, add DNS in tfvars, apply platform then service. - **Fix crashed pods**: Run healthcheck first. Safe to delete evicted/failed pods and CrashLoopBackOff pods with >10 restarts. - **OOMKilled**: Check `kubectl describe limitrange tier-defaults -n `. Increase `resources.limits.memory` in the stack's main.tf. diff --git a/cli/README.md b/cli/README.md index 48b83c93..80ea0f52 100644 --- a/cli/README.md +++ b/cli/README.md @@ -1,2 +1,68 @@ -# What is this? -This is a CLI to manipulate files in the terraform repo and commit and push them +# homelab + +`homelab` is the unified, agent-facing CLI for operating this homelab — one +composable, JSON-capable surface for the operations agents run over and over, +discovered progressively at runtime. It is grown **in place** from this +directory (the former `infra-cli`), and the legacy webhook use-cases still work +(see below). + +It encodes *actions*, never *judgment*: methodology (debugging, TDD, review) and +third-party/owned MCP servers (e.g. phpIPAM) are deliberately out of scope. + +## Usage + +``` +homelab [args] +homelab manifest [--json] # list every verb + its read/write tier (discovery entrypoint) +homelab version +``` + +### v0.1 verbs — the infra inner-loop + +| Command | Tier | What it does | +|---|---|---| +| `claim : --purpose "…"` | write | claim a shared resource on the presence board (wraps `scripts/presence`) | +| `release :` | write | release a presence claim | +| `tf plan ` | read | `scripts/tg plan` for a stack (resolved from cwd) | +| `tf validate ` | read | `scripts/tg validate` | +| `tf fmt ` | read | `terraform fmt -recursive` on the stack | +| `tf force-unlock ` | write | release a stuck state lock | +| `tf apply ` | write | `scripts/tg apply` — auto-claims `stack:`, always releases, warns it's out-of-band | +| `work start ` | write | create `.worktrees/` on `/` off `/master`; enter with native `EnterWorktree` | +| `work land [--verify-cmd "…"] [--no-verify]` | write | merge master in → verify → push `HEAD:master` (non-ff retry; PR fallback) | +| `work clean ` | write | remove a task's worktree + branch (run from the main checkout) | + +`tf` resolves the stack dir by walking up from cwd to the infra root and +delegates to `scripts/tg` (which owns state decrypt/encrypt, the Vault lock, and +the ingress auth-comment check). git-crypt filter flags are auto-injected on git +operations in the encrypted infra repo. + +**`work land` refuses to push when it cannot verify** (no `--verify-cmd` and no +auto-detected suite) unless you pass `--no-verify` — landing to master unverified +must be deliberate. It does not yet block on CI to green (that arrives with the +ci/deploy watch verbs); it reminds you to follow the pipeline. + +Tiers are recorded per verb so a future PreToolUse classifier can auto-allow +reads / prompt writes; v0.1 allows everything and relies on existing gates +(permission mode, presence claims, plan approval). + +## Build / install + +Built from source to `/usr/local/bin/homelab` during devvm provisioning +(`scripts/workstation/setup-devvm.sh`, the `t3-dispatch` pattern); version is +stamped from `cli/VERSION` via ldflags. Manual build: + +``` +cd cli && go build -ldflags "-X main.version=$(cat VERSION)" -o /usr/local/bin/homelab . +go test ./... +``` + +## Legacy webhook use-cases (preserved) + +This binary is also the in-cluster `infra-cli` image. Invocations starting with +`-use-case=` fall through to the +original flag-based path unchanged, so the webhook handler is unaffected. + +## Design + +See `infra/docs/adr/0004`–`0006` for the architecture decisions. diff --git a/cli/VERSION b/cli/VERSION new file mode 100644 index 00000000..b82608c0 --- /dev/null +++ b/cli/VERSION @@ -0,0 +1 @@ +v0.1.0 diff --git a/docs/adr/0004-homelab-unified-cli.md b/docs/adr/0004-homelab-unified-cli.md new file mode 100644 index 00000000..27cce02a --- /dev/null +++ b/docs/adr/0004-homelab-unified-cli.md @@ -0,0 +1,30 @@ +# homelab: a unified infra-ops CLI grown in place from infra/cli + +Agents re-derive the same operational command boilerplate every session — mining +51,116 bash commands across 2,225 past sessions showed dense, repeated patterns +(the infra inner-loop alone is ~29%). We are building `homelab`, one CLI encoding +the deterministic, repeated **actions** (not judgment) agents run — composable in +bash, JSON-capable, and discovered progressively via `homelab manifest`. It is +grown **in place** in `cli/` (the existing `infra-cli`), absorbing new verb-groups +alongside the preserved legacy webhook use-cases. Versioned with a `cli/VERSION` +file (the infra repo deploys continuously and does not cut semver tags). + +## Considered options + +- **Its own top-level repo** (the original plan) — rejected in favour of keeping + it where the Terraform/Terragrunt and `scripts/tg` it drives already live; the + Go source isn't git-crypt-encrypted and a provision-time build is unaffected by + GitOps continuous-deploy. +- **A fresh CLI ignoring infra-cli** — rejected: strands the VPN/DNS/email + webhook use-cases. +- **Raw kubectl/tg/ssh + skills + MCP only** — kept for everything outside the + recurring action surface (methodology skills; third-party/owned MCP such as + phpIPAM, which homelab does NOT duplicate). + +## Consequences + +- The binary is dual-purpose: the agent-facing `homelab` verb surface AND the + in-cluster `infra-cli` webhook image. `main()` front-dispatches homelab verbs + and falls through to the legacy `-use-case` path verbatim. +- Distribution: built from source to `/usr/local/bin/homelab` during devvm + provisioning (`t3-dispatch` precedent), refreshed by `t3-autoupdate`. diff --git a/docs/adr/0005-homelab-v01-scope.md b/docs/adr/0005-homelab-v01-scope.md new file mode 100644 index 00000000..c1da7a95 --- /dev/null +++ b/docs/adr/0005-homelab-v01-scope.md @@ -0,0 +1,23 @@ +# homelab v0.1 scope: the infra inner-loop; everything allowed, tiers recorded + +v0.1 ships only the highest-volume surface — the infra inner-loop: `work` +(worktree lifecycle), `tf` (terragrunt via `scripts/tg` + fmt/validate/ +force-unlock), and `claim`/`release` (presence) — because it is ~29% of all mined +commands and where agents lose the most time and leak the most presence claims. + +v0.1 enforces **no** homelab-level permission gating: everything is allowed, +relying on existing gates (harness permission mode, presence claims, plan +approval). But every verb records a `read|write` tier (visible in `manifest`), so +a PreToolUse classifier hook (auto-allow reads / prompt writes) can be added +later with zero restructuring. + +## Considered options + +- **Reads-first vertical slice** (top read verb per domain) — lower risk, broad + value, but defers the toil that motivated the project. +- **One domain deep (k8s)** — cleanest template, narrow day-one value. + +We chose the highest-volume-but-write-heavy infra loop deliberately, accepting +the extra complexity (worktree lifecycle, git-crypt flag injection, presence +coupling, branch-protection PR fallback) for the biggest immediate toil +reduction. k8s/node/secret/net/ci verb-groups are deferred to later versions. diff --git a/docs/adr/0006-homelab-work-and-tf.md b/docs/adr/0006-homelab-work-and-tf.md new file mode 100644 index 00000000..fcdddc30 --- /dev/null +++ b/docs/adr/0006-homelab-work-and-tf.md @@ -0,0 +1,29 @@ +# homelab work/tf behaviour: native worktree entry, gated auto-land, presence-coupled apply + +Four behaviours of the infra-loop verbs are surprising enough to record: + +1. **`work` owns worktree create/land/clean, but session *entry* delegates to the + native harness worktree tool.** A CLI is a child process and cannot change the + agent's working directory; `EnterWorktree` can. So `homelab work start ` + creates the worktree + branch off `/master` (git-crypt-aware) and + prints the path — the agent enters it with native `EnterWorktree({path})`. + +2. **`work land` is auto-land, but gated on verification.** It merges master in → + runs verification → pushes `HEAD:master` (fetch+merge+retry on + non-fast-forward) → falls back to pushing the feature branch for a PR when the + direct push is rejected (branch protection). It **refuses to push when it + cannot verify** (no `--verify-cmd` and no auto-detected suite) unless + `--no-verify` is passed — added after an accidental smoke-test land pushed + unverified WIP to master (benign: the infra CI applied 0 stacks because the + diff was `cli/`-only, but an unverified land must be deliberate, not default). + +3. **`tf apply` is first-class despite GitOps, and mandatorily presence-coupled.** + Local applies are out-of-band (CI applies canonically on push) but happen + constantly (~763× in the corpus). `tf apply ` auto-claims `stack:`, + delegates to `scripts/tg apply --non-interactive`, and **always releases on + exit** (normal, error, or signal via `sync.Once` + handler) — fixing the + documented ~200-claim leak — and prints an out-of-band reminder. + +4. **Known v0.1 limitation:** `work land` does not yet block on CI to green; that + arrives with the ci/deploy watch verb-group. It prints a reminder to follow + the pipeline manually. diff --git a/scripts/workstation/setup-devvm.sh b/scripts/workstation/setup-devvm.sh index 37779867..1807bb80 100755 --- a/scripts/workstation/setup-devvm.sh +++ b/scripts/workstation/setup-devvm.sh @@ -175,6 +175,18 @@ if [[ ! -x /usr/local/bin/t3-dispatch ]]; then log "WARN: go absent -> cannot build t3-dispatch; install golang-go or deploy the binary" fi fi +# 9b2) homelab: unified infra-ops CLI (agent-facing verbs + the in-cluster +# infra-cli webhook image). Rebuilt from cli/ each run so it tracks the +# repo; version stamped from cli/VERSION. See cli/README.md + docs/adr/0004-0006. +if command -v go >/dev/null; then + _hl_src="$SCRIPTS/../cli" + _hl_ver="$(cat "$_hl_src/VERSION" 2>/dev/null || echo dev)" + log "building homelab CLI ($_hl_ver)" + ( cd "$_hl_src" && go build -ldflags "-X main.version=$_hl_ver" -o /usr/local/bin/homelab . ) \ + || log "WARN: homelab CLI build failed" +else + log "WARN: go absent -> cannot build homelab CLI" +fi # 9c) sudoers: t3-dispatch may run ONLY t3-mint as root. A malformed file in # /etc/sudoers.d breaks ALL sudo, so validate with visudo when available. if ! command -v visudo >/dev/null || visudo -cf "$SCRIPTS/sudoers-t3-autopair" >/dev/null; then