Author SHA1 Message Date
lyrathorpe 6868182ef5 Merge pull request 'feat(darwin): install mole' (#86) from feat/mole-macos into main
CI / flake (push) Successful in 4m19s
Reviewed-on: #86
2026-08-07 11:40:58 +01:00
lyrathorpe 90a57ab73b feat(darwin): install mole
CI / flake (push) Skipped
CI / flake (pull_request) Successful in 4m24s
useful to clean up caches
2026-08-07 11:35:06 +01:00
lyrathorpe 75f4e22624 Merge pull request 'feat: add darktable to all systems' (#85) from feat/darktable-install into main
CI / flake (push) Successful in 4m8s
Reviewed-on: #85
2026-08-07 11:16:17 +01:00
lyrathorpe cf96fec63e feat: add darktable to all systems
CI / flake (push) Skipped
CI / flake (pull_request) Successful in 3m54s
so i can edit photos wherever i have a gui
2026-08-07 11:11:55 +01:00
lyrathorpe 3cdf4d4e54 Merge pull request 'chore(claude): require ticket-scoped conventional commits on every commit' (#84) from chore/claude-memory-commit-conventions into main
CI / flake (push) Successful in 4m35s
Reviewed-on: #84
2026-08-06 16:57:27 +01:00
Emma Thorpe f61a206977 style(claude): apply prettier formatting to the git conventions memory
CI / flake (push) Skipped
CI / flake (pull_request) Successful in 1m6s
treefmt runs prettier over markdown in this repository and the CI
formatting check failed on the two preceding commits. Prettier prefers
underscores for emphasis and requires blank lines around fenced code
blocks.

No wording changes.
2026-08-06 16:54:16 +01:00
Emma Thorpe 1d5a5adbcc chore(claude): exempt repos without an issue tracker from the ticket scope
CI / flake (push) Skipped
CI / flake (pull_request) Failing after 1m8s
The previous commit required a ticket scope on every commit in every
repository. This repository has no Jira project, so the rule as written
would either block a commit or invite a fabricated WSP number.

Record the exception: in personal repositories the scope is the area of
the change (claude, deps, hosts) and conventional form still applies.
The ticket requirement is scoped to the Jira-backed work repositories
that enforce it in CI.
2026-08-06 15:20:53 +01:00
Emma Thorpe 4029866ed4 chore(claude): require ticket-scoped conventional commits on every commit
The git conventions memory said to match the repository's existing log
style. Several repositories (multicluster, core-services-cloud) have
histories dominated by bare "WSP-1234: summary" subjects, so matching
them produced commits that were not in conventional form. A related
failure was scope decay within a session: the first commit was correct
and later ones degraded to bare "test:" or "refactor:" subjects. Both
required commit history to be rebased by hand.

- Make "<type>(<TICKET-ID>): <summary>" mandatory on every commit and
  explicitly override repository log style. Style matching now applies
  to branch names only.
- Describe how to establish the real ticket ID (named in the request,
  extracted from the branch, or taken from existing commits on the
  branch) and require asking rather than guessing when none is
  available. Replace the literal WSP-1234 examples with <TICKET-ID> so
  the placeholder cannot be committed verbatim.
- Record scope decay across a session as a named failure mode.
- Cover merge commits, preferring rebase and requiring an explicit
  message when a merge commit is unavoidable.
- Add a pre-push verification grep that must return no output.
- Note that a clean git log does not prove a subject was correct when
  written, because rebasing replaces it; compare author and committer
  dates instead.

Update the MEMORY.md index entry to match.
2026-08-06 15:20:16 +01:00
renovate-bot 66b27517ba Merge pull request 'chore(deps): lock file maintenance flake inputs' (#83) from renovate/lock-file-maintenance-flake-inputs into main
CI / flake (push) Successful in 5m13s
2026-08-03 00:08:39 +01:00
Renovate Bot 6b43e76457 chore(deps): lock file maintenance flake inputs
CI / flake (push) Skipped
renovate/stability-days Updates have not met minimum release age requirement
CI / flake (pull_request) Successful in 5m50s
2026-08-02 23:02:21 +00:00
renovate-bot cbf2fdac42 Merge pull request 'chore(deps): lock file maintenance flake inputs' (#82) from renovate/lock-file-maintenance-flake-inputs into main
CI / flake (push) Successful in 4m2s
2026-07-27 05:05:54 +01:00
Renovate Bot 1fdd048eed chore(deps): lock file maintenance flake inputs
renovate/stability-days Updates have not met minimum release age requirement
CI / flake (push) Skipped
CI / flake (pull_request) Successful in 4m8s
2026-07-27 04:01:34 +00:00
renovate-bot 9b72a81d43 Merge pull request 'chore(deps): lock file maintenance flake inputs' (#81) from renovate/lock-file-maintenance-flake-inputs into main
CI / flake (push) Successful in 5m1s
2026-07-27 04:06:30 +01:00
Renovate Bot 2f0302d66e chore(deps): lock file maintenance flake inputs
CI / flake (push) Skipped
renovate/stability-days Updates have not met minimum release age requirement
CI / flake (pull_request) Successful in 4m36s
2026-07-27 03:01:35 +00:00
renovate-bot 256a9a9745 Merge pull request 'chore(deps): lock file maintenance flake inputs' (#80) from renovate/lock-file-maintenance-flake-inputs into main
CI / flake (push) Successful in 6m50s
2026-07-27 01:10:14 +01:00
Renovate Bot b746d58812 chore(deps): lock file maintenance flake inputs
CI / flake (pull_request) Successful in 6m46s
CI / flake (push) Skipped
renovate/stability-days Updates have not met minimum release age requirement
2026-07-27 00:02:59 +00:00
renovate-bot 67963ed0e0 Merge pull request 'chore(deps): lock file maintenance flake inputs' (#79) from renovate/lock-file-maintenance-flake-inputs into main
CI / flake (push) Successful in 6m1s
2026-07-27 00:09:18 +01:00
Renovate Bot 7bcc5feb35 chore(deps): lock file maintenance flake inputs
CI / flake (push) Skipped
renovate/stability-days Updates have not met minimum release age requirement
CI / flake (pull_request) Successful in 6m46s
2026-07-26 23:02:17 +00:00
lyrathorpe 9759cb70cf Merge pull request 'fix: skip commitizen's stale py_3_13 invalid-command test' (#78) from fix/commitizen-py313-invalid-command-test into main
CI / flake (push) Successful in 3m58s
Reviewed-on: #78
2026-07-21 11:26:21 +01:00
Emma Thorpe 4d27b29233 fix: skip commitizen's stale py_3_13 invalid-command test
CI / flake (pull_request) Successful in 4m4s
CI / flake (push) Skipped
commitizen 4.13.9 ships per-Python-minor golden files for its CLI
regression tests. The py_3_13 golden was captured against an early 3.13
whose argparse did not quote invalid choices. CPython later backported
quoting into the 3.13.x line, and nixos-26.05 now ships 3.13.14, so the
golden no longer matches argparse's output:

  -cz: error: ... invalid choice: 'x' (choose from init, commit, ...)
  +cz: error: ... invalid choice: 'x' (choose from 'init', 'commit', ...)

This fails commitizen's checkPhase and breaks the home-manager closure.
The package itself is unaffected. Deselect just that test via an overlay
until nixpkgs updates the fixture (or the 3.13.x revert lands upstream).
2026-07-21 10:53:49 +01:00
lyrathorpe dbc30b4b0e Merge pull request 'docs(memory): record WSP Jira transition field + ADF quirks' (#77) from docs/jira-tooling-transition-adf-quirks into main
CI / flake (push) Successful in 4m36s
Reviewed-on: #77
2026-07-21 10:34:56 +01:00
Emma ThorpeandClaude Opus 4.8 86ef677f2f docs(memory): record WSP Jira transition field + ADF quirks
CI / flake (push) Skipped
CI / flake (pull_request) Successful in 1m6s
Add to jira-tooling memory:
- per-issue-type transition required fields (Story Cancelled needs
  Resolution + Justification; Epic Cancelled needs neither)
- cancel/won't-do resolution IDs (Won't Fix 10068, Canceled 10070,
  Obsolete 10073)
- customfield_10070 (Justification) requires ADF, not a plain string,
  despite its textarea schema

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 10:32:14 +01:00
renovate-bot a65771ccac Merge pull request 'chore(deps): update gitea actions to 3d3c42e' (#76) from renovate/gitea-actions into main
CI / flake (push) Successful in 5m7s
2026-07-20 17:05:46 +01:00
Renovate Bot 89e55f4365 chore(deps): update gitea actions to 3d3c42e
CI / flake (pull_request) Successful in 4m24s
CI / flake (push) Skipped
renovate/stability-days Updates have not met minimum release age requirement
2026-07-20 16:01:03 +00:00
renovate-bot fee2f66385 Merge pull request 'chore(deps): lock file maintenance flake inputs' (#75) from renovate/lock-file-maintenance-flake-inputs into main
CI / flake (push) Successful in 3m49s
2026-07-20 00:05:56 +01:00
Renovate Bot 72770a4ddb chore(deps): lock file maintenance flake inputs
renovate/stability-days Updates have not met minimum release age requirement
CI / flake (pull_request) Successful in 4m8s
2026-07-19 23:01:34 +00:00
renovate-bot 6d9e4443e1 Merge pull request 'chore(deps): update gitea actions to 630ae54' (#74) from renovate/gitea-actions into main
CI / flake (push) Successful in 3m53s
2026-07-15 14:05:29 +01:00
Renovate Bot 7d504e68be chore(deps): update gitea actions to 630ae54
renovate/stability-days Updates have not met minimum release age requirement
CI / flake (pull_request) Successful in 4m11s
2026-07-15 13:01:08 +00:00
lyrathorpe 432a00fb35 Merge pull request 'fix(shell): don't exec tmux during VS Code's shell-env probe' (#73) from fix/vscode-shell-env-tmux-guard into main
CI / flake (push) Successful in 3m37s
Reviewed-on: #73
2026-07-14 16:48:15 +01:00
Emma Thorpe 2125dd7aac fix(shell): don't exec tmux during VS Code's shell-env probe
CI / flake (pull_request) Successful in 3m46s
VS Code on macOS resolves the shell environment at startup by running an
interactive login shell with stdout piped and no controlling terminal.
The order-200 auto-tmux block treated that probe as a normal interactive
shell and ran `exec tmux new-session`, which fails without a tty ("open
terminal failed: not a terminal") and exits non-zero. VS Code then reports
"Unable to resolve your shell environment: Unexpected exit code from
spawned shell (code 1)".

Gate the exec on a real terminal (-t 1) and skip it when
VSCODE_RESOLVING_ENVIRONMENT is set. Real terminals still land in tmux;
the integrated terminal was already exempt via TERM_PROGRAM.
2026-07-14 16:43:44 +01:00
lyrathorpe 0ff75654ce Merge pull request 'docs(claude/memory): capture PR-review comment-style feedback' (#72) from docs/claude-memory-pr-comment-style into main
CI / flake (push) Successful in 3m42s
Reviewed-on: #72
2026-07-14 16:42:21 +01:00
Emma Thorpe 51df3473ca docs(claude/memory): capture PR-review comment-style feedback
CI / flake (pull_request) Successful in 58s
Add two memories and correct one existing, from a review of PR comments
across multicluster and unified-helm over the past two months:

- code_comment_style: no Jira/ticket IDs in code comments by default,
  keep comments concise and about the non-obvious why, and use # (not
  Helm template) comments where they must reach the rendered manifest.
- copilot_review_false_positives: verify Copilot blocking claims against
  the spec and live config before acting; records two Terraform FPs.
- workflow_review_and_comments: drop the now-contradicted 'one-liner +
  WSP ticket reference' guidance, which reviewers repeatedly strip.
2026-07-14 16:40:22 +01:00
lyrathorpe 6ea5183f0e Merge pull request 'fix(darwin): uninstall virtualbox' (#71) from fix/remove-virtualbox into main
CI / flake (push) Successful in 3m42s
Reviewed-on: #71
2026-07-14 16:27:42 +01:00
lyrathorpe 00ad68a5be fix(darwin): uninstall virtualbox
CI / flake (pull_request) Successful in 3m36s
not used, currently broken
2026-07-14 16:23:50 +01:00
lyrathorpe bd309f38a2 Merge pull request 'fix(asahi): set hardware.asahi.enable explicitly' (#70) from fix/asahi-explicit-enable into main
CI / flake (push) Successful in 3m52s
Reviewed-on: #70
2026-07-14 16:12:24 +01:00
Emma Thorpe 50e2b68a23 fix(asahi): set hardware.asahi.enable explicitly
CI / flake (pull_request) Successful in 4m1s
Upstream will stop defaulting hardware.asahi.enable to true and currently emits an evaluation warning to that effect. Set it explicitly on the lyrathorpe-mbp (MBP-Asahi) host to silence the warning and be robust to the future default change. Verified the warning no longer appears in the host toplevel eval.
2026-07-14 16:08:15 +01:00
lyrathorpe e0fc1021ea Merge pull request 'feat(work): source ~/.jenkinsenv in all zsh shells' (#64) from feat/jenkins_mcp into main
CI / flake (push) Successful in 3m50s
Reviewed-on: #64
2026-07-14 16:05:51 +01:00
Emma Thorpe 10c64c77f1 feat(work.nix): source splunk MCP token too
CI / flake (pull_request) Successful in 3m51s
2026-07-14 16:01:51 +01:00
Emma ThorpeandClaude Opus 4.8 0dbf33d476 feat(work): source ~/.jenkinsenv in all zsh shells
Add programs.zsh.envExtra to the EDaaS work profile so ~/.jenkinsenv is
sourced from ~/.zshenv on every zsh invocation (login, interactive, and
non-interactive), exporting the JENKINS_UCE_/JENKINS_STF_ tokens the Jenkins
MCP servers read via ${JENKINS_*} expansion. Guarded so a missing file does
not break the shell; the file is kept out of the world-readable nix store
because it holds secrets.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 16:01:51 +01:00
lyrathorpe 677cefdb52 Merge pull request 'docs(claude/memory): Gitea pushable in-sandbox; PRs via tea' (#69) from chore/claude-memory-fix-gitea-network-ops into main
CI / flake (push) Successful in 4m5s
Reviewed-on: #69
2026-07-14 15:56:58 +01:00
Emma Thorpe 0e47006bdb docs(claude/memory): Gitea is now pushable in-sandbox; PRs via tea
CI / flake (pull_request) Successful in 59s
The code.emmathe.dev key is now loaded in the ssh-agent, so git push works with sandbox off and PRs are raised via the tea CLI. Correct the git-network-ops memory that said Gitea always needs hand-off; keep hand-off as the fallback only if the key drops from the agent.
2026-07-14 15:55:39 +01:00
lyrathorpe cf8ec786bd Merge pull request 'docs(claude/memory): note SSH-signing local-verify trap' (#68) from chore/claude-memory-signing-verification into main
CI / flake (push) Successful in 4m27s
Reviewed-on: #68
2026-07-14 15:50:19 +01:00
lyrathorpe f6d379efcc Merge pull request 'chore(claude/memory): add WSP Jira field map' (#67) from chore/claude-memory-jira-wsp-fields into main
CI / flake (push) Successful in 4m47s
Reviewed-on: #67
2026-07-14 15:50:03 +01:00
Emma Thorpe 474c5436c8 docs(claude/memory): note SSH-signing local-verify trap (sig=N without allowedSignersFile)
CI / flake (pull_request) Successful in 24s
The gpg.ssh.allowedSignersFile error and %G?=N mean git cannot verify locally, not that the commit is unsigned. Add how to confirm via gpgsig header and how to enable local verification.
2026-07-14 15:45:30 +01:00
Emma Thorpe d62a23680a chore(claude/memory): add WSP Jira field map for faster ticket creation
CI / flake (pull_request) Successful in 22s
Record WSP project field map: issue-type IDs, the six required Bug fields with their allowed values/option IDs and JSON shapes, the Task shortcut that avoids them, and platform-relevant component IDs. Cross-links jira-tooling.
2026-07-14 14:39:25 +01:00
renovate-bot 734be2a727 Merge pull request 'chore(deps): lock file maintenance flake inputs' (#66) from renovate/lock-file-maintenance-flake-inputs into main
CI / flake (push) Successful in 3m47s
2026-07-13 01:07:14 +01:00
Renovate Bot ad12062cde chore(deps): lock file maintenance flake inputs
renovate/stability-days Updates have not met minimum release age requirement
CI / flake (pull_request) Successful in 4m47s
2026-07-13 00:02:15 +00:00
renovate-bot e72d007a7d Merge pull request 'chore(deps): lock file maintenance flake inputs' (#65) from renovate/lock-file-maintenance-flake-inputs into main
CI / flake (push) Failing after 1m32s
2026-07-13 00:06:31 +01:00
Renovate Bot 8d016a546a chore(deps): lock file maintenance flake inputs
renovate/stability-days Updates have not met minimum release age requirement
CI / flake (pull_request) Successful in 4m11s
2026-07-12 23:02:10 +00:00
renovate-bot a93ca2c04d Merge pull request 'chore(deps): lock file maintenance flake inputs' (#44) from renovate/lock-file-maintenance-flake-inputs into main
CI / flake (push) Successful in 4m6s
2026-07-10 13:05:56 +01:00
Renovate Bot dcd6fa6be3 chore(deps): lock file maintenance flake inputs
renovate/stability-days Updates have not met minimum release age requirement
CI / flake (pull_request) Successful in 3m42s
2026-07-10 12:02:03 +00:00
lyrathorpe f4a9e638a5 Merge pull request 'fix(nixfiles): fix formatting stuff' (#63) from ci/format-check-all-prs into main
CI / flake (push) Successful in 3m35s
Reviewed-on: #63
2026-07-10 12:15:34 +01:00
Emma Thorpe 4fd26b1662 fix(nixfiles): fix formatting stuff
CI / flake (pull_request) Successful in 3m45s
2026-07-10 12:11:14 +01:00
lyrathorpe 665703fbe6 memory: SIBO Workabout MX project state for cross-session resume (#60)
CI / flake (push) Failing after 2m10s
Persist the state of the Psion Workabout MX reverse-engineering / barcode-inventory project so it can be resumed in a future session after a reboot.

Adds `home/claude/memory/sibo_workabout_mx_scanner.md` (project memory) and indexes it in `MEMORY.md`. Records: the repo/branch and committed docs, the confirmed scanner behaviour, the key finding that the scanner is an OO library object driven via p_getlibh/p_newsend/p_send, the blocker (on-device ordinal capture), the RE toolchain, and where the continuation procedure lives.

Takes effect after a home-manager rebuild.

Reviewed-on: #60
2026-07-10 11:42:33 +01:00
lyrathorpe ad6dac634e Merge pull request 'docs: add project CLAUDE.md and nix-shell tooling memory' (#62) from docs/dev-workflow-notes into main
CI / flake (push) Successful in 3m32s
2026-07-10 11:32:25 +01:00
Emma ThorpeandClaude Opus 4.8 9b7a9fa9b9 chore(memory): note nix shell for ad-hoc nixpkgs tooling
CI / flake (pull_request) Successful in 13s
Record that any nixpkgs tool can be run on the fly via nix run / nix
shell, so a missing command during development is not a dead end. Takes
effect after a home-manager rebuild.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 11:25:56 +01:00
Emma ThorpeandClaude Opus 4.8 33278d9ed2 docs: add project CLAUDE.md documenting flake checks
Document the formatting and lint gates (treefmt/nixfmt/shfmt/prettier,
deadnix, statix, pre-commit) and how to run them, so changes -- docs
included -- are formatted before commit. Notes the CI detect step that
skips heavy checks on docs-only PRs, which can report a false green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 11:25:56 +01:00
renovate-bot 40aef99289 Merge pull request 'chore(deps): update gitea actions to a49548c' (#59) from renovate/gitea-actions into main
CI / flake (push) Successful in 3m33s
2026-07-10 11:20:48 +01:00
Renovate Bot b191d8883c chore(deps): update gitea actions to a49548c
CI / flake (pull_request) Successful in 3m39s
2026-07-10 11:17:05 +01:00
lyrathorpe 1df7bec2d7 Merge pull request 'fix(docs): reformat README module table to satisfy treefmt' (#61) from fix/treefmt-readme-formatting into main
CI / flake (push) Successful in 3m33s
2026-07-10 11:11:53 +01:00
Emma ThorpeandClaude Opus 4.8 87318cd04d fix(docs): reformat README module table to satisfy treefmt
CI / flake (pull_request) Successful in 10s
The module-catalogue table in README.md was committed without prettier's
alignment, so the treefmt formatting flake check fails. This has left
CI (nix flake check) red on main since the #56 docs merge and blocks
every PR that triggers the full check.

Reformat with the flake's pinned formatter; no content change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 11:10:02 +01:00
lyrathorpe ad9decdf47 Merge pull request 'docs: repo layout, module catalogue, host READMEs, stale-path fixes' (#56) from docs/audit-improvements into main
CI / flake (push) Failing after 4m2s
Reviewed-on: #56
2026-07-06 15:37:28 +01:00
lyrathorpe 819633260e docs(rpi5): fix stale module paths
CI / flake (pull_request) Successful in 19s
2026-07-06 15:27:03 +01:00
lyrathorpe 610d5d8b28 docs(macpro31): fix stale module paths 2026-07-06 15:27:02 +01:00
lyrathorpe 58c0004f20 docs(t400): fix stale module paths 2026-07-06 15:27:02 +01:00
lyrathorpe f57d6ab1f9 docs(darwin): add host README 2026-07-06 15:27:01 +01:00
lyrathorpe 574773de73 docs(edaas): add host README 2026-07-06 15:27:00 +01:00
lyrathorpe a857365cc3 docs: add repo layout, module catalogue and add-a-host guide 2026-07-06 15:26:59 +01:00
31 changed files with 551 additions and 407 deletions
+28 -16
View File
@@ -1,15 +1,21 @@
# Flake CI: full `nix flake check` (formatting + deadnix + statix + pre-commit)
# plus an explicit per-host evaluation pass for granular output.
# Flake CI. Formatting (treefmt) runs on *every* PR; the heavier Nix work
# (deadnix/statix/pre-commit lints + per-host evaluation) runs only when the
# change can affect it.
name: CI
# Deliberately no `paths:` filter. This job is a required status check on main,
# and a path-filtered workflow is *skipped* (never runs) for PRs that touch no
# matching file -- which leaves the required check pending forever and blocks the
# merge (e.g. a .renovaterc.json-only change). So the workflow always runs and
# always reports. To avoid burning a full Nix evaluation on changes that can't
# affect it, the "detect" step below diffs the PR and the heavy steps run only
# when a .nix file, flake.lock, or this workflow changed; otherwise they skip and
# the job still passes. The required check is therefore always green-reportable.
# always reports.
#
# Two tiers of checks:
# * Formatting always runs. treefmt covers Markdown, YAML, and JSON as well as
# Nix and shell, so a docs- or config-only PR must be format-checked too. It
# is cheap (no host evaluation).
# * The heavy steps (full `nix flake check` + host evals) run only when a .nix
# file, flake.lock, or this workflow changed; otherwise they skip and the job
# still passes, keeping the required check green-reportable.
on:
push:
branches: [main]
@@ -20,16 +26,15 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
# Full history so the detect step can diff the PR against its base.
fetch-depth: 0
# Decide whether the Nix steps need to run. On a pull_request, diff the PR
# against its base and look for files that can affect the flake: any .nix,
# the lockfile, or this workflow. On any other event (push to main) always
# run. The job itself always succeeds, so the required status check is
# reported even when the heavy steps are skipped.
# Decide whether the *heavy* Nix steps need to run. On a pull_request, diff
# against the base for files that can affect them: any .nix, the lockfile,
# or this workflow. On any other event (push to main) always run. The
# formatting step below is unaffected -- it always runs.
- name: Detect Nix-relevant changes
id: detect
run: |
@@ -45,16 +50,16 @@ jobs:
echo "Changed files:"
echo "$changed"
if echo "$changed" | grep -Eq '(\.nix$|^flake\.lock$|^\.gitea/workflows/ci\.yaml$)'; then
echo "Nix-relevant changes found: running checks."
echo "Nix-relevant changes found: running heavy checks."
echo "run=true" >> "$GITHUB_OUTPUT"
else
echo "No Nix-relevant changes: skipping checks (job still passes)."
echo "No Nix-relevant changes: heavy checks skip (formatting still runs)."
echo "run=false" >> "$GITHUB_OUTPUT"
fi
# Nix drives the formatting check, so install it unconditionally.
- name: Install Nix
if: steps.detect.outputs.run == 'true'
uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31
uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31
with:
extra_nix_config: |
experimental-features = nix-command flakes
@@ -62,6 +67,13 @@ jobs:
substituters = https://cache.nixos.org https://nix-community.cachix.org
trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs=
# Always run: treefmt formats Markdown/YAML/JSON (docs + config) as well as
# Nix and shell, so documentation-only PRs are format-checked too. This is
# the cheap gate (no host evaluation) and pre-builds the `formatting`
# derivation that the flake check below reuses from cache.
- name: Formatting check
run: nix build --print-build-logs '.#checks.x86_64-linux.formatting'
# Runs every flake check: treefmt formatting, deadnix, statix, and the
# pre-commit hooks (so a --no-verify commit can't ship unlinted).
- name: Flake check
+63
View File
@@ -0,0 +1,63 @@
# Working on this flake
Project notes for changes to this repository. Persona and memory rules live in
the user-global config; this file is about the flake's checks and conventions.
## Before you commit: run the formatter
Formatting and linting are driven by the flake. CI (`.gitea/workflows/ci.yaml`)
runs `nix flake check`, which fails the build if any file is unformatted or trips
a lint. From the repo root:
- `nix fmt` — format the whole tree (writes changes).
- `nix flake check` — run every check read-only (what CI runs).
- `nix develop` — dev shell; its `shellHook` installs the git pre-commit hooks so
the same gates run on `git commit`.
Never commit with `--no-verify`. A bypassed commit ships unformatted content and
turns CI red on the next push to `main` (see "Docs are checked too").
## What gets checked
Defined in `flake.nix` (the `treefmt`, `pre-commit`, and `checks` blocks) and
`statix.toml`:
| Check | Tool | Covers |
| ------------ | --------------------------------- | ------------------------------------------------------- |
| `formatting` | treefmt → `nixfmt` | all `*.nix` |
| `formatting` | treefmt → `shfmt` | shell scripts |
| `formatting` | treefmt → `prettier` | **Markdown, YAML, JSON** (incl. `README.md`, this file) |
| `deadnix` | deadnix | dead Nix bindings (`--no-lambda-pattern-names`) |
| `statix` | statix | Nix antipatterns (config in `statix.toml`) |
| pre-commit | nixfmt-rfc-style, deadnix, statix | the same gates, run on commit |
Excluded from formatting: `*/hardware-configuration.nix` (generated by
`nixos-generate-config`) and `flake.lock`. Editor defaults (indent, EOL, final
newline) are in `.editorconfig`; note Markdown keeps trailing whitespace, which
encodes hard line breaks.
## Docs are checked too
prettier formats `*.md`, so **documentation edits must be run through `nix fmt`**
exactly like code. prettier re-aligns Markdown tables in particular; hand-editing
a table almost always leaves it non-conformant and fails the `formatting` check.
The CI `formatting` step runs on **every** PR — including docs- and config-only
changes — so a Markdown/YAML/JSON edit is format-checked before merge, not just
after it lands on `main`. (The heavier `deadnix`/`statix`/`pre-commit` lints and
the per-host evaluation still run only when a `.nix` file, `flake.lock`, or the
workflow changed; see `.gitea/workflows/ci.yaml`.) Run `nix fmt` before you
commit and the formatting check stays green.
## Host evaluation
CI also evaluates every `nixosConfigurations` / `darwinConfigurations` host's
toplevel (eval only, no build) on an x86_64 runner, so eval errors fail cheaply.
Reproduce locally:
```sh
nix eval --raw ".#nixosConfigurations.<host>.config.system.build.toplevel.drvPath"
```
Host lists are discovered from the flake, so adding or removing a host needs no
change to the workflow.
+99 -32
View File
@@ -12,16 +12,67 @@ Defined in the host table in [`flake.nix`](./flake.nix):
| `lyrathorpe-mbp` | `aarch64-linux` | MacBook Pro (Apple Silicon, Asahi) |
| `lyrathorpe-t400` | `x86_64-linux` | ThinkPad T400 — [install notes](./hosts/T400/README.md) |
| `lyrathorpe-macpro31` | `x86_64-linux` | Mac Pro 3,1, desktop — [install notes](./hosts/MacPro31/README.md) |
| `emmathorpe-edaas` | `x86_64-linux` | Work WSL box (NixOS-WSL) |
| `emmathorpe-edaas` | `x86_64-linux` | Work WSL box (NixOS-WSL) — [notes](./hosts/EDaaS/README.md) |
| `lyrathorpe-rpi5` | `aarch64-linux` | Raspberry Pi 5 headless server: Docker host + nginx reverse proxy — [install notes](./hosts/RPi5/README.md) |
| `lyrathorpe-mac` | `aarch64-darwin` | macOS (nix-darwin) |
| `lyrathorpe-mac` | `aarch64-darwin` | macOS (nix-darwin) — [notes](./hosts/Darwin/README.md) |
Shared layers: `home` (home-manager: shell, git, editor),
`modules/common-nixos.nix` (all NixOS hosts: fonts, nix-ld, caches),
`modules/workstation.nix` (physical graphical hosts: audio, thermald,
earlyoom, fwupd), `modules/laptop.nix` (laptops: Wi-Fi, Bluetooth, power,
lid), and `modules/ssh.nix` (key-only sshd). The x86 hosts also pull
`nixos-hardware` profiles.
lid), `modules/desktop.nix` (wired desktops: NetworkManager), and
`modules/ssh.nix` (key-only sshd). The x86 hosts also pull `nixos-hardware`
profiles. The full module catalogue is below.
## Repository layout
```
flake.nix # inputs, mkHost/mkDarwinHost, the host tables, dev shell + checks
flake.lock # pinned input revisions (Renovate keeps this fresh)
modules/ # reusable NixOS system modules (see "Module catalogue")
home/ # home-manager profile: shell, git, editor, claude, desktop, sway
users/ # identity registry + per-user home extras (see "Users")
hosts/<Name>/ # per-machine config: configuration.nix + hardware-configuration.nix
lib/ # small pure helpers (currently the Catppuccin Mocha palette)
.gitea/workflows/ # CI (nix flake check + per-host eval)
statix.toml # lint config (house-style lints disabled)
.editorconfig # base whitespace style
tf-inspect/ # UNRELATED scratch project (gitignored, its own git repo);
# RouterOS / home-services Terraform, not part of this flake
```
Each `nixosConfiguration` / `darwinConfiguration` is assembled in `flake.nix`
from three layers: the shared `baseModules` (or `darwinBaseModules`), the
per-form-factor and `nixos-hardware` modules listed in the host table, and the
per-machine `hosts/<Name>/configuration.nix`. Home-manager is wired in as a
system module; each user's home is composed from the `homeModules` list in that
host's table entry.
## Module catalogue
Reusable NixOS modules under [`modules/`](./modules). "Imported by" says how a
module reaches a host: **baseModules** (every NixOS host, via `flake.nix`),
**host table** (listed explicitly per host in `flake.nix`), or **transitively**
(pulled in by another module's `imports`).
| Module | Imported by | What it does / when to use it |
| ------------------ | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `common-nixos.nix` | baseModules (all NixOS) | Timezone/locale, store hygiene (auto-optimise, big download buffer, **no** auto-GC), the nix-community binary cache, `nix-ld`, base CLI (`git`, `fastfetch`), and the fleet-wide font stack. |
| `users.nix` | baseModules (all NixOS) | Builds `users.users` from the registry for the host's `hostUsers`; enables zsh; enables Firefox + Thunderbird **only** when `features.swayDesktop.enable` is on. Applies per-user `linger`. |
| `features.nix` | baseModules (all NixOS) | Declares feature-flag options (currently `features.swayDesktop.enable`) so any host can read/set them without importing the heavy implementation module. |
| `workstation.nix` | transitively (via laptop/desktop) | Form-factor-agnostic base for physical graphical hosts: turns on `swayDesktop`, Dvorak console, PipeWire, firewall (default-deny), fstrim, earlyoom, fwupd, thermald (x86), redistributable fw. |
| `laptop.nix` | host table (MBP, T400) | `imports` workstation.nix, then adds the portable bits: iwd Wi-Fi, lid suspend/lock, Bluetooth + blueman. |
| `desktop.nix` | host table (Mac Pro) | `imports` workstation.nix, then swaps Wi-Fi for wired NetworkManager. Pair with `portable = false` in the host table. |
| `sway.nix` | host table (graphical hosts) | Implementation of `features.swayDesktop`: the system Sway package, the greetd/ReGreet (cage) greeter forced to Dvorak, xdg-portal, Wayland utility packages. Home-side Sway config is in `home/sway.nix`. |
| `ssh.nix` | host table (T400, Mac Pro, RPi5) | Enables sshd, opens port 22, enforces a key-only policy (no password / keyboard-interactive, no root). Authorized keys come from the registry via `users.nix`. |
| `firmware/` | referenced by MBP host config | Committed Apple peripheral firmware blobs for the Asahi MBP (see "MacBook (Asahi) firmware"). |
Form-factor decision: a **laptop** imports `laptop.nix` (default
`portable = true`); a **wired desktop** imports `desktop.nix` and sets
`portable = false`; a **headless server** imports neither (leaves
`features.swayDesktop.enable` at its default `false`) and adds only what it
serves. `portable` is threaded through to `home/sway.nix`, which drops the
battery block and brightness keys on desktops.
## Users
@@ -38,6 +89,16 @@ Identity is data, kept separate from the reusable modules:
identity into that user's home config as the `identity` module arg. A host can
therefore declare any number of users.
Per-user home extras live under `users/<name>/`:
- [`users/lyrathorpe/home.nix`](./users/lyrathorpe/home.nix) — personal extras
(an ssh host shortcut, gammastep coordinates); imported on Lyra's hosts.
- [`users/emmathorpe/work.nix`](./users/emmathorpe/work.nix) — the work
toolchain (kubectl/helm/az/etc.), work-only LSP servers, and the corporate ssh
handling; imports
[`users/emmathorpe/renovate-review.nix`](./users/emmathorpe/renovate-review.nix),
the daily headless Renovate-PR review timer (EDaaS only).
### Portable home (off-NixOS / external consumers)
The home config is also exposed for use beyond these hosts:
@@ -52,33 +113,6 @@ The home config is also exposed for use beyond these hosts:
(`inputs.<this>.homeModules.default`). Consumers must supply the module args
these expect: `inputs` always, `identity` for git/desktop, `portable` for sway.
## Directory authentication (SSSD → Authentik LDAP)
Every NixOS host authenticates users against the Authentik LDAP outpost via
SSSD, implemented in [`modules/sssd.nix`](./modules/sssd.nix) and enabled by
default through the `services.authentikLdap.enable` option (added to
`baseModules`). The **EDaaS** WSL box opts out
(`services.authentikLdap.enable = false`) as a work-managed environment; the
macOS host is unaffected (SSSD is Linux-only).
- Connects over LDAPS to `ldap.lyrapup.pet:636`, search base
`dc=ldap,dc=goauthentik,dc=io`, binding as
`cn=sssd-bind,ou=users,dc=ldap,dc=goauthentik,dc=io`.
- The schema mappings match Authentik's non-standard object classes
(`goauthentik.io/ldap/user`, `goauthentik.io/ldap/group`) over the POSIX
attributes (`uid`, `uidNumber`, `gidNumber`, `homeDirectory`).
- Home directories are created on first login (`pam_mkhomedir`).
### Secrets (agenix)
The LDAP bind credential is an [agenix](https://github.com/ryantm/agenix)
secret, decrypted at activation with each host's SSH host key. The decrypted
plaintext is a full `sssd.conf` drop-in delivered to
`/etc/sssd/conf.d/01-ldap-authtok.conf`, so the password never enters the Nix
store. Owner setup (host recipient keys, encrypting the bind password, DNS for
`ldap.lyrapup.pet`, rebuild) is documented in
[`secrets/README.md`](./secrets/README.md).
## Applying
```sh
@@ -88,6 +122,36 @@ sudo nixos-rebuild switch --flake .#<configuration>
darwin-rebuild switch --flake .#lyrathorpe-mac
```
On a host whose `networking.hostName` matches its flake attribute (the WSL box
and the Pi are set up this way), `nh os switch` resolves the configuration from
the hostname with no `--flake`/`-H` flag.
## Adding a new host
1. **Create `hosts/<Name>/`.** Add `configuration.nix` with the host-specific
bits only: `networking.hostName`, bootloader (firmware-specific — it is
deliberately not set in the shared modules), and any per-machine hardware
quirks. Keep anything reusable in `modules/` instead.
2. **Hardware config.** Generate `hardware-configuration.nix` on the real
machine with `nixos-generate-config` and commit it. If the machine does not
exist yet, commit a clearly-labelled placeholder so the host still evaluates
in CI (see the existing T400 / RPi5 placeholders), and replace it at install.
These files are excluded from the formatter and linters.
3. **Add a host-table entry in `flake.nix`.** Under `hosts` (NixOS) or
`darwinHosts` (macOS), set `system`, the `modules` list (host config + form
factor + any `nixos-hardware` profiles), and the `users` map (each user's
`homeModules`). Choose the form factor per the decision note above; a headless
host imports neither `laptop.nix` nor `desktop.nix`.
4. **Users.** If the host introduces a new person, add them to
`users/registry.nix` first; otherwise reference an existing username.
5. **Verify.** `nix flake check` formats, lints, and evaluates every host —
including the new one — so a broken entry fails locally before CI. Then
`sudo nixos-rebuild switch --flake .#<configuration>` on the machine.
No change to CI is needed: the host-eval step discovers hosts from the flake
(`attrNames` of the configuration sets), so a new entry is picked up
automatically.
## Shell environment & keybindings
- Interactive shell features (zsh, tmux, git, ssh, CLI tools, auto-tmux):
@@ -138,4 +202,7 @@ A dev shell and a formatting/lint gate are wired through the flake:
[`.gitea/workflows/ci.yaml`](./.gitea/workflows/ci.yaml) runs `nix flake check`
(formatting, `deadnix`, `statix`, the pre-commit hooks) and evaluates every
NixOS and Darwin host configuration on push/PR.
NixOS and Darwin host configuration on push/PR. It always runs (no `paths:`
filter) so the required check never hangs pending; the heavy Nix steps are
skipped when a PR touches no `.nix`/lockfile/workflow file, and the job still
reports green.
Generated
+46 -68
View File
@@ -3,16 +3,16 @@
"brew-src": {
"flake": false,
"locked": {
"lastModified": 1781226006,
"narHash": "sha256-w4ZTuOnhYiDxjaynrMTASzp802QblBWmo3wpB8wVN4Y=",
"lastModified": 1785146564,
"narHash": "sha256-Sa7/HrfB04H32OJ7/ofxXjiZEbkWtCNOriONYYTL1OA=",
"owner": "Homebrew",
"repo": "brew",
"rev": "109191be4988470b51a60a5ef1998520aa24c01b",
"rev": "b2cfc03346d482f79886de108fee5dc49a6efc10",
"type": "github"
},
"original": {
"owner": "Homebrew",
"ref": "6.0.1",
"ref": "6.0.13",
"repo": "brew",
"type": "github"
}
@@ -25,11 +25,11 @@
},
"locked": {
"dir": "pkgs/firefox-addons",
"lastModified": 1782014564,
"narHash": "sha256-F/royQHyJAyKWKrV8AaG4Yf1yjzxa+PFk5xvTdvBrzk=",
"lastModified": 1785643380,
"narHash": "sha256-6LdHFP+av+MSeCWLEl0p7qqD8fBH9pVAfMYHSqbjfA4=",
"owner": "rycee",
"repo": "nur-expressions",
"rev": "d6668e34bbce788459883a1097bf0ee170f49c61",
"rev": "49e519c7b98b21d278be7d72bce2e8ff3b4f3065",
"type": "gitlab"
},
"original": {
@@ -93,11 +93,11 @@
]
},
"locked": {
"lastModified": 1778716662,
"narHash": "sha256-m1Yf0wZ8j1OHjTc2UwHwyQRSnNeSgLJOd7q5Y45hzi4=",
"lastModified": 1785627969,
"narHash": "sha256-4dtXQk/NMePegK/nWp5NSeuZKLATItOq61lpEvmXqGw=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "f7c1a2d347e4c52d5fb8d10cb4d94b5884e546fb",
"rev": "427bf4bd9435fdf21321c8cc628c24efc14c0f7a",
"type": "github"
},
"original": {
@@ -130,17 +130,16 @@
"git-hooks": {
"inputs": {
"flake-compat": "flake-compat",
"gitignore": "gitignore",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1781733627,
"narHash": "sha256-U3yTuGBnmXvXoQI3qkpfEDsn9RovQPAjN7ndRco+3u0=",
"lastModified": 1784288435,
"narHash": "sha256-ReRHaLgr/uVqdD8afFSn+myXIfpHeOhP0yYe0TJqAA8=",
"owner": "cachix",
"repo": "git-hooks.nix",
"rev": "3bbec39bc90eadfa031e6f3b77272f3f60803e39",
"rev": "43b3c1ab9d40fb1dbb008f451988a91e375825e9",
"type": "github"
},
"original": {
@@ -149,27 +148,6 @@
"type": "github"
}
},
"gitignore": {
"inputs": {
"nixpkgs": [
"git-hooks",
"nixpkgs"
]
},
"locked": {
"lastModified": 1709087332,
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "gitignore.nix",
"type": "github"
}
},
"home-manager": {
"inputs": {
"nixpkgs": [
@@ -177,11 +155,11 @@
]
},
"locked": {
"lastModified": 1781981105,
"narHash": "sha256-/1nNBbA7PrSQpTc9Qazkhl4kIPg+TNl0CjxS3UQJKlw=",
"lastModified": 1785119570,
"narHash": "sha256-Rgs2xKnGLFWQscxUaXX07oyZeuMDOHEbqDOsgliLFGM=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "7bfff44b465909f69a442701293bc0badcf476dc",
"rev": "d4fd24667c8cbef124bb70a20380cab75ec8474d",
"type": "github"
},
"original": {
@@ -214,11 +192,11 @@
]
},
"locked": {
"lastModified": 1781772065,
"narHash": "sha256-xIbRSwDB1GBAUsWsQZUjudGfAGQt3BOpsWaO/ugVa4w=",
"lastModified": 1783744694,
"narHash": "sha256-2cp6N3rrwnGYLTx9l6N+NI+kwrCWxvJUbj5WJhvB29A=",
"owner": "nix-darwin",
"repo": "nix-darwin",
"rev": "adda04f0bf4819575b1978c2f8d78401b3c2be12",
"rev": "c3e90c89649b07d1a96e4b9dd6cd0d6e44b91a74",
"type": "github"
},
"original": {
@@ -233,11 +211,11 @@
"brew-src": "brew-src"
},
"locked": {
"lastModified": 1781389246,
"narHash": "sha256-ORqLAo/hoJdsZC7UPAuEHev6S0+XIqKEC7vjo5prz1k=",
"lastModified": 1785544760,
"narHash": "sha256-qV6OoNuly4ntqpCg7esIeJjUboxSnQNlLPxz+y5h9/o=",
"owner": "zhaofengli",
"repo": "nix-homebrew",
"rev": "de7953a08ed4bb9245be043e468561c17b89130d",
"rev": "937ce52c7d046310571f3a070713804ead496843",
"type": "github"
},
"original": {
@@ -253,11 +231,11 @@
]
},
"locked": {
"lastModified": 1782030356,
"narHash": "sha256-h4WpMr455AfRub0FXBaon6Vcpe0waUyJ4GivIW6oyd4=",
"lastModified": 1785650611,
"narHash": "sha256-q4kR7g+pCcz6NASvoVPYu+CWWUurX03wogtBqGmR4h0=",
"owner": "nix-community",
"repo": "nix-index-database",
"rev": "3017088b49efd404f78e3b104f553b97e4af786b",
"rev": "dbc756c9d7287de19b3e0e38c928c47510d42c3e",
"type": "github"
},
"original": {
@@ -274,11 +252,11 @@
]
},
"locked": {
"lastModified": 1781520503,
"narHash": "sha256-XuqQQG1qRyc3o8ld937sDLQNx+QrGV852KJ0dNglJDg=",
"lastModified": 1785426242,
"narHash": "sha256-QHAP8KsJQmI+dpNS/wfWAtEBQ0Zby+B0Ty+qZIO/U2w=",
"owner": "nix-community",
"repo": "nixos-apple-silicon",
"rev": "43043ad207529650f9fa68e1705f7cf9c08bfdeb",
"rev": "66d8dd2c27f99bd5420c99938b60695aac1785c4",
"type": "github"
},
"original": {
@@ -294,11 +272,11 @@
]
},
"locked": {
"lastModified": 1781622756,
"narHash": "sha256-JrPh4M6S7aPsEE9tOENuZrxC6o2szSLlK+t4+nLke9s=",
"lastModified": 1785232496,
"narHash": "sha256-65EQYIRRpTdpH8lUiB6Mvo5uBkG60aBIzAJuALfx+O0=",
"owner": "NixOS",
"repo": "nixos-hardware",
"rev": "08018c72174a4df5657f8d94178ac69fb9c243e5",
"rev": "2e790b0a6be8ec2b76174ac0931b8ff11919ec98",
"type": "github"
},
"original": {
@@ -315,11 +293,11 @@
]
},
"locked": {
"lastModified": 1781182279,
"narHash": "sha256-V5EQQbDnmdiXGQXrEF1PEL7QYsFqfH8N1E89Z5ONwFk=",
"lastModified": 1784642409,
"narHash": "sha256-hcbDqFuySAJawljt5r0sKBCJKYnbtGD0T/ZIozH1Dq0=",
"owner": "nix-community",
"repo": "NixOS-WSL",
"rev": "5675822ba756e6e56f8f6a5a76e90e0da2ece94d",
"rev": "eaeb18da90024448a60eb1ec7132eafa4003404e",
"type": "github"
},
"original": {
@@ -330,11 +308,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1781216227,
"narHash": "sha256-9mUW6gNwoN2SWc/l0fW4svPNOulXLl8ijqKyeSOGgJE=",
"lastModified": 1785599192,
"narHash": "sha256-dg4RTtDxnXY13UkJNdhmgTUTl0n/IJBlCigfO7nutZw=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "a0374025a863d007d98e3297f6aa46cc3141c2f0",
"rev": "6d65bfc1bcef2ef39a239d38e577e92a89fb0f07",
"type": "github"
},
"original": {
@@ -346,11 +324,11 @@
},
"nixpkgs-unstable": {
"locked": {
"lastModified": 1781577229,
"narHash": "sha256-lrp67w8AulE9Ks53n27I45ADSzbOCn4H+CNW1Ck8B+8=",
"lastModified": 1785571196,
"narHash": "sha256-KoTsyMQqnXQZq8deCEnu4QkyldkwH/bpMMhUcfMdGIw=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "567a49d1913ce81ac6e9582e3553dd90a955875f",
"rev": "148bab9c1c3c53136ecb44a6ea356a0ed5b39b06",
"type": "github"
},
"original": {
@@ -369,11 +347,11 @@
"systems": "systems"
},
"locked": {
"lastModified": 1781971008,
"narHash": "sha256-T2u2RQZWKvD1J+TgcxjiJr8IymBr/PrUNeAGhMZFZU4=",
"lastModified": 1782919967,
"narHash": "sha256-pRwjfB5HQJ3m8J8bOR43pPHtHI7VUJSqwLA3P06cOY0=",
"owner": "nix-community",
"repo": "nixvim",
"rev": "7afca458f064f166d3a9c98db3b41a984fe46492",
"rev": "667c8471f4a0fb24d702d1a61af8609f1a5f1ba6",
"type": "github"
},
"original": {
@@ -424,11 +402,11 @@
]
},
"locked": {
"lastModified": 1780220602,
"narHash": "sha256-eynAfOmbmxJnkp7YewvCEbShNnnYJ9gLLqkzsYtBPeM=",
"lastModified": 1785360170,
"narHash": "sha256-XE1lKgQ3eIO3E7zWryqcRsax+mYXod/5RHBn4YaR9YE=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "db947814a175b7ca6ded66e21383d938df01c227",
"rev": "d1187f8bc71fb8aab02395869ec3f5c1920f75c0",
"type": "github"
},
"original": {
+10 -11
View File
@@ -46,15 +46,6 @@
url = "github:cachix/git-hooks.nix";
inputs.nixpkgs.follows = "nixpkgs";
};
# agenix: age-encrypted secrets, decrypted at activation with each host's
# SSH host key. Provides the SSSD LDAP bind credential (secrets/, see
# modules/sssd.nix). The darwin module is intentionally unused (SSSD is
# Linux-only).
agenix = {
url = "github:ryantm/agenix";
inputs.nixpkgs.follows = "nixpkgs";
inputs.home-manager.follows = "home-manager";
};
# Declarative Neovim (the editor; see home/editor.nix). Release
# branch matched to the pinned nixpkgs (26.05); follows our nixpkgs to keep a
# single nixpkgs in the closure. editor.nix sets programs.nixvim.nixpkgs.source
@@ -104,6 +95,16 @@
claude-code
;
})
# commitizen 4.13.9's regression test for the invalid-command error
# message asserts argparse's older, unquoted "invalid choice" wording;
# the argparse in Python 3.13 quotes each choice, so the fixture no
# longer matches and the checkPhase fails. The package itself is fine
# -- deselect just that test. Drop once nixpkgs updates the fixture.
(_final: prev: {
commitizen = prev.commitizen.overridePythonAttrs (old: {
disabledTests = (old.disabledTests or [ ]) ++ [ "test_invalid_command" ];
});
})
];
# Unfree packages permitted to be built (replaces blanket allowUnfree).
@@ -132,9 +133,7 @@
./modules/users.nix
./modules/common-nixos.nix
./modules/features.nix
./modules/sssd.nix
commonModule
inputs.agenix.nixosModules.default
home-manager.nixosModules.home-manager
{
home-manager.useGlobalPkgs = true;
+8 -3
View File
@@ -1,11 +1,16 @@
- [User name](user_name.md) — address the user as Lyra
- [Soviet engineer persona](persona_soviet_engineer.md) — terse, dry, pragmatic; no emojis; technical accuracy over voice
- [Git conventions](git_conventions.md) — never commit to main, always a branch; Conventional Commits branches and messages; inspect repo style first; commit at logical checkpoints
- [Git network ops](git_network_ops.md) — GitHub pushable in-sandbox (agent key; just sandbox off); Gitea code.emmathe.dev needs hand-off
- [Git commit signing](git_commit_signing.md) — signs in-sandbox via ssh-agent (allowAllUnixSockets + inlined pubkey)
- [Git conventions](git_conventions.md) — never commit to main, always a branch; EVERY commit is `type(<TICKET-ID>): summary` using the live ticket, overrides repo's bare-prefix style; watch for scope decay on follow-up commits; grep to verify before pushing
- [Git network ops](git_network_ops.md) — GitHub and Gitea (code.emmathe.dev) both pushable in-sandbox (sandbox off, agent key); raise Gitea PRs via tea CLI
- [Git commit signing](git_commit_signing.md) — signs in-sandbox via ssh-agent (allowAllUnixSockets + inlined pubkey); sig=N without allowedSignersFile is cosmetic, still signed
- [Git check state first](git_check_state.md) — always check branch/status/divergence before git work; Lyra edits repos between sessions
- [Keep docs updated](docs_keep_updated.md) — update docs in the same pass as code/config changes; stale docs are a defect
- [SIBO Workabout MX project](sibo_workabout_mx_scanner.md) — RE + barcode-inventory project state; scanner is an OO DYL object (oscanner), blocked on on-device ordinal capture; resume via code/inventory/CONTINUATION.md
- [Jira tooling](jira_tooling.md) — comments are Markdown not wiki; transitions may need assignee; link direction; WSP transition IDs
- [Jira WSP fields](jira_wsp_fields.md) — WSP field map: issue-type IDs, required Bug fields with allowed values/IDs, Task shortcut, relevant components
- [Review and comments workflow](workflow_review_and_comments.md) — show PR body and non-trivial Jira comments before posting; terse IaC code comments; PR body content rules
- [Code comment style](code_comment_style.md) — reviewer feedback: no ticket IDs in comments by default, concise, explain non-obvious why; Helm needs `#` not `{{/* */}}` to render
- [Copilot review false positives](copilot_review_false_positives.md) — verify Copilot "this breaks X" claims against spec/live config before acting; two recorded Terraform false positives
- [Sandbox prompts](feedback_sandbox_prompts.md) — don't prompt for sandbox-disable or routine read-only shell ops; broaden permissions instead
- [Dev clusters disposable](dev_clusters_disposable.md) — Lyra's dev clusters are recreatable; mutate/break freely, no confirmation needed
- [Nix shell tooling](nix_shell_tooling.md) — any nixpkgs tool runs ad hoc via `nix run`/`nix shell nixpkgs#<pkg>`; a missing command is never a dead end
+19
View File
@@ -0,0 +1,19 @@
---
name: code_comment_style
description: "Code/comment style from PR review feedback: no ticket IDs in comments by default, concise, explain the non-obvious why"
metadata:
node_type: memory
type: feedback
originSessionId: 59e09a3f-1429-4f68-a5fb-9af4390e9b0d
---
Recurring PR-review feedback from human reviewers (Tom Wilkins, Andrew Hyde, Gilberto Pestanarosa) on the `multicluster` and `unified-helm` repos, on how to write comments in code and IaC:
- **No Jira/WSP ticket IDs in code comments or WAF `msg:` strings by default.** Add a ticket ref only when there is a specific reason to. Never duplicate the id, and never put a ticket URL in a comment. Tracking/rationale belongs in the PR description and the Jira ticket, not in `.tf`, `.tftpl`, or `.yaml`. (Flagged repeatedly — PRs #1735, #1762.)
- **Comment the non-obvious "why", not the obvious "what".** Drop comments that restate what the code or file plainly does (e.g. a header on `namespace.yaml` re-announcing that it defines a namespace). If a reviewer can't tell why a comment exists, it shouldn't.
- **Keep it short and readable.** No multi-line block where one line does; if a comment isn't clear after a couple of reads, rewrite it plainer. Prefer trimming to the single load-bearing sentence over hedged prose. (PRs #1745, #216.)
- **In Helm charts, use `#` YAML comments — not `{{/* */}}` — for anything that must appear in the rendered manifest.** Helm template comments are stripped before render, so port/label explanations meant for the live chart have to be `#`. (PR #216.)
**Why:** Multiple human reviewers, across multiple PRs, consistently push back on verbose comments and gratuitous ticket references. Terse, purpose-driven comments clear review faster.
**How to apply:** When writing or editing comments in code/IaC, default to: no ticket id, one line, non-obvious "why" only. This supersedes the "one-liner + WSP ticket reference" phrasing that used to live in [[workflow-review-and-comments]]. Relates to [[docs_keep_updated]].
@@ -0,0 +1,19 @@
---
name: copilot_review_false_positives
description: "Verify Copilot PR-review 'this breaks X' claims against spec/live config before acting; two recorded false positives"
metadata:
node_type: memory
type: feedback
originSessionId: 59e09a3f-1429-4f68-a5fb-9af4390e9b0d
---
The Copilot reviewer on the `multicluster` / `unified-helm` repos raises blocking-sounding "this will fail" claims that are sometimes wrong. Verify against the language spec and the live/`master` config before treating one as real or applying its fix.
Recorded false positives (both Terraform, both Emma-flagged "for future reference"):
- **PR #1742** — claimed `var.map.hyphenated-key` dot access is parsed as subtraction and breaks `terraform plan`. False: HCL2 identifiers may contain hyphens (`ID_Start (ID_Continue | "-")*`), and the same pattern is already live on `master` in prod. Bracket indexing was adopted anyway as marginally clearer, not as a fix.
- **PR #1745** — claimed the `aks_pools` per-pool `max_surge` lookup was off-by-one and should use `count.index + 1`. False: every config attribute on that resource indexes with `count.index`; only the cosmetic `name`/`az_nodepool` label uses `+1`. Applying `+1` would have introduced a real bug (wrong pool, and out-of-bounds on the last pool).
**Why:** Blindly applying a plausible-but-wrong Copilot suggestion can introduce a real defect or waste review cycles.
**How to apply:** For any Copilot claim that code is broken or unsafe, confirm it against the relevant spec and the existing working config first; if it's wrong, say so plainly on the PR and leave the code. Genuine Copilot catches (over-broad WAF `@beginsWith`, missing input validation, doc/behaviour drift) still get fixed. Relates to [[workflow-review-and-comments]] and [[code_comment_style]].
+3 -1
View File
@@ -1,6 +1,6 @@
---
name: git-commit-signing
description: "Commits sign in-sandbox via ssh-agent — needs `allowAllUnixSockets: true` in settings, plus pubkey inlined in user.signingkey."
description: "Commits sign in-sandbox via ssh-agent (allowAllUnixSockets + inlined pubkey); local verify shows sig=N without an allowedSignersFile but the commit IS signed."
metadata:
node_type: memory
type: feedback
@@ -19,4 +19,6 @@ Lyra's git is configured to SSH-sign commits (`commit.gpgsign=true`, `gpg.format
**How to apply:** Commit normally with `git commit`. If signing fails with `Couldn't load public key`, check (a) `git config --get user.signingkey` starts with `key::ssh-ed25519 AAAA...` (not literal `$(...)`), (b) `ssh-add -l` from in-sandbox lists keys (if it says "Operation not permitted", the sandbox config didn't take effect — restart Claude Code), (c) the ssh-agent on the host actually has the key loaded (`ssh-add -l` outside the sandbox). Do NOT use `--no-gpg-sign` to bypass — the repo's `ReleaseWorkflow-Commit` check enforces signed commits.
**Verifying — the recurring trap:** `git log --show-signature` and the `%G?` format both report `N` and print `error: gpg.ssh.allowedSignersFile needs to be configured and exist for ssh signature verification`. This does NOT mean the commit is unsigned — it means git has no local allowed-signers file to check it against. The signature is present. Confirm the real state with `git cat-file commit <ref> | grep -i '^gpgsig'`: an `-----BEGIN SSH SIGNATURE-----` block means signed. So `N` here is cosmetic, not a signing failure — do not "fix" it by re-committing. To make local verification actually pass, set `gpg.ssh.allowedSignersFile` to a file mapping the signer to the pubkey (a line like `emma.thorpe@cloud.com ssh-ed25519 AAAA...`); Gitea/CI verifies server-side regardless.
Related: [[git-network-ops]], [[git-conventions]].
+29 -3
View File
@@ -11,8 +11,34 @@ metadata:
**Branch naming:** Follow the repo's existing convention — inspect with `git branch -a` or `git for-each-ref` before creating. Prefer Conventional Commits prefixes (`feat/`, `fix/`, `chore/`, `docs/`, `refactor/`). Format: `<prefix>/<TICKET-ID>-<kebab-summary>`. Only ask if no convention is discoverable.
**Commit messages:** Conventional Commits. Subject line: `<type>(<TICKET-ID>): <imperative summary>` ticket ID as the scope. Use additional `-m` flags for rationale/body. Commit at logical checkpoints, not one giant final commit.
**Commit messages — every commit, without exception:** `<type>(<TICKET-ID>): <imperative summary>`. The ticket ID goes in the scope. Use additional `-m` flags for rationale/body. Commit at logical checkpoints, not one giant final commit.
**Why:** Lyra's standard workflow for traceability and clean history.
**`<TICKET-ID>` is the real ticket for the work in hand.** It is a symbol to substitute, never a literal — if a commit subject ever reaches git still containing `<TICKET-ID>`, or a made-up number, that is a defect. Establish the actual ID before the first commit, in this order:
**How to apply:** Whenever creating a branch or committing in any repo. Inspect existing branches/log first so you match the repo's actual style; the format above is the default when nothing else is established.
1. The ticket Lyra named in the request.
2. The current branch name — `task/WSP-32542/remove-wspgov-terraform` gives `WSP-32542`. Extract it: `git branch --show-current | grep -oE '[A-Z]{2,}-[0-9]+'`.
3. The ticket the branch's existing commits already use.
If none of those yield an ID, ask which ticket to file the work under. Do not guess, do not reuse the ID from an unrelated earlier task in the session, and do not invent a plausible-looking number. Every commit in a branch normally carries the same ID; if the work genuinely spans two tickets, split the commits accordingly rather than picking one at random.
**Exception — repos with no issue tracker.** Personal repos such as `nixfiles` have no Jira project. There the scope is the area of the change, not a ticket: `chore(claude): ...`, `chore(deps): ...`, `feat(hosts): ...`. Conventional form is still required; only the ticket scope is dropped. Never invent a WSP number to satisfy the rule in a repo that has no tickets. The ticket requirement applies to the work repos under `~/code` that are backed by the WSP Jira project and gated by CI.
**This format is mandatory and overrides the repo's existing log style.** Many repos (`multicluster`, `core-services-cloud`) have histories full of bare `<TICKET-ID>: summary` subjects written by other people. Do not copy that. Match repo style for _branch names_ only; commit subjects are always full Conventional Commits with the ticket scope. CI enforces this, and a failure means Lyra rebases the history by hand.
**Known failure mode — scope decay across a session.** The first commit gets `fix(<TICKET-ID>): ...` correctly, then follow-up commits in the same sitting degrade to bare `test: add tests for class`, `refactor: hoist middleware`, `chore: tidy`. This has caused real rebase work in `core-services-cloud`. The second, third and fifth commits need the ticket scope exactly as much as the first. Re-read the subject against the format before every single `git commit`.
**Merge commits count too.** Prefer `git rebase origin/<base>` over `git merge` so none is created. If unavoidable, set the message explicitly: `git merge --no-ff -m "<TICKET-ID>: merge master into <branch>"`. Keep the ID uppercase; the check is case-sensitive.
**Before pushing, verify — do not skip this:**
```
git log --format=%s origin/<base>..HEAD | grep -vE '^[a-z]+(\([A-Z]{2,}-[0-9]+\))!?: '
```
Must print nothing. Writing each subject carefully is not a substitute for running it.
**Auditing past behaviour is unreliable.** If Lyra has already rebased to fix a bad subject, the log shows her corrected version, not what was originally written. A clean `git log` is not evidence that nothing was wrong. Check author date vs committer date (`--format="%ad %cd"`) — a mismatch means history was rewritten. Never argue from a clean log that the fault did not occur.
**Why:** Lyra's standard workflow for traceability, and a hard CI gate. A malformed subject is manual rebase work for her, not just a red build.
**How to apply:** Conventional form on every commit in every repo; the ticket scope additionally on every commit in a Jira-backed work repo. Format first, repo style second. Run the verification grep before every push. Relates to [[git_check_state]].
+3 -3
View File
@@ -1,6 +1,6 @@
---
name: git-network-ops
description: Push/pull is remote-specific — GitHub is agent-pushable in-sandbox; Gitea (code.emmathe.dev) needs hand-off to Lyra.
description: Push/pull is remote-specific — both GitHub and Gitea (code.emmathe.dev) are agent-pushable in-sandbox (sandbox off); raise Gitea PRs with the tea CLI.
metadata:
node_type: memory
type: feedback
@@ -11,8 +11,8 @@ Whether a network op can run depends on which key the remote needs:
**GitHub remotes (e.g. csg-citrix-storefront/\*): pushable in-sandbox by the agent.** ssh-agent holds the decrypted `~/.ssh/id_ed25519` (`emma.thorpe@cloud.com`), which is authorized on GitHub. Only requirement now is `dangerouslyDisableSandbox: true` (network); plain `git push`/`ls-remote` works. Probe non-mutatively with `git ls-remote` first. (Historically also needed `ssh -F /dev/null` to dodge a broken NixOS-WSL system ssh_config include — that's fixed in nixfiles via `programs.ssh.systemd-ssh-proxy.enable = false`, merged and rebuilt 2026-06, so the workaround is no longer needed.)
**Gitea (`code.emmathe.dev`, e.g. nixfiles): hand off to Lyra.** Needs `~/.ssh/code.emmathe.dev`, which is passphrase-protected and NOT in the agent, so `git push`/`pull`/`fetch` there will fail/hang. Pause, give Lyra the exact command (she runs `ssh-add ~/.ssh/code.emmathe.dev` once, then pushes).
**Gitea (`code.emmathe.dev`, e.g. nixfiles): pushable in-sandbox by the agent (as of 2026-07-14).** The ssh-agent now holds the `code.emmathe.dev` key (`git@code.emmathe.dev`), so `git push` works with `dangerouslyDisableSandbox: true` — it needs the agent socket plus `~/.ssh/known_hosts`, both reachable with sandbox off. Probe with `git ls-remote` first. Raise PRs with the `tea` CLI, which is installed and logged in to `code.emmathe.dev` (user `lyrathorpe`): `tea pr create --login code.emmathe.dev --repo lyrathorpe/nixfiles --base main --head <branch> --title "..." --description "..."`. Only fall back to hand-off if `ssh-add -l` (sandbox off) does NOT list the `code.emmathe.dev` key — then it dropped from the agent and Lyra must re-add it (`ssh-add ~/.ssh/code.emmathe.dev`, passphrase-protected).
**Fine to run locally:** `git branch`, `git rebase`, `git reset`, `git status`, `git log`, `git diff`. `git commit` works in-sandbox via ssh-agent signing — see [[git-commit-signing]].
**How to apply:** Check the remote host before a network op. GitHub → just do it (sandbox off). Gitea → hand off. Related: [[git-conventions]].
**How to apply:** Both remotes → do it with sandbox off; probe with `git ls-remote` first, and raise Gitea PRs via `tea`. Hand off only if the Gitea key is missing from the agent. Related: [[git-conventions]].
+4
View File
@@ -9,6 +9,10 @@ metadata:
**Transitions:** `transitionJiraIssue` may fail if the issue lacks an assignee. Set assignee first via `editJiraIssue` when a transition errors on assignee requirement.
**Transition required fields (WSP):** the same target status can enforce different required fields per issue type — e.g. `Cancelled` on a Story requires `Resolution` + `Justification`, but on an Epic requires neither (so an Epic can land in Cancelled while still reading Unresolved). Fetch requirements with `getTransitionsForJiraIssue` + `expand=transitions.fields` before transitioning. Cancel/won't-do resolution values: `Won't Fix` (10068), `Canceled` (10070), `Obsolete` (10073 — use for superseded-by-another-ticket).
**ADF-only custom fields:** the WSP `Justification` field (`customfield_10070`) advertises schema `textarea` (string) but the API rejects a plain string — it requires an Atlassian Document Format object (`{type:"doc",version:1,content:[...]}`). If a transition/edit errors with "Operation value must be an Atlassian Document", wrap the text in ADF.
**Issue link direction:** For `createIssueLink`, "X is blocked by Y" means `inwardIssue=Y` (the blocker), `outwardIssue=X` (the blocked), `type.name="Blocks"`. Inward = the side the link points _from_; outward = the side it points _to_.
**WSP project transition IDs:**
+32
View File
@@ -0,0 +1,32 @@
---
name: jira-wsp-fields
description: WSP Jira project field map — issue-type IDs, required Bug fields with allowed values/IDs, and the Task shortcut for fast ticket creation
metadata:
type: reference
---
Field map for the **WSP (Workspace Platform)** Jira project, to create tickets without trial-and-error. Site `citrix.atlassian.net`, cloudId `70cbc59a-06d2-4508-a9a6-61f1dbc2057f`, project key `WSP`, project id `10061`. See also [[jira-tooling]].
**Issue-type IDs:** Epic `10000`, Story `10004`, Task `10008`, Bug `10123`, Sub-task `10009`.
**Fast path — use Task, not Bug.** A `Task` requires only `summary` (project/issuetype auto, reporter defaults to caller). A `Bug` requires six extra fields (below), so only pick Bug when it must be a Bug. The sibling infra/remediation tickets in WSP are Tasks.
**Bug required fields** (enforced by the create validator; note `createmeta` omits `versions` but the API rejects without it):
| Field | Key | Shape | Allowed values (value = id) |
| ------------------- | ------------------- | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| Severity | `customfield_10061` | `{"value":"S2"}` | S1=10643, S2=10644, S3=10645, S4=10646 |
| Affects Environment | `customfield_10116` | `{"value":"Production"}` | Production=11031, Staging=11032, Integration=11033, Development=11034, Test=11035 |
| Defect Source | `customfield_10138` | `{"value":"Internal - Manual"}` | Internal - Manual=12699, Internal - Automation=12700, Customer=12702, Security Review=12704 |
| Regression | `customfield_10141` | `{"value":"No"}` | Yes - Previous Build=12705, Yes - Previous Release=12706, No=12707 |
| Components | `components` | `[{"name":"Workspace Configuration"}]` | 109 options; relevant ones below |
| Affects versions | `versions` | `[{"name":"<version>"}]` | not in requiredFieldsOnly createmeta — fetch current list from full createmeta or project versions before setting |
Select customfields (`...customfieldtypes:select`) accept `{"value":"..."}` or `{"id":"..."}`. Components/versions accept `[{"name":...}]` or `[{"id":...}]`.
**Relevant Components (name=id):** Workspace Configuration=12052, Workspace-Platform=12060, Multicluster Platform=12023, WSP Core Ingress=12037, Microservice Infrastructure=12020, Infrastructure=34375, Custom Domain Proxy=12021, Custom Domain Ingress Manager=11968, Custom Domain Infrastructure=11975, StoreFrontConfiguration=12042, StoreFront=12050, Test Infrastructure=12012, WSP Release Infrastructure=12011.
**Other create notes:** pass `description`/`commentBody` with `contentFormat: markdown`; set labels via `additional_fields {"labels":[...]}`; attach to an epic with the top-level `parent` param (`parent: "WSP-32494"` works for epic→Task). WSP transition IDs live in [[jira-tooling]].
Example Bug `additional_fields`:
`{"customfield_10061":{"value":"S2"},"customfield_10116":{"value":"Production"},"customfield_10138":{"value":"Internal - Manual"},"customfield_10141":{"value":"No"},"components":[{"name":"Workspace Configuration"}],"versions":[{"name":"<version>"}],"labels":["..."]}`
+23
View File
@@ -0,0 +1,23 @@
---
name: nix-shell-tooling
description: "Any nixpkgs tool can be run ad hoc via nix run / nix shell — a missing command is never a dead end during development"
metadata:
node_type: memory
type: feedback
originSessionId: dfb56b58-518b-4daf-b531-7119bb4a9534
---
Any tool in nixpkgs can be run without installing it into the environment. If a
command is missing during development, pull it from nixpkgs on the fly instead
of working around its absence or reporting the tool as unavailable.
**Why:** Lyra runs NixOS; the ambient PATH is deliberately minimal, but the full
nixpkgs set is always one command away. "command not found" is not a blocker.
**How to apply:**
- One-off run: `nix run nixpkgs#<pkg> -- <args>` (e.g. `nix run nixpkgs#jq -- .`).
- Tools on PATH for a session: `nix shell nixpkgs#<pkg> [nixpkgs#<pkg2> ...]`,
then run commands normally.
- Legacy form also works: `nix-shell -p <pkg> --run '<cmd>'`.
- Prefer this over hand-rolling a substitute for a tool that exists in nixpkgs.
@@ -0,0 +1,25 @@
---
name: sibo-workabout-mx-scanner
description: State of the Psion Workabout MX reverse-engineering / barcode-inventory project and how to resume it
metadata:
node_type: memory
type: project
originSessionId: 74de014e-9cf4-47f6-92f4-c34197ac1858
---
Long-running project (July 2026) reverse-engineering the **Psion Workabout MX** (SIBO OS, NEC V30MX, TopSpeed C) to build a barcode **inventory demo** (scan UPC → DBF database file; add stock, consume by a quantity unit) and, alongside, **complete device programming documentation**. Repo: Gitea **lyrathorpe/sibo-playground**, working branch **`feat/inventory-phase1-scan`** (unmerged). Gitea needs hand-off / the contents API for pushes — see [[git-network-ops]]; [[git-conventions]] for branch/PR rules.
**Committed on the branch (durable, survive reboot):**
- `docs/reference/00-08` + index — the SIBO/MX programming reference (building apps, system/OS, I/O devices, PLIB core, file system & DBF, UI, hardware, and RE'd boot/OS-call internals).
- `code/inventory/` — app scaffold: `upc.c/.h` (UPC-A check-digit validation, correct), `bcode.c/.h`, `scan.c` (Phase-1 diagnostics), `README.md`, **`SCANNER-API.md`** (all scanner findings), **`CONTINUATION.md`** (the on-device debugging procedure to finish).
- `docs/mx-re/toolchain-and-plan.md` — the RE toolchain.
- The **ROM `w2mx_v7.20f_eng.bin`** and the full **SDK + HDK** (manuals as `docs/*.txt`; headers/libs/`bar*.ldd` under `code/SIBOSDK/`; HDK under `code/HDK/`) are on the branch. `/tmp/claude/sibo/` working files (ROM slices, MAME rom dir, Ghidra/decomp output) are transient and reproducible from the toolchain doc.
**Scanner — key result:** the integral laser is driven as an **OO library object** in `SCANNER.DYL` (category token **`oscanner`**) via `p_getlibh``p_newsend`/`f_newsend``p_send`, over **LIBMANAGER (INT 0x84)** / **MESSMANAGER (INT 0x83)** — NOT raw device I/O. Confirmed on the physical device: `p_open("WL2:D")` + control ops **6** then **7** (`p_iow(chan,6); p_iow(chan,7)`) fire the laser to a good decode (green LED). Default Symbol2 11-byte param block: `04 3f 01 15 06 04 1e 80 0d 0a 06` (decoded output is CR/LF-terminated). Dead ends (do not retry): raw `TTY:D` reads, and the wand `BAR:` / `bar*.ldd` decoders (probe expansion slots → `-41`).
**Blocked on / next step:** the OO **message ordinals + parameter structs** for init / set-params / trigger / read. OLIB assigns ordinals dynamically across the class hierarchy (base classes in `olib`/`hwim`), so they resolve only at runtime — capture them with the **SIBO Debugger on the physical device** (remote debug over serial; it supports breakpoints inside DYLs). MAME cannot inject a barcode, so the last mile must be on hardware. Full step-by-step is in `code/inventory/CONTINUATION.md`.
**RE toolchain (reproducible):** the ROM is MAME machine **`psionwamx`**; run its debugger headless via `xvfb-run -a mame psionwamx -rompath roms -debug -debugscript CMDS -sound none -seconds_to_run N` (MAME lua input injection into the keyboard matrix does NOT work headless — a known limitation). Static: **radare2** (16-bit x86). Decompile: **Ghidra headless** (processor `x86:LE:16:Real Mode`, a Java GhidraScript — Ghidra 12 has no bundled Python). Get MAME/radare2/Ghidra via `nix-shell -p ...`. Details in `docs/mx-re/toolchain-and-plan.md`.
**Fallback to deliver value now:** Phase 2 (the DBF inventory: add stock, consume by quantity) can be built with keyboard UPC entry against `docs/reference/05-filesystem-dbf.md`, dropping the scanner in behind the same interface once retrieval is finished. [[docs-keep-updated]]
@@ -11,7 +11,7 @@ metadata:
**Show non-trivial Jira comments before posting:** Same rule for any non-trivial public Jira comment — paste the proposed body in chat first when there is any doubt about content.
**Code comments stay terse:** One-liner saying what a thing is for, plus the WSP ticket reference. Full rationale lives in the Jira ticket or commit/PR description not in `.tf`, `.tftpl`, or `.yaml` files. See [[git-conventions]].
**Code comments stay terse:** One line on the non-obvious _why_, and **no Jira/WSP ticket id by default** — add one only when specifically warranted. Full rationale lives in the Jira ticket or commit/PR description, not in `.tf`, `.tftpl`, or `.yaml` files. Reviewers repeatedly strip gratuitous ticket refs and verbose comments; see [[code_comment_style]] for the full rule set and [[git-conventions]].
**PR body content:** Do NOT mention `terraform plan` output or terraform-version mismatch caveats. Stick to: what changed, why, and validation results.
@@ -19,4 +19,4 @@ metadata:
**Why:** Lyra reviews everything Claude publishes externally before it goes out; terraform-version noise in PR descriptions is unhelpful clutter.
**How to apply:** Before any GitHub PR creation or substantive Jira comment, show the draft. When writing code comments in IaC files, keep to one-liner + ticket ref.
**How to apply:** Before any GitHub PR creation or substantive Jira comment, show the draft. When writing code comments in IaC files, keep to a one-line non-obvious _why_ with no ticket id by default ([[code_comment_style]]).
+1
View File
@@ -19,6 +19,7 @@
pkgs.element-desktop
pkgs.legcord
pkgs.nemo # file manager (launched via Mod+e, see ./sway.nix)
pkgs.darktable
#pkgs.plex-desktop
#pkgs.plexamp
];
+11
View File
@@ -96,6 +96,15 @@ in
# runs before oh-my-zsh/compinit so the exec replaces the shell before
# that setup is wasted. Guards, each preventing a real breakage:
# interactive only -> don't hijack scp / `ssh host cmd` / scripted shells
# stdout is a tty -> VS Code (macOS) resolves the shell environment on
# startup by running an interactive login shell with
# stdout piped, no controlling terminal. Without this
# guard `exec tmux` runs there, fails ("open terminal
# failed: not a terminal"), exits non-zero, and VS
# Code reports "Unable to resolve your shell
# environment". A real terminal always has a tty here.
# not VS Code env -> also skip VS Code's env-resolution probe explicitly,
# in case a future version allocates a pty for it.
# $TMUX empty -> a pane's zsh won't re-exec tmux (infinite loop)
# not SSH -> don't force inbound SSH logins into a server tmux
# not VS Code -> its integrated terminal manages itself
@@ -103,6 +112,8 @@ in
# $NO_TMUX unset -> escape hatch: `NO_TMUX=1 <term>` opens a bare shell
(lib.mkOrder 200 ''
if [[ $- == *i* ]] \
&& [[ -t 1 ]] \
&& [[ -z "$VSCODE_RESOLVING_ENVIRONMENT" ]] \
&& [[ -z "$TMUX" ]] \
&& [[ -z "$NO_TMUX" ]] \
&& [[ -z "$SSH_CONNECTION" && -z "$SSH_TTY" ]] \
+51
View File
@@ -0,0 +1,51 @@
# macOS (nix-darwin) — `lyrathorpe-mac`
Flake host: `lyrathorpe-mac` (`aarch64-darwin`). Apple Silicon Mac managed by
**nix-darwin** from this same flake. Built via `mkDarwinHost` (single-user —
macOS owns the account; identity still comes from the registry). Files:
`configuration.nix`.
## What this host is
A macOS workstation. The interactive user environment (shell, git, editor,
Claude) is the **shared `../../home` bundle** — the same modules the Linux hosts
use — so the terminal experience matches. The Linux-only `desktop.nix`/`sway.nix`
are intentionally left out. This host config covers the macOS-specific layer:
system packages, Homebrew, and macOS UI defaults.
## Package sourcing
- **nixpkgs** (`environment.systemPackages`) for CLI tooling and libraries.
- **Homebrew**, owned declaratively by `nix-homebrew` (Rosetta enabled for
x86_64 formulae). The `brews`/`casks` lists are **authoritative**:
`onActivation.cleanup = "zap"` uninstalls anything not declared. GUI apps are
casks (nixpkgs darwin GUI support is unreliable); a few version-pinned
toolchains and the PWA host stay on brew for continuity.
- **Mac App Store** apps are **not** declarative: nix-darwin 26.05 runs
activation as root, and `mas` cannot reach the App Store session from root.
Install them by hand with `mas install <id>` from a GUI Terminal (the `mas`
CLI is in `environment.systemPackages`).
## macOS integration
- `security.pam.services.sudo_local`**Touch ID for sudo** (and
`darwin-rebuild`'s sudo prompt), kept in `sudo_local` so it survives OS
updates. `reattach` pulls in `pam_reattach` so Touch ID works inside tmux
(which the terminals auto-start).
- `system.defaults` — declarative dock / finder / global / trackpad preferences,
applied on activation and reversible. This is the main reason to run nix-darwin
beyond package management.
- The JetBrainsMono Nerd Font is installed to `/Library/Fonts`; set it in
iTerm2 (Settings → Profiles → Text → Font) so the tmux statusline glyphs
render.
## stateVersion
`system.stateVersion = 5` (the nix-darwin state version, an integer — not a
NixOS release string). Read `darwin-rebuild changelog` before changing it.
## Apply
```sh
darwin-rebuild switch --flake .#lyrathorpe-mac
```
+2 -1
View File
@@ -98,6 +98,7 @@
"lld@21"
"python@3.14"
"dosbox-staging"
"mole"
];
# GUI applications. macOS app bundles are managed as casks; nixpkgs darwin
# GUI support is unreliable, so these stay on brew for continuity.
@@ -111,6 +112,7 @@
"bitwarden"
"citrix-workspace"
"curseforge"
"darktable"
"discord"
"firefox"
"freecad"
@@ -131,7 +133,6 @@
"signal"
"steam"
"thunderbird"
"virtualbox"
"visual-studio-code"
"vnc-viewer"
"vscodium"
+53
View File
@@ -0,0 +1,53 @@
# Work WSL box — `emmathorpe-edaas`
Flake host: `emmathorpe-edaas` (`x86_64-linux`). NixOS running under
**NixOS-WSL** on the corporate Windows machine. Headless: no Sway desktop
(`features.swayDesktop.enable = false`), plain WSL shell login. Files:
`configuration.nix`.
## What this host is
The day-to-day work environment. It layers the corporate Kubernetes / Helm /
Terraform / cloud toolchain and a couple of work-only editor language servers on
top of the shared home profile. The system config here is thin — it is mostly
WSL plumbing; the user-facing tooling lives in
[`../../users/emmathorpe/work.nix`](../../users/emmathorpe/work.nix).
## WSL specifics
- `wsl.enable`, default user `emmathorpe`, Windows PATH interop and start-menu
launchers on. `/etc/hosts` generation is off (`generateHosts = false`).
- **Docker Desktop integration**, not the native daemon as the primary path:
`wsl.extraBin` shims the coreutils/`groupadd`/`usermod` binaries Docker
Desktop's `wsl-distro-proxy` expects, and `docker-desktop-proxy.script` is
patched to the real proxy path. The native `virtualisation.docker` is also
enabled (with `enableOnBoot` + `autoPrune`).
- `programs.ssh.systemd-ssh-proxy.enable = false` — the NixOS-WSL store is a
read-only VHD owned by `nobody`, and OpenSSH rejects the generated
`ssh-proxy` Include as "Bad owner or permissions", which would break ssh/git
for every command. The vsock proxy it provides is unused under WSL.
- `networking.hostName = "emmathorpe-edaas"` matches the flake attribute so
`nh os switch` resolves without `-H`.
## Renovate review timer
The host-table entry sets `users.emmathorpe.linger = true` so the user's
`systemd --user` instance stays alive without an open login session. That keeps
the daily headless **Renovate PR review** timer firing — defined in
[`../../users/emmathorpe/renovate-review.nix`](../../users/emmathorpe/renovate-review.nix)
(imported only from `work.nix`, so it exists on this machine alone). See that
file's header for the auth (Vertex AI ADC), triage policy, and caveats.
## stateVersion
`system.stateVersion = "24.11"` — the release this box was first installed on.
Leave it; it freezes stateful defaults and is not meant to track the current
nixpkgs.
## Apply
```sh
sudo nixos-rebuild switch --flake .#emmathorpe-edaas
# or, since the hostname matches the attribute:
nh os switch
```
-5
View File
@@ -62,11 +62,6 @@
features.swayDesktop.enable = false;
# Opt out of fleet-wide SSSD/Authentik LDAP auth: this is a work-managed WSL
# box, not part of the personal directory. Every other NixOS host inherits the
# default-true from modules/sssd.nix.
services.authentikLdap.enable = false;
# NOTE: this user's systemd --user lingering -- so the home-manager renovate
# timer fires without an open login session -- is enabled from the host table
# in flake.nix (users.emmathorpe.linger = true) and applied by
+4
View File
@@ -27,6 +27,10 @@
];
};
# Explicit rather than relying on the module default (which upstream will stop
# defaulting to true; the eval warns otherwise).
hardware.asahi.enable = true;
# Apple peripheral firmware (Wi-Fi/Bluetooth). The directory is gitignored and
# populated out-of-band -- see README.
hardware.asahi.peripheralFirmwareDirectory = ../../modules/firmware;
+1 -1
View File
@@ -48,7 +48,7 @@ gigabit ports.
## Login
Graphical login via a Wayland greeter — `greetd` running ReGreet inside the
`cage` kiosk compositor — configured centrally in `lyrathorpe/swaywm.nix` for
`cage` kiosk compositor — configured centrally in `../../modules/sway.nix` for
every Sway host (gated on `features.swayDesktop.enable`). The greeter is forced
to the Dvorak layout to match the console and Sway session. Set the user
password (`passwd lyrathorpe`) after install, or the greeter cannot
+3 -2
View File
@@ -15,7 +15,7 @@ Headless `aarch64-linux` server with two roles:
```sh
nixos-generate-config --root /mnt
# copy /mnt/etc/nixos/hardware-configuration.nix over
# system/machine/RPi5/hardware-configuration.nix in this repo, then commit
# hosts/RPi5/hardware-configuration.nix in this repo, then commit
```
`hardware-configuration.nix` in this directory is a **placeholder** committed
only so the host evaluates in CI. The machine will not boot correctly until it
@@ -28,7 +28,8 @@ Headless `aarch64-linux` server with two roles:
nh os switch
```
4. Give the login user a password (`passwd lyrathorpe`) and confirm the key in
`system/modules/ssh.nix` is the one you will connect with.
the user registry (`../../users/registry.nix`, applied by
`../../modules/ssh.nix`) is the one you will connect with.
## Docker socket (security)
+1 -1
View File
@@ -35,7 +35,7 @@ change and `radeon` stays idle.
## Login
Graphical login via a Wayland greeter — `greetd` running ReGreet inside the
`cage` kiosk compositor — configured centrally in `lyrathorpe/swaywm.nix` for
`cage` kiosk compositor — configured centrally in `../../modules/sway.nix` for
every Sway host (gated on `features.swayDesktop.enable`). The greeter is forced
to the Dvorak layout to match the console and Sway session. Set the user
password (`passwd lyrathorpe`) after install, or the greeter cannot
-130
View File
@@ -1,130 +0,0 @@
# Authentik LDAP authentication for NixOS hosts.
#
# Wires SSSD (System Security Services Daemon) to the Authentik LDAP outpost so
# every Linux host authenticates users against the same directory that backs the
# SSO stack. Enabled by default on every NixOS host via baseModules; the EDaaS
# WSL box opts out (services.authentikLdap.enable = false) because it is a
# work-managed Windows-hosted environment.
#
# The Authentik LDAP provider exposes NON-standard object classes/attributes
# (goauthentik.io/ldap/user, goauthentik.io/ldap/group) alongside the POSIX
# attributes (uid, uidNumber, gidNumber, homeDirectory), so the schema mappings
# below are explicit rather than relying on an RFC2307 default.
#
# The bind password is NOT inlined: services.sssd.config renders to the world-
# readable Nix store, so the credential is delivered out-of-band by agenix as an
# sssd.conf drop-in under /etc/sssd/conf.d/ (SSSD merges conf.d/*.conf after the
# main file). See secrets/README.md.
{
config,
lib,
...
}:
let
cfg = config.services.authentikLdap;
# Directory coordinates for the Authentik LDAP provider.
ldapUri = "ldaps://ldap.lyrapup.pet:636";
searchBase = "dc=ldap,dc=goauthentik,dc=io";
bindDn = "cn=sssd-bind,ou=users,dc=ldap,dc=goauthentik,dc=io";
in
{
options.services.authentikLdap.enable =
lib.mkEnableOption "SSSD authentication against the Authentik LDAP outpost"
// {
default = true;
};
config = lib.mkIf cfg.enable {
services.sssd = {
enable = true;
# Non-secret sssd.conf. The bind password is injected separately via the
# agenix conf.d drop-in (ldap_default_authtok lives there, not here) to
# keep it out of the Nix store.
config = ''
[sssd]
config_file_version = 2
services = nss, pam
domains = default
[nss]
# Do not walk the whole directory for `getent passwd` etc.
filter_users = root
filter_groups = root
[pam]
[domain/default]
# --- Providers --------------------------------------------------------
id_provider = ldap
auth_provider = ldap
chpass_provider = none
access_provider = permit
# --- Connection -------------------------------------------------------
ldap_uri = ${ldapUri}
ldap_search_base = ${searchBase}
ldap_default_bind_dn = ${bindDn}
ldap_default_authtok_type = password
# ldap_default_authtok is supplied by the agenix drop-in in conf.d.
# --- TLS (LDAPS on 636; no StartTLS) ---------------------------------
ldap_id_use_start_tls = false
ldap_tls_reqcert = demand
# --- Schema: Authentik LDAP provider ---------------------------------
# Authentik returns DN-valued group membership (member/memberOf), so
# rfc2307bis (not rfc2307) is the correct base schema.
ldap_schema = rfc2307bis
# Users: goauthentik.io/ldap/user, keyed by uid; POSIX attrs are
# standard names (uidNumber/gidNumber/homeDirectory).
ldap_user_object_class = goauthentik.io/ldap/user
ldap_user_name = uid
ldap_user_uid_number = uidNumber
ldap_user_gid_number = gidNumber
ldap_user_home_directory = homeDirectory
ldap_user_gecos = displayName
ldap_user_shell = loginShell
# Groups: goauthentik.io/ldap/group, keyed by cn.
ldap_group_object_class = goauthentik.io/ldap/group
ldap_group_name = cn
ldap_group_gid_number = gidNumber
ldap_group_member = member
# --- Behaviour --------------------------------------------------------
cache_credentials = true
enumerate = false
'';
};
# agenix delivers the bind password as an sssd.conf drop-in. The decrypted
# plaintext IS a valid conf.d snippet:
#
# [domain/default]
# ldap_default_authtok = <the bind password>
#
# SSSD requires conf.d files to be root-owned and 0600 or it ignores them.
age.secrets.ldap-bind = {
file = ../secrets/ldap-bind.age;
path = "/etc/sssd/conf.d/01-ldap-authtok.conf";
owner = "root";
group = "root";
mode = "0600";
};
# Restart SSSD when the credential drop-in changes. agenix writes secrets in
# a system activation script that runs before systemd (re)starts services on
# a `switch`, so the file is present by the time sssd starts; the trigger
# picks up rotations of the bind password.
systemd.services.sssd.restartTriggers = [ config.age.secrets.ldap-bind.path ];
# Create home directories on first login for LDAP users (they have no
# locally-provisioned home). NixOS wires nss + the SSSD PAM stack when
# services.sssd.enable is true; mkHomeDir adds pam_mkhomedir to it.
security.pam.services.login.makeHomeDir = true;
security.pam.services.sshd.makeHomeDir = true;
};
}
-92
View File
@@ -1,92 +0,0 @@
# Secrets (agenix)
Encrypted secrets for the fleet, managed with [agenix](https://github.com/ryantm/agenix).
Each secret is an age-encrypted file (`*.age`) encrypted to a set of recipient
public keys declared in [`secrets.nix`](./secrets.nix). A host decrypts its
secrets at activation using its SSH **host** key
(`/etc/ssh/ssh_host_ed25519_key`), so every host that must read a secret has to
be listed as a recipient for it.
`secrets.nix` is read only by the `agenix` CLI. It is never imported into the
NixOS evaluation.
## Secrets in this repo
| File | Purpose | Recipients |
| --------------- | ----------------------------------------------------------------------- | ----------------------------------- |
| `ldap-bind.age` | SSSD → Authentik LDAP bind credential, as an `sssd.conf` drop-in snippet | all SSSD-enabled hosts (not EDaaS) |
Consumed by [`modules/sssd.nix`](../modules/sssd.nix) via
`age.secrets.ldap-bind.path`, which places the decrypted snippet at
`/etc/sssd/conf.d/01-ldap-authtok.conf`.
> **`ldap-bind.age` is not committed yet.** Only `ldap-bind.age.PLACEHOLDER`
> ships in this change (real host recipient keys and the real password were not
> available when it was written). Follow the steps below to create the real
> secret, then delete the `.PLACEHOLDER`.
## Owner setup checklist
Run these once (per new host or when the bind password rotates):
### 1. Collect host recipient keys
On each SSSD-enabled host (all Linux hosts **except** EDaaS):
```sh
cat /etc/ssh/ssh_host_ed25519_key.pub
```
Paste each value into the matching placeholder in `secrets.nix`, replacing the
`AAAA_PLACEHOLDER_REPLACE_ME_*` strings. (Optionally uncomment and set `admin`
to an operator user key so the secret can be edited off-host.)
### 2. Encrypt the bind password
The plaintext must be a **full sssd.conf drop-in snippet**, because SSSD cannot
read `ldap_default_authtok` from a separate file — it only merges `conf.d/*.conf`.
The content is exactly:
```ini
[domain/default]
ldap_default_authtok = <the sssd-bind service-account password>
```
Use the password of the `sssd-bind` (Terraform: `sssd-bind`) Authentik LDAP
service account. Then, from the repo root:
```sh
# Requires the agenix CLI: `nix run github:ryantm/agenix -- -e secrets/ldap-bind.age`
cd secrets
agenix -e ldap-bind.age
```
An `$EDITOR` opens; paste the two-line snippet above, save, quit. agenix writes
the encrypted `ldap-bind.age`. Commit it and delete `ldap-bind.age.PLACEHOLDER`.
### 3. Rekey after changing recipients
If you add/remove hosts in `secrets.nix`, re-encrypt every secret to the new
recipient set:
```sh
cd secrets
agenix -r
```
### 4. DNS
`ldap.lyrapup.pet` must resolve to the Authentik LDAP outpost and serve LDAPS on
port 636 with a certificate the hosts trust (`ldap_tls_reqcert = demand`). If the
cert is not from a system-trusted CA, add it to the hosts' trust store
(`security.pki.certificateFiles`) or relax `ldap_tls_reqcert` in
`modules/sssd.nix`.
### 5. Rebuild
```sh
sudo nixos-rebuild switch --flake .#<host>
```
Verify with `getent passwd <ldap-user>` and `id <ldap-user>`.
-21
View File
@@ -1,21 +0,0 @@
THIS IS A PLACEHOLDER, NOT A REAL AGE SECRET.
The real secrets/ldap-bind.age is produced by the repo owner with `agenix -e`
(see secrets/README.md) and is a binary age-encrypted blob. It is intentionally
NOT committed here because:
* the real host age recipients are not available to the author of this change
(they are each host's /etc/ssh/ssh_host_ed25519_key.pub), and
* fabricating an encrypted blob or fake host keys would be misleading.
Committing this file as `ldap-bind.age` would let modules/sssd.nix reference
`../secrets/ldap-bind.age` and evaluate, but SSSD would fail to decrypt it at
runtime. Do ONE of the following before deploying:
1. Preferred: generate the real secret (secrets/README.md), commit it as
secrets/ldap-bind.age, and delete this .PLACEHOLDER file.
The decrypted plaintext must be a valid sssd.conf drop-in (NOT the bare
password):
[domain/default]
ldap_default_authtok = <the sssd-bind service-account password>
-15
View File
@@ -1,15 +0,0 @@
let
lyrathorpe-mbp = "ssh-ed25519 AAAA_PLACEHOLDER_REPLACE_ME_mbp";
lyrathorpe-t400 = "ssh-ed25519 AAAA_PLACEHOLDER_REPLACE_ME_t400";
lyrathorpe-macpro31 = "ssh-ed25519 AAAA_PLACEHOLDER_REPLACE_ME_macpro31";
lyrathorpe-rpi5 = "ssh-ed25519 AAAA_PLACEHOLDER_REPLACE_ME_rpi5";
sssdHosts = [
lyrathorpe-mbp
lyrathorpe-t400
lyrathorpe-macpro31
lyrathorpe-rpi5
];
in
{
"ldap-bind.age".publicKeys = sssdHosts;
}
+11
View File
@@ -51,6 +51,17 @@
home.shellAliases = {
docker = "/run/current-system/sw/bin/docker";
};
# Source the (nix-unmanaged) Jenkins credentials file into every zsh, so the
# JENKINS_UCE_/JENKINS_STF_ tokens are exported for all shells and anything they
# launch -- the Jenkins MCP servers read them via ${JENKINS_*} expansion.
# envExtra lands in ~/.zshenv, which zsh sources for login, interactive, and
# non-interactive shells alike. Guarded so a missing file never breaks a shell;
# the file holds secrets, so it is kept out of the world-readable nix store.
programs.zsh.envExtra = ''
[ -f "$HOME/.jenkinsenv" ] && . "$HOME/.jenkinsenv"
[ -f "$HOME/.splunkenv" ] && . "$HOME/.splunkenv"
'';
programs.tmux = {
# kube context/namespace in the status line. kube-tmux is pinned as a flake
# input (it is not in nixpkgs), so the script is always present in the store.