Commit e33af02

HPCesia <me@hpcesia.com>
2026-08-04 15:47:15
dev: init
1 parent 793b0ce
modules/dev/agents/skills/grilling/SKILL.md
@@ -0,0 +1,20 @@
+---
+name: grilling
+description: >
+  Grill the user relentlessly about a plan or design. Use when the user wants to
+  stress-test a plan before building, or uses any 'grill' trigger phrases.
+---
+
+# Grilling Me
+
+Interview me relentlessly about every aspect of this plan until we reach a shared
+understanding. Walk down each branch of the design tree, resolving dependencies
+between decisions one-by-one. For each question, provide your recommended answer.
+
+Ask the questions one at a time, waiting for feedback on each question before
+continuing. Asking multiple questions at once is bewildering.
+
+If a *fact* can be found by exploring the codebase, look it up rather than asking
+me. The *decisions*, though, are mine — put each one to me and wait for my answer.
+
+Do not enact the plan until I confirm we have reached a shared understanding.
modules/dev/agents/skills/jujutsu/SKILL.md
@@ -0,0 +1,467 @@
+---
+name: jujutsu
+description: >
+  Always activate FIRST on any git/VCS operations (commit, status, branch, push, etc.),
+  especially when HEAD is detached. If `.jj/` exists -> this is a Jujutsu (jj) repo,
+  raw git commands can corrupt data. Essential git safety instructions inside.
+allowed-tools: Bash(jj *)
+---
+
+# Jujutsu (jj) Version Control System
+
+This skill helps you work with Jujutsu, a Git-compatible VCS with mutable commits
+and automatic rebasing.
+
+**Tested with jj v0.37.0** - Commands may differ in other versions.
+
+## Important: Automated/Agent Environment
+
+When running as an agent:
+
+1. **Always use `--no-pager`** to prevent commands from opening an interactive
+  pager (like `less`), which will hang the agent:
+
+```bash
+# Always use --no-pager on commands that produce output
+jj --no-pager log          # NOT: jj log
+jj --no-pager diff         # NOT: jj diff
+jj --no-pager show <id>    # NOT: jj show <id>
+```
+
+2. **Always use `-m` flags** to provide messages inline rather than relying on
+  editor prompts:
+
+```bash
+# Always use -m to avoid editor prompts
+jj desc -m "message"      # NOT: jj desc
+jj squash -m "message"    # NOT: jj squash (which opens editor)
+```
+
+Editor-based commands will fail in non-interactive environments.
+
+3. **Verify operations with `jj st`** after mutations (`squash`, `abandon`,
+`rebase`, `restore`) to confirm the operation succeeded.
+
+## Core Concepts
+
+### The Working Copy is a Commit
+
+In jj, your working directory is always a commit (referenced as `@`). Changes
+are automatically snapshotted when you run any jj command. There is no staging area.
+
+There is no need to run `jj commit`.
+
+### Commits Are Mutable
+
+**CRITICAL**: Unlike git, jj commits can be freely modified after creation. You
+can update descriptions, squash changes, rebase, and absorb — all without creating
+new commits. See "Essential Workflow" below for the recommended working pattern.
+
+### Change IDs vs Commit IDs
+
+- **Change ID**: A stable identifier (like `tqpwlqmp`) that persists when a commit
+  is rewritten — prefer these when referencing commits
+- **Commit ID**: A content hash (like `3ccf7581`) that changes when commit content
+  changes
+
+### Revsets
+
+jj uses a revset language to select commits in commands. Common revsets:
+
+- `@` — the working copy commit
+- `@-` — the parent of the working copy
+- `::@` — all ancestors of `@`
+- `@::` — all descendants of `@`
+- `trunk()..@` — commits between trunk and `@` (your branch)
+- `bookmarks()` — all commits with bookmarks
+
+Use revsets with `-r` flags: `jj log -r 'trunk()..@'`
+
+## Essential Workflow
+
+### Starting Work: Describe First, Then Code
+
+**Always create your commit message before writing code:**
+
+Validate that you're on a blank revision with `jj st`. If you are not, you should
+type:
+
+```bash
+jj new
+```
+
+```bash
+# First, describe what you intend to do
+jj desc -m "Add user authentication to login endpoint"
+
+# Then make your changes - they automatically become part of this commit
+# ... edit files ...
+
+# Check status
+jj st
+```
+
+### Creating Atomic Commits
+
+Each commit should represent ONE logical change. Use this format for commit messages:
+
+```text
+Examples:
+- "Add validation to user input forms"
+- "Fix null pointer in payment processor"
+- "Remove deprecated API endpoints"
+- "Update dependencies to latest versions"
+```
+
+### Viewing History
+
+```bash
+# View recent commits
+jj --no-pager log
+
+# View with patches
+jj --no-pager log -p
+
+# View specific commit
+jj --no-pager show <change-id>
+
+# View diff of working copy (use --git for familiar +/- format)
+jj --no-pager diff --git
+```
+
+**IMPORTANT: `jj diff` output format**: The default `jj diff` output uses a side-by-side
+line number format (e.g. `26   26:`) that looks very different from git's `+`/`-`
+prefix format. This is **normal and correct** — it is NOT corrupted or showing stale
+content. However, to avoid confusion, **always use `jj diff --git`** to get standard
+unified diff format with `+`/`-` lines.
+
+### Moving Between Commits
+
+```bash
+# Create a new empty commit on top of current
+jj new
+
+# Create new commit with message
+jj new -m "Commit message"
+
+# Edit an existing commit (working copy becomes that commit)
+jj edit <change-id>
+
+# Edit the previous commit
+jj prev -e
+
+# Edit the next commit
+jj next -e
+```
+
+## Refining Commits
+
+### Squashing Changes
+
+Move changes from current commit into its parent:
+
+```bash
+# Squash all changes into parent
+jj squash
+```
+
+**Note**: `jj squash -i` opens an interactive UI and will hang in agent environments.
+Avoid it.
+
+### Splitting Commits
+
+**Warning**: `jj split` is interactive and will hang in agent environments. To divide
+a commit, use `jj restore` to move changes out, then create separate commits manually.
+
+### Absorbing Changes
+
+Automatically distribute changes to the commits that last modified those lines:
+
+```bash
+# Absorb working copy changes into appropriate ancestor commits
+jj absorb
+```
+
+### Abandoning Commits
+
+Remove a commit entirely (descendants are rebased to its parent):
+
+```bash
+jj abandon <change-id>
+```
+
+### Undoing Operations
+
+Reverse the last jj operation:
+
+```bash
+jj undo
+```
+
+This reverts the repository to its state before the previous command. Useful
+for recovering from mistakes like accidental `abandon`, `squash`, or `rebase`.
+
+### Rebasing Commits
+
+Move commits to a different parent:
+
+```bash
+# Rebase current branch onto a destination
+jj rebase -d <destination>
+
+# Rebase a specific revision (without descendants) onto a destination
+jj rebase -r <change-id> -d <destination>
+
+# Rebase a revision and all its descendants
+jj rebase -s <change-id> -d <destination>
+
+# Rebase onto trunk (common: update your branch to latest main)
+jj rebase -d main
+```
+
+### Restoring Files
+
+Discard changes to specific files or restore files from another revision:
+
+```bash
+# Discard all uncommitted changes in working copy (restore from parent)
+jj restore
+
+# Discard changes to specific files
+jj restore path/to/file.txt
+
+# Restore files from a specific revision
+jj restore --from <change-id> path/to/file.txt
+```
+
+## Working with Bookmarks (Branches)
+
+Bookmarks are jj's equivalent to git branches:
+
+```bash
+# Create a bookmark at current commit
+jj bookmark create my-feature -r@
+
+# Move bookmark to a different commit
+jj bookmark move my-feature --to <change-id>
+
+# List bookmarks
+jj --no-pager bookmark list
+
+# Delete a bookmark
+jj bookmark delete my-feature
+```
+
+## Workspaces
+
+A **workspace** is a working copy plus its associated repo. One repo can have
+multiple workspaces — each with its own working directory and working-copy
+commit (`@`) — all sharing the same commits, operations, and bookmarks. This is
+jj's equivalent of `git worktree`.
+
+Useful for running a long build or test in one workspace while editing in another.
+Workspaces are a rarely-needed feature; consult the [official docs](https://docs.jj-vcs.dev/latest/working-copy/#workspaces)
+for anything beyond the basics below.
+
+### Common commands
+
+```bash
+# Create a new workspace (defaults: name = basename of path, parent = current @'s parent)
+jj workspace add ../my-tests
+jj workspace add --name tests -r <change-id> ../my-tests   # explicit name and base
+
+# Inspect
+jj --no-pager workspace list
+jj workspace root [--name <ws>]
+
+# Remove (does NOT delete files on disk — rm the directory separately)
+jj workspace forget [<ws>]
+
+# Rename current workspace
+jj workspace rename <new-name>
+```
+
+In `jj log`, each workspace's `@` appears as `<workspace-name>@`.
+
+### Key semantics
+
+- **Isolation by default.** `jj workspace add` gives the new workspace its own
+  fresh empty commit; workspaces don't start out sharing `@`, and on-disk files
+  are never live-mirrored between them.
+- **Propagation at command boundaries.** Each jj command snapshots the current
+  workspace's files and reads the op log, so it sees commits/bookmarks made by
+  other workspaces. There is no filesystem watcher.
+- **Stale working copy.** If another workspace rewrites this workspace's `@`
+  (e.g. via `jj squash`, `rebase`, `abandon`), jj refuses commands here until you
+  run `jj workspace update-stale`. Same recovery path if a command was interrupted
+  mid-update.
+- **Shared `@` is sharp-edged.** `jj edit <id>` lets two workspaces point at the
+  same change without warning. When one mutates it, the other goes stale; if the
+  stale one had un-snapshotted edits, `update-stale` preserves them as a **divergent
+  commit** (same change ID, shown as `xyz??` in `jj log`) that you must resolve.
+  Avoid sharing `@` unless both workspaces are read-only.
+
+### Agent guidance
+
+- Always pass `--no-pager` to `jj workspace list`.
+- Don't `jj edit` a change another workspace already has as its `@` — main cause
+  of accidental divergence.
+- Don't `rm -rf` a workspace directory without also running
+  `jj workspace forget <name>`.
+
+## Git Integration
+
+### Working with Existing Git Repos
+
+```bash
+# Clone a git repository
+jj git clone <url>
+
+# Initialize jj in an existing git repo
+jj git init --colocate
+```
+
+### Fetching Remote Changes
+
+```bash
+# Fetch all branches from the default remote
+jj git fetch
+
+# Fetch from a specific remote
+jj git fetch --remote <remote-name>
+
+# Fetch specific branches
+jj git fetch -b <branch-name>
+```
+
+After fetching, rebase your work onto the updated trunk: `jj rebase -d main`
+
+### Switching Between jj and git (Colocated Repos Only)
+
+**This section only applies to colocated repos** (where both `.jj/` and `.git/`
+exist). In non-colocated repos, do not use git commands — they will corrupt jj state.
+
+In a colocated repository, you can use both jj and git commands with care:
+
+**Switching to git mode** (e.g., for merge workflows):
+
+```bash
+# First, ensure your jj working copy is clean
+jj st
+
+# Then checkout a branch with git
+git checkout <branch-name>
+```
+
+**Switching back to jj mode**:
+
+```bash
+# Use jj edit to resume working with jj
+jj edit <change-id>
+```
+
+**Important notes:**
+
+- Git may complain about uncommitted changes if jj's working copy differs from
+  the git HEAD
+- ALWAYS ensure your work is committed in jj before switching to git
+- After git operations, jj will detect and incorporate the changes on next command
+
+### Pushing Changes
+
+When the user asks you to push changes:
+
+```bash
+# Push a specific bookmark to the remote
+jj git push -b <bookmark-name>
+
+# Example: push the main bookmark
+jj git push -b main
+```
+
+**Before pushing, ensure:**
+
+1. Your bookmark points to the correct commit (bookmarks don't auto-advance like
+   git branches)
+2. The commits are refined and atomic
+3. The user has explicitly requested the push
+
+**IMPORTANT**: Unlike git branches, jj bookmarks do not automatically move when
+you create new commits. You must manually update them before pushing:
+
+```bash
+# Move an existing bookmark to the current commit
+jj bookmark move my-feature --to @
+
+# Then push it
+jj git push -b my-feature
+```
+
+If no bookmark exists for your changes, create one first:
+
+```bash
+# Create a bookmark at the current commit
+jj bookmark create my-feature
+
+# Then push it
+jj git push -b my-feature
+```
+
+## Handling Conflicts
+
+jj allows committing conflicts — you can resolve them later:
+
+```bash
+# View conflicts
+jj st
+```
+
+**Agent conflict resolution**: Do not use `jj resolve` (interactive). Instead, edit
+the conflicted files directly to remove conflict markers, then run `jj st` to verify
+resolution.
+
+## Preserving Commit Quality
+
+**IMPORTANT**: Because commits are mutable, always refine them before considering
+work done:
+
+1. **Review your commit**: `jj --no-pager show @` or `jj --no-pager diff --git`
+2. **Is it atomic?** One logical change per commit
+3. **Is the message clear?** Use imperative verb phrase in sentence case format
+   with no full stop: e.g. "Add login endpoint", "Fix null pointer in payment
+   processor", "Remove deprecated API endpoints"
+4. **Are there unrelated changes?** Use `jj restore` to move changes out, then create
+   separate commits
+5. **Should changes be elsewhere?** Use `jj squash` or `jj absorb`
+
+## Quick Reference
+
+| Action | Command |
+|--------|---------|
+| Describe commit | `jj desc -m "message"` |
+| View status | `jj st` |
+| View log | `jj --no-pager log` |
+| View diff | `jj --no-pager diff --git` |
+| New commit | `jj new -m "message"` (use `jj st` first; skip if `@` is empty) |
+| Edit commit | `jj edit <id>` |
+| Squash to parent | `jj squash` |
+| Auto-distribute | `jj absorb` |
+| Rebase | `jj rebase -d <destination>` |
+| Abandon commit | `jj abandon <id>` |
+| Undo last operation | `jj undo` |
+| Restore files | `jj restore [paths]` |
+| Create bookmark | `jj bookmark create <name>` |
+| Fetch remote | `jj git fetch` |
+| Push bookmark | `jj git push -b <name>` |
+| Add workspace | `jj workspace add <path>` |
+| List workspaces | `jj --no-pager workspace list` |
+| Forget workspace | `jj workspace forget [name]` |
+| Fix stale working copy | `jj workspace update-stale` |
+
+## Best Practices Summary
+
+1. **Describe first**: Set the commit message before coding
+2. **One change per commit**: Keep commits atomic and focused
+3. **Use change IDs**: They're stable across rewrites
+4. **Refine commits**: Leverage mutability for clean history
+5. **Embrace the workflow**: No staging area, no stashing - just commit
modules/dev/agents/default.nix
@@ -0,0 +1,18 @@
+{
+  den.aspects.dev.agents = {
+    hjem = {lib, ...}: let
+      skills = {
+        grilling = ./skills/grilling;
+        jujutsu = ./skills/jujutsu;
+      };
+    in {
+      xdg.config.files =
+        {
+          "agents/skills".type = "directory";
+        }
+        // lib.mapAttrs'
+        (n: v: lib.nameValuePair "agents/skills/${n}" {source = v;})
+        skills;
+    };
+  };
+}
modules/dev/agents/opencode.nix
@@ -0,0 +1,77 @@
+{den, ...}: {
+  den.aspects.dev.agents.opencode = {
+    includes = [den.aspects.dev.agents];
+
+    persistHome = {config, ...}: {
+      directories = [
+        "${config.xdg.config.directory}/opencode"
+      ];
+    };
+
+    cacheHome = {config, ...}: {
+      directories = [
+        "${config.xdg.data.directory}/opencode"
+        "${config.xdg.state.directory}/opencode"
+      ];
+    };
+
+    hjem = {
+      config,
+      pkgs,
+      ...
+    }: {
+      packages = [pkgs.opencode];
+
+      xdg.config.files."opencode/opencode.json" = {
+        generator = (pkgs.formats.json {}).generate "opencode-config.json";
+        value = {
+          permission = {
+            external_directory = {
+              "/nix/store/**" = "allow";
+            };
+            edit = {
+              "*" = "ask";
+              "/nix/store/**" = "deny";
+            };
+            grep = {
+              "/nix/store" = "deny";
+            };
+            webfetch = "ask";
+          };
+          mcp = {
+            deepwiki = {
+              type = "remote";
+              url = "https://mcp.deepwiki.com/mcp";
+            };
+          };
+          formatter = {
+            alejandra = {
+              command = ["alejandra" "$FILE"];
+              extensions = [".nix"];
+            };
+          };
+          plugin = [
+            "opencode-wakatime"
+            "@simonwjackson/opencode-direnv"
+          ];
+        };
+      };
+
+      xdg.config.files."opencode/tui.json" = {
+        generator = (pkgs.formats.json {}).generate "opencode-config-tui.json";
+        value = {
+        };
+      };
+
+      xdg.config.files."opencode/skills" = {
+        source = "${config.xdg.config.directory}/agents/skills";
+      };
+    };
+  };
+
+  den.aspects.develop.opencode.themes = {
+    catppuccin = {
+      hjem.xdg.config.files."opencode/tui.json".value.theme = "catppuccin";
+    };
+  };
+}
modules/dev/editor/helix/lang/nix.nix
@@ -0,0 +1,35 @@
+{den, ...}: {
+  den.aspects.dev.editor.helix.includes = [den.aspects.dev.editor.helix.lang.nix];
+  den.aspects.dev.editor.helix.lang.nix = {
+    hjem = {
+      xdg.config.files."helix/languages.toml".value = {
+        language = [
+          {
+            name = "nix";
+            auto-format = true;
+            indent = {
+              tab-width = 2;
+              unit = "  ";
+            };
+            language-servers = ["nixd"];
+          }
+        ];
+        language-server = {
+          nil.config.nil = {
+            formatting.command = ["alejandra"];
+            nix = {
+              maxMemoryMB = 4096;
+              flake = {
+                autoArchive = false;
+                autoEvalInputs = true;
+              };
+            };
+          };
+          nixd.config.nixd = {
+            formatting.command = ["alejandra"];
+          };
+        };
+      };
+    };
+  };
+}
modules/dev/editor/helix/default.nix
@@ -0,0 +1,112 @@
+{
+  den.aspects.dev.editor.helix = {
+    cacheHome = {config, ...}: {
+      directories = [
+        "${config.xdg.config.directory}/helix"
+        "${config.xdg.data.directory}/helix" # Workspace trust data
+        "${config.xdg.data.directory}/steel" # Steel plugin system data
+      ];
+    };
+
+    hjem = {
+      config,
+      pkgs,
+      ...
+    }: let
+      steel-wrapped = pkgs.symlinkJoin {
+        pname = "steel-wrapped";
+        inherit (pkgs.steel) version;
+        paths = [pkgs.steel];
+        nativeBuildInputs = [pkgs.makeBinaryWrapper];
+        postBuild = ''
+          wrapProgram $out/bin/forge \
+            --prefix PATH : ${pkgs.lib.makeBinPath (with pkgs; [gcc cargo])}
+        '';
+        meta = {
+          inherit
+            (pkgs.steel.meta)
+            description
+            homepage
+            changelog
+            license
+            mainProgram
+            ;
+        };
+      };
+    in {
+      packages = [
+        pkgs.nur.repos.hpcesia.steelix
+
+        steel-wrapped
+        pkgs.schemat
+      ];
+
+      environment.sessionVariables = {
+        EDITOR = "hx";
+        VISUAL = "hx";
+        STEEL_HOME = "${config.xdg.data.directory}/steel";
+      };
+
+      xdg.config.files."helix/config.toml" = {
+        generator = (pkgs.formats.toml {}).generate "helix-config.toml";
+        value = {
+          editor = {
+            line-number = "relative";
+            cursorline = true;
+            bufferline = "multiple";
+            statusline = {
+              left = ["mode" "spinner" "diagnostics" "workspace-diagnostics"];
+              center = ["file-name" "read-only-indicator" "file-modification-indicator"];
+              right = ["file-type" "file-encoding" "separator" "position" "total-line-numbers"];
+            };
+            cursor-shape = {
+              normal = "block";
+              insert = "bar";
+              select = "block";
+            };
+            color-modes = true;
+            trim-trailing-whitespace = true;
+            inline-diagnostics.cursor-line = "warning";
+            end-of-line-diagnostics = "error";
+            lsp = {
+              display-inlay-hints = true;
+              inlay-hints-length-limit = 16;
+            };
+            indent-guides = {
+              render = true;
+              character = "┊";
+              skip-levels = 1;
+            };
+            file-picker = {
+              hidden = false;
+            };
+          };
+          keys = {
+            normal = {
+              "H" = "goto_previous_buffer";
+              "L" = "goto_next_buffer";
+            };
+          };
+        };
+      };
+
+      xdg.config.files."helix/languages.toml" = {
+        generator = (pkgs.formats.toml {}).generate "helix-languages.toml";
+        value = {};
+      };
+
+      xdg.config.files."helix/helix.scm" = {
+        source = ./helix.scm;
+        type = "copy";
+      };
+      xdg.config.files."helix/init.scm" = {
+        source = ./init.scm;
+        type = "copy";
+      };
+
+      xdg.data.files."steel/cogs/smith.hx" = {
+        source = "${pkgs.nur.repos.hpcesia.helixPlugins.smith}/cogs/smith.hx";
+      };
+    };
+  };
+}
modules/dev/editor/helix/helix.scm
@@ -0,0 +1,15 @@
+(require "helix/editor.scm")
+(require (prefix-in helix. "helix/commands.scm"))
+(require (prefix-in helix.static. "helix/static.scm"))
+
+(provide open-helix-scm open-init-scm)
+
+;;@doc
+;; Open the helix.scm file
+(define (open-helix-scm)
+  (helix.open (helix.static.get-helix-scm-path)))
+
+;;@doc
+;; Opens the init.scm file
+(define (open-init-scm)
+  (helix.open (helix.static.get-init-scm-path)))
modules/dev/editor/helix/init.scm
@@ -0,0 +1,24 @@
+(require "helix/configuration.scm")
+
+(define-lsp "steel-language-server" (command "steel-language-server") (args '()))
+(define-language "scheme"
+  (language-servers '("steel-language-server"))
+  (formatter (command "schemat")))
+
+(require (only-in "smith.hx/smith.scm"
+          smith-plugin
+          smith-prune
+          smith-init))
+
+(smith-plugin "https://github.com/mtul0729/helix-fcitx-focus.git")
+(smith-plugin "https://github.com/Xerxes-2/wakatime.hx.git")
+(smith-plugin "https://github.com/Ra77a3l3-jar/who.hx.git")
+
+(smith-plugin "https://github.com/Ra77a3l3-jar/forest.hx.git"
+  (config
+    (forest-configure! 'left #:ignore (list ".git" ".jj" "result" "target" "__pycache__"))
+    (forest-set-style! 'mini))
+  (bind
+    ("normal" ("space" "e") ":forest-open")))
+
+(smith-init)
modules/dev/zellij/layouts/dev.kdl
@@ -0,0 +1,27 @@
+layout {
+    pane_template name="help_bar" {
+        borderless true
+        plugin location="zellij:status-bar"
+    }
+    pane_template name="tab_bar" {
+        borderless true
+        plugin location="zellij:tab-bar"
+    }
+    pane_template name="editor" { command "$EDITOR"; }
+
+    default_tab_template {
+        tab_bar size=1
+        children
+        help_bar size=1
+    }
+
+    tab name="edit" {
+        editor
+    }
+    tab name="shell" {
+        pane
+    }
+    tab name="agent" {
+        pane
+    }
+}
modules/dev/zellij/default.nix
@@ -0,0 +1,25 @@
+{
+  den.aspects.dev.zellij = {
+    provides.to-users = {
+      hjem = {pkgs, ...}: {
+        packages = [pkgs.zellij];
+
+        xdg.config.files = {
+          "zellij/config.kdl".text =
+            ''
+              pane_frames false
+              show_release_notes false
+              show_startup_tips false
+            ''
+            + builtins.readFile ./keybinds.kdl;
+          "zellij/layouts".source = ./layouts;
+        };
+
+        files.".bashrc".text = ''
+          alias -- zj=zellij
+          alias -- zjd='zellij --layout dev'
+        '';
+      };
+    };
+  };
+}
modules/dev/zellij/keybinds.kdl
@@ -0,0 +1,43 @@
+keybinds {
+    shared {
+        // Previously: Ctrl + o (in helix: jump_backward)
+        bind "Ctrl e" { SwitchToMode "Session"; }
+        unbind "Ctrl o"
+        // Previously: Ctrl + s (in helix: save_selection)
+        bind "Ctrl y" { SwitchToMode "Scroll"; }
+        unbind "Ctrl s"
+        // Previously: Alt + i (in helix: shrink_selection)
+        bind "Alt w" { MoveTab "Left"; }
+        unbind "Alt i"
+        // Previously: Alt + o (in helix: expand_selection)
+        bind "Alt q" { MoveTab "Right"; }
+        unbind "Alt o"
+        // Previously: Alt + n (in helix: select_next_sibling)
+        bind "Alt m" { NewPane; }
+        unbind "Alt n"
+        // Previously: Ctrl + b (in helix: move_page_up)
+        bind "Alt 1" { SwitchToMode "Tmux"; }
+        unbind "Ctrl b"
+
+        bind "Alt f" {
+            ToggleFocusFullscreen
+            SwitchToMode "Normal"
+        }
+    }
+    session {
+        // Exit session mode
+        bind "Ctrl e" { SwitchToMode "Normal"; }
+        unbind "Ctrl o"
+    }
+    scroll {
+        // Exit scroll mode
+        bind "Ctrl y" { SwitchToMode "Normal"; }
+        unbind "Ctrl s"
+    }
+    tmux {
+        // Exit tmux mode
+        bind "Alt 1" { SwitchToMode "Normal"; }
+        unbind "Ctrl b"
+    }
+}
+
modules/dev/binfmt.nix
@@ -0,0 +1,16 @@
+{den, ...}: {
+  den.aspects.dev.includes = [den.aspects.dev.binfmt];
+  den.aspects.dev.binfmt = {
+    nixos = {
+      host,
+      lib,
+      ...
+    }: let
+      currentHostSystem = host.system;
+      allHostSystems = lib.filter (sys: lib.hasSuffix "linux" sys) (lib.attrNames den.hosts);
+      emulatedSystems = lib.filter (sys: sys != currentHostSystem) allHostSystems;
+    in {
+      boot.binfmt.emulatedSystems = emulatedSystems;
+    };
+  };
+}
modules/dev/direnv.nix
@@ -0,0 +1,28 @@
+{
+  den.aspects.dev.direnv = {
+    cacheHome = {config, ...}: {
+      directories = [
+        "${config.xdg.data.directory}/direnv" # Allowed directoies data
+      ];
+    };
+
+    provides.to-users = {
+      hjem = {
+        pkgs,
+        lib,
+        ...
+      }: {
+        packages = [pkgs.direnv];
+
+        files.".bashrc".text = lib.mkAfter ''
+          eval "$(${lib.getExe pkgs.direnv} hook bash)"
+        '';
+
+        xdg.config.files."direnv/direnvrc".text = ''
+          #Load nix-direnv
+          source ${pkgs.nix-direnv}/share/nix-direnv/direnvrc
+        '';
+      };
+    };
+  };
+}
modules/dev/git.nix
@@ -0,0 +1,59 @@
+{den, ...}: {
+  den.aspects.dev.includes = [den.aspects.dev.git];
+  den.aspects.dev.git = {
+    nixos = {pkgs, ...}: {
+      environment.systemPackages = [pkgs.git];
+    };
+
+    provides.to-users = {
+      hjem = {
+        user,
+        pkgs,
+        lib,
+        ...
+      }: {
+        packages = [
+          pkgs.git
+          pkgs.git-lfs
+        ];
+
+        files.".gitconfig".type = "delete";
+
+        xdg.config.files."git/config" = {
+          generator = lib.generators.toGitINI;
+          value = {
+            user = {
+              name = user.identity.displayName;
+              email = user.identity.email;
+            };
+            init.defaultBranch = "main";
+            trim.bases = "develop,master,main"; # for git-trim
+            push.autoSetupRemote = true;
+            pull.rebase = true;
+            filter.lfs = let
+              lfsPath =
+                lib.getExe pkgs.git-lfs;
+            in {
+              clean = "${lfsPath} clean -- %f";
+              process = lib.concatStringsSep " " [
+                lfsPath
+                "filter-process"
+              ];
+              required = true;
+              smudge = lib.concatStringsSep " " (
+                [
+                  lfsPath
+                  "smudge"
+                ]
+                ++ [
+                  "--"
+                  "%f"
+                ]
+              );
+            };
+          };
+        };
+      };
+    };
+  };
+}
modules/dev/jujutsu.nix
@@ -0,0 +1,26 @@
+{
+  den.aspects.dev.jujutsu = {
+    provides.to-users = {
+      hjem = {
+        user,
+        pkgs,
+        ...
+      }: {
+        packages = [
+          pkgs.jujutsu
+          pkgs.jjui
+        ];
+
+        xdg.config.files."jj/config.toml" = {
+          generator = (pkgs.formats.toml {}).generate "jj-config.toml";
+          value = {
+            user = {
+              name = user.identity.displayName;
+              email = user.identity.email;
+            };
+          };
+        };
+      };
+    };
+  };
+}
modules/dev/utils.nix
@@ -0,0 +1,40 @@
+{den, ...}: {
+  den.aspects.dev.includes = [den.aspects.dev.utils];
+  den.aspects.dev.utils = {
+    nixos = {
+      programs.nix-ld.enable = true;
+    };
+
+    provides.to-users = {
+      hjem = {pkgs, ...}: {
+        packages = with pkgs; [
+          # Nix
+          nixd # Nix LSP
+          alejandra # Nix formatter
+
+          # Make-like
+          gnumake
+          just
+          just-lsp
+
+          # Config file
+          vscode-json-languageserver
+          yaml-language-server
+          taplo # TOML LSP / formatter
+
+          # Script related
+          bash-language-server
+          shellcheck
+          shfmt
+
+          python3
+          ty # Python LSP
+          ruff # Python fomatter
+
+          # Documents
+          rumdl # Markdown linter / formatter
+        ];
+      };
+    };
+  };
+}
modules/dev/wakatime.nix
@@ -0,0 +1,21 @@
+{
+  den.aspects.dev.wakatime = {
+    persistHome = {config, ...}: {
+      directories = [
+        "${config.xdg.config.directory}/wakatime"
+      ];
+    };
+
+    hjem = {
+      pkgs,
+      config,
+      ...
+    }: {
+      packages = [pkgs.wakatime-cli];
+
+      environment.sessionVariables = {
+        WAKATIME_HOME = "${config.xdg.config.directory}/wakatime";
+      };
+    };
+  };
+}
modules/dev/yazi.nix
@@ -0,0 +1,65 @@
+{
+  den.aspects.dev.yazi = {
+    hjem = {pkgs, ...}: let
+      auto-layout = pkgs.yaziPlugins.mkYaziPlugin {
+        pname = "auto-layout.yazi";
+        version = "unstable-25-07-30";
+        src = pkgs.fetchFromGitHub {
+          owner = "luccahuguet";
+          repo = "auto-layout.yazi";
+          rev = "e24bee9f6dd15ff80eae1b3dc1a6b06ee7e66121";
+          hash = "sha256-4vRIGU/ArXhW9ervhyNhpfDN7UF4pqVYnxi6FExlgGk=";
+        };
+        meta = {
+          description = "Automatically change the column layout in yazi based on available window width";
+          homepage = "https://github.com/luccahuguet/auto-layout.yazi";
+          license = pkgs.lib.licenses.mit;
+        };
+      };
+    in {
+      packages = [pkgs.yazi];
+
+      files.".bashrc".text = ''
+        function yy() {
+          local tmp="$(mktemp -t "yazi-cwd.XXXXX")"
+          command yazi "$@" --cwd-file="$tmp"
+          if cwd="$(<"$tmp")" && [ -n "$cwd" ] && [ "$cwd" != "$PWD" ]; then
+            builtin cd -- "$cwd"
+          fi
+          rm -f -- "$tmp"
+        }
+      '';
+
+      xdg.config.files = {
+        "yazi/plugins/auto-layout.yazi".source = auto-layout;
+        "yazi/plugins/git.yazi".source = pkgs.yaziPlugins.git;
+        "yazi/init.lua".text = ''
+          require("git"):setup()
+          require("auto-layout").setup()
+        '';
+
+        "yazi/yazi.toml" = {
+          generator = (pkgs.formats.toml {}).generate "yazi-settings.toml";
+          value = {
+            mgr = {
+              show_hidden = true;
+              sort_dir_first = true;
+            };
+            plugin.prepend_fetchers = [
+              {
+                url = "*";
+                run = "git";
+                group = "git";
+              }
+              {
+                url = "*/";
+                run = "git";
+                group = "git";
+              }
+            ];
+          };
+        };
+      };
+    };
+  };
+}
modules/dev/zoxide.nix
@@ -0,0 +1,21 @@
+{
+  den.aspects.dev.zoxide = {
+    cacheHome = {config, ...}: {
+      directories = [
+        "${config.xdg.data.directory}/zoxide" # Zoxide database
+      ];
+    };
+
+    hjem = {
+      pkgs,
+      lib,
+      ...
+    }: {
+      packages = [pkgs.zoxide];
+
+      files.".bashrc".text = lib.mkOrder 2000 ''
+        eval "$(${lib.getExe pkgs.zoxide} init bash)"
+      '';
+    };
+  };
+}
modules/hosts/kevin/default.nix
@@ -1,4 +1,4 @@
-{
+{den, ...}: {
   den.hosts.kevin = {
     system = "x86_64-linux";
     description = ''
@@ -14,4 +14,10 @@
 
     settings.secret.pubKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOO9CyAqGo/WbJkncrt1a9jxS1E+hd550SC4A08I/l0/ root@kevin";
   };
+
+  den.aspects.kevin = {
+    includes = with den.aspects; [
+      dev
+    ];
+  };
 }
modules/users/hpcesia/dev.nix
@@ -0,0 +1,36 @@
+{den, ...}: let
+  includeWhen = cond: aspects:
+    map (aspect: (den.lib.policy.when cond aspect)) aspects;
+in {
+  den.aspects.hpcesia = {
+    includes = includeWhen ({host, ...}: host.hasAspect den.aspects.dev) (with den.aspects; [
+      dev.agents.opencode
+      dev.direnv
+      dev.editor.helix
+      dev.jujutsu
+      dev.wakatime
+      dev.yazi
+      dev.zellij
+      dev.zoxide
+
+      {
+        name = "hpcesia/dev/git";
+        hjem = {
+          xdg.config.files."git/config".value = {
+            url = {
+              "ssh://git@github.com/HPCesia" = {
+                insteadOf = "https://github.com/HPCesia";
+              };
+              "ssh://git@codeberg.org/HPCesia" = {
+                insteadOf = "https://codeberg.org/HPCesia";
+              };
+              "ssh://git@git.net.trin.one/HPCesia" = {
+                insteadOf = "https://git.net.trin.one/HPCesia";
+              };
+            };
+          };
+        };
+      }
+    ]);
+  };
+}