chore: coalesce coding agent markdownfiles

This commit is contained in:
Bogdan Buduroiu 2026-09-10 11:03:25 +08:00
commit 58c75095db
Signed by: bogdan
GPG key ID: F71EF75A4C09A095
51 changed files with 230 additions and 156 deletions

2
.gitignore vendored
View file

@ -8,7 +8,7 @@ scripts/
# Skill helper scripts are source, not scratch: SKILL.md files reference them
# and home/terminal/programs/claude/scripts.nix builds them into PATH binaries.
# An ignored file is invisible to jj *and* to the git-backed flake.
!home/terminal/programs/claude/config/skills/*/scripts/
!home/terminal/programs/coding-agents/skills/*/scripts/
docs/
/niri/
result

12
flake.lock generated
View file

@ -1505,11 +1505,11 @@
},
"nixpkgs-unstable": {
"locked": {
"lastModified": 1788614874,
"narHash": "sha256-7QYjT2vHLuX9Z1pdxHXDKCbh1CR3D/2rywB9Tx0MPRg=",
"lastModified": 1788881743,
"narHash": "sha256-2V9GZGvPfrNzxFozhI9dcqV+c3QdA8YZrvAAzqEB+dI=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "c043004d1c6985732bcc1cbc5a9c9aecbbb4e0f0",
"rev": "d6524aaca2ff07876657ae2b323f24be4874944b",
"type": "github"
},
"original": {
@ -1646,11 +1646,11 @@
},
"nixpkgs_8": {
"locked": {
"lastModified": 1788584326,
"narHash": "sha256-Fd3OB8J9JhgliQwOKcqx4M672CInxi1I5VnwsaXeSQo=",
"lastModified": 1788921488,
"narHash": "sha256-8+xWRxEkD6l217cIUdRxfeUGS9lQX0hVtUuNVsBaDzk=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "6713828a351efa628b025a1adf7f43cbf8597513",
"rev": "6aefcda9401be8acc2b74244fb3b37520ea1f0a8",
"type": "github"
},
"original": {

View file

@ -6,6 +6,8 @@
}:
let
codingAgents = import ../coding-agents { inherit lib pkgs; };
# Hook scripts live in ./config/hooks/*.sh; hooks.nix wraps each one in a
# writeShellApplication (deps pinned, shellchecked at build time) and exports
# the settings.json registration graph that actually makes them fire.
@ -51,12 +53,12 @@ in
home.file = {
"${workConfigDir}/settings.json".source =
config.home.file."${config.programs.claude-code.configDir}/settings.json".source;
"${workConfigDir}/CLAUDE.md".source = ./CLAUDE.md;
"${workConfigDir}/CLAUDE.md".source = codingAgents.context;
# recursive: recreate the directory and symlink each file individually,
# so the directory itself stays writable — `/agents` can still add one at
# runtime. A bare `source = ./dir` would symlink the whole read-only dir.
"${workConfigDir}/agents" = {
source = ./config/agents;
source = codingAgents.claudeAgents;
recursive = true;
};
"${workConfigDir}/rules" = {
@ -64,7 +66,7 @@ in
recursive = true;
};
"${workConfigDir}/skills" = {
source = ./config/skills;
source = codingAgents.skills;
recursive = true;
};
};
@ -82,16 +84,16 @@ in
# `memory.source`/`memory.text` were collapsed into a single `context`
# option (either lines or a path); a path still lands as a symlink, a
# string would be written inline.
context = ./CLAUDE.md;
context = codingAgents.context;
# agentsDir/rulesDir are dir-path options in 26.05 (no warning).
agentsDir = ./config/agents;
agentsDir = codingAgents.claudeAgents;
rulesDir = ./config/rules; # replaces the old home.file.".claude/rules" workaround
# No hooksDir: it only symlinks the scripts into ~/.claude/hooks as 0444
# (not executable, and not registered anywhere). settings.hooks below points
# at the built store paths instead — see hooks.nix.
# skillsDir was renamed to `skills` in 26.05 (accepts a dir path or attrset).
skills = ./config/skills;
skills = codingAgents.skills;
# MCP servers configuration — Nix store paths resolved at build time
mcpServers = {

View file

@ -1,7 +1,7 @@
# Scripts that ship inside a skill directory *and* as binaries on PATH.
#
# A skill's SKILL.md may reference a helper script. Kept next to the skill
# (config/skills/<name>/scripts/*.sh) it travels with the skill when the
# (../coding-agents/skills/<name>/scripts/*.sh) it travels with the skill when the
# directory is copied to a colleague and runs with plain `bash`. Built here
# with writeShellApplication it also becomes an executable store path with
# pinned deps and a build-time shellcheck — the same pattern as hooks.nix,
@ -21,7 +21,7 @@ let
text = lib.removePrefix "#!/usr/bin/env bash\n" (builtins.readFile file);
};
jjStackCheck = mkScript "jj-stack-check" ./config/skills/jj-stack/scripts/stack-check.sh [
jjStackCheck = mkScript "jj-stack-check" ../coding-agents/skills/jj-stack/scripts/stack-check.sh [
pkgs.jujutsu
pkgs.coreutils
pkgs.gnugrep

View file

@ -1,113 +0,0 @@
{ config, pkgs, ... }:
{
# OpenAI Codex CLI, driven by the ChatGPT Plus/Pro subscription rather than a
# platform API key. This is the same subscription opencode's `openai` provider
# uses (see ./opencode/default.nix), but the two keep separate credentials —
# codex under $CODEX_HOME, opencode under ~/.local/share/opencode.
#
# `programs.codex` writes ~/.codex/config.toml. It would move to
# ~/.config/codex only if `home.preferXdgDirectories` were enabled, which this
# config does not set — so CODEX_HOME stays at its default and no session
# variable is exported.
#
# Every key below was checked against codex 0.146.0 by feeding it a bogus
# value and reading the rejected-variant list back out of the binary; the
# enum comments are those lists verbatim. This matters because codex parses
# config.toml with untagged enums that silently swallow unknown *table*
# fields — `tools.web_search = { mode = "nonsense" }` loads without complaint
# and does nothing. Only scalar forms are validated, so scalars are what this
# file uses.
programs.codex = {
enable = true;
# Overlaid to nixpkgs-unstable in system/nix/nixpkgs.nix — 26.05 pins 0.133,
# which predates most of the schema below.
package = pkgs.codex;
# Auth is a one-time interactive step that cannot be declared in Nix (it
# mints a refresh token into $CODEX_HOME/auth.json):
# codex login # browser OAuth against the ChatGPT account
# codex login status # verify
# `forced_login_method` is the guard rail that keeps it that way: with it
# set to "chatgpt", an OPENAI_API_KEY sitting in the environment cannot
# quietly redirect this CLI onto metered platform billing.
# Variants: chatgpt | api
settings = {
forced_login_method = "chatgpt";
# `model` is deliberately unset. codex 0.146 resolves the default from a
# server-side models manager tied to the account, so hardcoding a slug
# here only risks pinning to something the subscription tier has moved
# past. Override per-session with `codex -m <model>`; the slugs this
# build knows are gpt-5.6-sol / -terra / -luna, gpt-5.5, gpt-5.4(-mini),
# gpt-5.2.
# Free-form string, not an enum — codex forwards it and lets the model
# reject what it does not support. Practical range across the gpt-5.x
# family: none | minimal | low | medium | high | xhigh.
model_reasoning_effort = "medium";
# Variants: auto | concise | detailed | none
model_reasoning_summary = "auto";
# Variants: low | medium | high
model_verbosity = "medium";
# Variants: untrusted | on-failure | on-request | granular | never
# "on-request" lets the model work uninterrupted inside the sandbox and
# ask only when it needs to step outside it — the closest match to the
# opencode permission set next door, which allows by default and puts
# `ask` on the destructive and network-touching commands.
approval_policy = "on-request";
# Variants: read-only | workspace-write | danger-full-access
sandbox_mode = "workspace-write";
sandbox_workspace_write = {
# Required on NixOS more than most places: nearly every useful command
# here is `nix run nixpkgs#x`, `nix build`, or `nix develop`, and all of
# them fetch from cache.nixos.org on first use.
network_access = true;
# The nix client writes eval and fetcher caches outside the workspace;
# without these roots `nix build` fails on a cache write rather than on
# anything to do with the build itself. The store itself is absent by
# design — nix-daemon does that writing from outside the sandbox.
writable_roots = [
"${config.home.homeDirectory}/.cache/nix"
"${config.home.homeDirectory}/.local/state/nix"
];
};
# Boolean form on purpose. The table form (`{ mode = "indexed" }`) parses
# but is not validated — see the header note.
tools.web_search = true;
tui = {
notifications = true;
# Variants: auto | osc9 | bel
notification_method = "auto";
# Variants: unfocused | always
notification_condition = "unfocused";
};
# Variants: vscode | vscode-insiders | windsurf | cursor | none
# None of the supported openers is nvim, and a wrong one spawns an editor
# that is not installed, so file references stay plain paths.
file_opener = "none";
};
# $CODEX_HOME/AGENTS.md — codex's equivalent of ~/.claude/CLAUDE.md, loaded
# into every session. Pointed at the same file claude-code uses rather than
# a copy: the NixOS operating manual (no FHS, no system python/node, reach
# for `nix run`/`direnv exec`) is the one piece of context an agent on this
# host cannot work without, and two drifting copies of it would be worse
# than one shared one.
context = ./claude/CLAUDE.md;
# No MCP servers. claude-code and opencode both carry the datadog/deepwiki/
# linear/notion set; codex stays a fast local coding agent, which also
# avoids four `codex mcp login` OAuth dances. Add them under
# `settings.mcp_servers.<name>.url` if that changes — 0.146 speaks
# streamable HTTP natively, no rmcp feature flag needed.
};
}

View file

@ -0,0 +1,140 @@
{
config,
lib,
pkgs,
...
}:
let
codingAgents = import ../coding-agents { inherit lib pkgs; };
codexConfigDir = "${config.home.homeDirectory}/.codex";
codexSettings = {
forced_login_method = "chatgpt";
model_reasoning_effort = "medium";
model_reasoning_summary = "auto";
model_verbosity = "medium";
personality = "pragmatic";
approval_policy = "on-request";
sandbox_mode = "workspace-write";
sandbox_workspace_write = {
network_access = true;
writable_roots = [
"${config.home.homeDirectory}/.cache/nix"
"${config.home.homeDirectory}/.local/state/nix"
];
};
web_search = "live";
check_for_update_on_startup = false;
shell_environment_policy.ignore_default_excludes = false;
agents = {
enabled = true;
}
// codingAgents.codexAgents;
tui = {
notifications = true;
notification_method = "auto";
notification_condition = "unfocused";
};
file_opener = "none";
};
codexConfig = (pkgs.formats.toml { }).generate "codex-config.toml" codexSettings;
codexConfigBacking = "${codexConfigDir}/config.toml.mutable";
codexConfigMerger = pkgs.writeText "merge-codex-config.py" ''
import sys
import tomllib
import tomli_w
runtime_path, managed_path, output_path = sys.argv[1:]
with open(runtime_path, "rb") as runtime_file:
runtime = tomllib.load(runtime_file)
with open(managed_path, "rb") as managed_file:
merged = tomllib.load(managed_file)
for runtime_key in ("projects", "notice"):
if runtime_key in runtime and runtime_key not in merged:
merged[runtime_key] = runtime[runtime_key]
with open(output_path, "wb") as output_file:
tomli_w.dump(merged, output_file)
'';
codexConfigPython = pkgs.python3.withPackages (ps: [ ps.tomli-w ]);
in
{
# OpenAI Codex CLI, driven by the ChatGPT Plus/Pro subscription rather than a
# platform API key. This is the same subscription opencode's `openai` provider
# uses (see ./opencode/default.nix), but the two keep separate credentials —
# codex under $CODEX_HOME, opencode under ~/.local/share/opencode.
#
# CODEX_HOME stays at its default, ~/.codex, because this configuration does
# not enable home.preferXdgDirectories. Settings are checked against Codex
# 0.151.0 and its published JSON schema.
programs.codex = {
enable = true;
# Overlaid to nixpkgs-unstable in system/nix/nixpkgs.nix — 26.05 pins 0.133,
# which predates most of the schema below.
package = pkgs.codex;
# Auth is a one-time interactive step that cannot be declared in Nix (it
# mints a refresh token into $CODEX_HOME/auth.json):
# codex login # browser OAuth against the ChatGPT account
# codex login status # verify
# `forced_login_method` is the guard rail that keeps it that way: with it
# set to "chatgpt", an OPENAI_API_KEY sitting in the environment cannot
# quietly redirect this CLI onto metered platform billing.
# Variants: chatgpt | api
# Kept empty because Home Manager otherwise creates an immutable symlink.
# Codex writes project trust decisions to config.toml at runtime; the
# activation below materialises codexSettings as a normal writable file.
settings = { };
# The model remains unset so Codex can use the account's current default.
# Override a session with `codex -m <model>` when needed.
# $CODEX_HOME/AGENTS.md — codex's equivalent of ~/.claude/CLAUDE.md, loaded
# into every session. Pointed at the same file claude-code uses rather than
# a copy: the NixOS operating manual (no FHS, no system python/node, reach
# for `nix run`/`direnv exec`) is the one piece of context an agent on this
# host cannot work without, and two drifting copies of it would be worse
# than one shared one.
context = codingAgents.context;
skills = codingAgents.skills;
# No MCP servers. Claude Code and OpenCode carry the remote integrations;
# Codex stays a local coding agent and avoids separate OAuth sessions.
};
# Home Manager owns the stable link while Codex writes through it to the
# mutable backing file. This remains rollback-safe because config.toml itself
# is still a Home Manager-managed symlink.
home.file.".codex/config.toml".source = config.lib.file.mkOutOfStoreSymlink codexConfigBacking;
# On declarative changes, retain runtime-only top-level tables such as
# [projects] and [notice], then replace managed keys with the new values.
home.activation.codexWritableConfig = lib.hm.dag.entryBefore [ "linkGeneration" ] ''
run mkdir -p "${codexConfigDir}"
managedSource="${codexConfigDir}/.config.toml.hm-source"
previousSource=""
if [ -f "$managedSource" ]; then
previousSource="$(cat "$managedSource")"
fi
if [ ! -e "${codexConfigBacking}" ] || [ "$previousSource" != "${codexConfig}" ]; then
runtimeConfig="${codexConfigBacking}"
if [ ! -f "$runtimeConfig" ] && [ -f "${codexConfigDir}/config.toml" ]; then
runtimeConfig="${codexConfigDir}/config.toml"
fi
if [ -f "$runtimeConfig" ]; then
run ${codexConfigPython}/bin/python ${codexConfigMerger} "$runtimeConfig" "${codexConfig}" "${codexConfigBacking}.new"
else
run install -m 0600 "${codexConfig}" "${codexConfigBacking}.new"
fi
run install -m 0600 "${codexConfigBacking}.new" "${codexConfigBacking}"
run rm -f "${codexConfigBacking}.new"
run sh -c 'printf "%s\n" "$1" > "$2"' -- "${codexConfig}" "$managedSource"
fi
'';
}

View file

@ -0,0 +1,66 @@
{ lib, pkgs }:
let
agentDir = ./agents;
agentFiles = lib.filterAttrs (name: type: type == "regular" && lib.hasSuffix ".md" name) (
builtins.readDir agentDir
);
agentNames = map (lib.removeSuffix ".md") (builtins.attrNames agentFiles);
sourceFor = name: agentDir + "/${name}.md";
linesFor = name: lib.splitString "\n" (builtins.readFile (sourceFor name));
fieldFor =
name: field:
lib.removePrefix "${field}: " (
lib.findFirst (lib.hasPrefix "${field}: ") "${field}:" (linesFor name)
);
dropUntil =
predicate: list:
if list == [ ] || predicate (builtins.head list) then
list
else
dropUntil predicate (builtins.tail list);
bodyFor =
name:
lib.concatStringsSep "\n" (
lib.drop 1 (dropUntil (line: line == "---") (lib.drop 1 (linesFor name)))
);
hasTool = name: tool: lib.hasInfix tool (fieldFor name "tools");
isWritable = name: hasTool name "Write" || hasTool name "Edit";
needsWritableSandbox = name: isWritable name || hasTool name "Bash";
opencodeAgent = name: ''
---
description: ${builtins.toJSON (fieldFor name "description")}
mode: subagent
model: openai/gpt-5.6-sol
permission:
read: ${if hasTool name "Read" then "allow" else "deny"}
edit: ${if isWritable name then "allow" else "deny"}
glob: ${if hasTool name "Glob" then "allow" else "deny"}
grep: ${if hasTool name "Grep" then "allow" else "deny"}
bash: ${if hasTool name "Bash" then "allow" else "deny"}
task: deny
webfetch: deny
websearch: deny
---
${bodyFor name}
'';
toml = pkgs.formats.toml { };
codexAgent = name: {
description = fieldFor name "description";
config_file = toml.generate "codex-agent-${name}.toml" {
developer_instructions = bodyFor name;
sandbox_mode = if needsWritableSandbox name then "workspace-write" else "read-only";
};
};
in
{
context = ./AGENTS.md;
skills = ./skills;
claudeAgents = agentDir;
opencodeAgents = lib.genAttrs agentNames opencodeAgent;
codexAgents = lib.genAttrs agentNames codexAgent;
}

View file

@ -7,7 +7,7 @@
./btop.nix
./claude
./cli.nix
./codex.nix
./codex
./delta.nix
./direnv.nix
./eza.nix

View file

@ -1,23 +0,0 @@
---
description: Reviews code changes for bugs, security issues, and maintainability. Use after implementation to vet diffs before commit or merge.
mode: subagent
model: openai/gpt-5.6-sol
temperature: 0.1
tools:
write: false
edit: false
bash: true
---
You are a senior engineer reviewing a code change. You may run read-only
commands (git diff, git log, tests) but never modify files.
Review priorities, in order:
1. Correctness — bugs, unhandled edge cases, broken invariants, race conditions
2. Security — injection, secret leakage, unsafe permissions, unvalidated input
3. Maintainability — naming, duplication, dead code, needless complexity
For each finding give: file:line, the problem in one sentence, and a concrete fix.
Distinguish blocking issues from nitpicks. If the change looks good, say so
plainly — do not invent findings.

View file

@ -7,6 +7,7 @@
}:
let
codingAgents = import ../coding-agents { inherit lib pkgs; };
lspDeps = import ./lsp-deps.nix { inherit pkgs; };
secretsFile = "${inputs.secrets}/secrets/common/api-keys.yaml";
in
@ -33,13 +34,12 @@ in
"4096"
];
agents = {
agents = codingAgents.opencodeAgents // {
plan = ./agents/plan.md;
code = ./agents/code.md;
code-review = ./agents/code-review.md;
};
skills = { };
skills = codingAgents.skills;
settings = {
# `openai` here is the ChatGPT Plus/Pro (Codex) subscription, not an API
@ -129,7 +129,9 @@ in
};
autoshare = false;
autoupdate = true;
# The package is upgraded through nixpkgs, not by mutating the store.
autoupdate = false;
instructions = [ codingAgents.context ];
# gpt-5.5 rather than the newer gpt-5.6-sol: under OAuth opencode
# hard-allow-lists gpt-5.5, so it is the one Codex-subscription model
# guaranteed to survive the provider's model filter. gpt-5.6-sol is