Probe: marks prompt.compose and prompt.section output with sentinels

Stacked commit workflows, MCP servers, and declarative configuration for AI coding CLIs (Claude Code, Codex, Copilot, Kiro). Works without Nix; Nix unlocks overlays, home-manager modules, and devenv modules.
Prerequisites: git-branchless, git-absorb, git-revise.
# Claude Code
cp -r packages/stacked-workflows/skills/stack-* .claude/skills/
# OpenAI Codex
cp -r packages/stacked-workflows/skills/stack-* .agents/skills/
# GitHub Copilot
cp -r packages/stacked-workflows/skills/stack-* .github/skills/
# Kiro
cp -r packages/stacked-workflows/skills/stack-* .kiro/skills/
Each skill is self-contained with a SKILL.md and bundled reference docs.
# flake.nix
inputs.nix-agentic-tools = {
url = "github:higherorderfunctor/nix-agentic-tools";
};
# Optional: `pkgs.ai.*` for your own use. The modules install this flake's
# builds without it.
nixpkgs.overlays = [inputs.nix-agentic-tools.overlays.default];
# Unfree: claude-code, copilot-cli, kimchi-docs, kiro-cli and kiro-cli-workflows.
# Allow them in the nixpkgs.config your pkgs comes from (NixOS's when
# useGlobalPkgs is set).
# Home-manager config
imports = [inputs.nix-agentic-tools.homeManagerModules.default];
ai = {
claude.enable = true;
codex = {
enable = true;
settings.model = "gpt-5.6-sol";
};
copilot.enable = true;
kiro.enable = true;
programs.delegate-routing.enable = true;
programs.stacked-workflows.enable = true;
settings.reasoningEffort = "high";
};
# Git companion: mkDefault values on the git.* options, which Home Manager
# applies user-global and devenv at repository scope.
stacked-workflows.gitPreset = "full";
services.mcp-servers.servers.github-mcp = {
enable = true;
settings.credentials.file = "/run/secrets/github-token";
};
Static runtime files: every runtime exposes
ai.<runtime>.files."<relative-path>" = { text = "…"; };. An entry can instead setsource = ./file. Generated context/rule outputs use the same final map at default priority, so an ordinary whole entry replaces them andnullsuppresses them. Paths are relative to HOME here and to the project under devenv.
# devenv.yaml
# Unfree: claude-code, copilot-cli, kimchi-docs, kiro-cli and kiro-cli-workflows.
# Opt in as for nixpkgs.
allowUnfree: true
inputs:
nix-agentic-tools:
url: github:higherorderfunctor/nix-agentic-tools
# devenv.nix
{inputs, ...}: {
imports = [inputs.nix-agentic-tools.devenvModules.nix-agentic-tools];
# Optional: the modules install this flake's builds without it. Apply it
# to use or override `pkgs.ai.*` yourself; the modules then install your
# `pkgs.ai.*`. `package = null` configures a runtime without installing one.
overlays = [inputs.nix-agentic-tools.overlays.default];
ai = {
claude.enable = true;
codex.enable = true;
mcpServers.github-mcp = {
type = "stdio";
command = "github-mcp-server";
args = ["--stdio"];
};
};
}
CI builds every package here with this flake's own nixpkgs and pushes the results to nix-agentic-tools.cachix.org. The overlay and the module package defaults hand you those same builds, so they come from the cache whatever nixpkgs you use.
Add the cache to your own Nix configuration. This flake's nixConfig lists it, but Nix ignores a flake's substituters unless you are a trusted user.
# NixOS (or nix-darwin)
nix.settings = {
extra-substituters = ["https://nix-agentic-tools.cachix.org"];
extra-trusted-public-keys = ["nix-agentic-tools.cachix.org-1:0jFprh5fkDez9mk6prYisYxzalr0hn78kyywGPXvOn0="];
};
# nix.conf
extra-substituters = https://nix-agentic-tools.cachix.org
extra-trusted-public-keys = nix-agentic-tools.cachix.org-1:0jFprh5fkDez9mk6prYisYxzalr0hn78kyywGPXvOn0=
The packages bring this flake's runtime base (glibc, bash, and so on, about 100 MB) next to your own, from cache.nixos.org, shared by every package here. Module defaults also evaluate a second nixpkgs, like any flake whose modules default to its own packages.
nixpkgs input builds the overlay, the module defaults, packages and legacyPackages. Your own overlays on shared dependencies do not reach these packages, and security fixes arrive when this flake bumps nixpkgs (the update sweep runs four times a day). Go and Rust compilers come from this flake's locked toolchain inputs.allowUnfree, allowUnfreePredicate, ...), with nixpkgs' own error when it refuses, and meta.available reports that verdict. The package set itself, dependencies included, is built once with this flake's nixpkgs. The check never changes a store path, so it costs no cache hits, and there is nothing to keep in sync. Set them where you set them for nixpkgs (nixpkgs.config, or devenv.yaml allowUnfree). A checkMeta = true config on a nixpkgs older than this flake's may reject newer meta keys; checkMeta is a nixpkgs-CI setting, default false.packages.<system> is free packages only. legacyPackages.<system> has every package plus the nested ai tree. nix run on an unfree package resolves there and needs your opt-in, as in nixpkgs: NIXPKGS_ALLOW_UNFREE=1 nix run --impure github:higherorderfunctor/nix-agentic-tools#claude-code..override. Overriding a package's bun, pnpm, Go or Rust works and costs a rebuild of that package.follows is the opt-out. Setting inputs.nix-agentic-tools.inputs.nixpkgs.follows = "nixpkgs" rebuilds every package on your nixpkgs, with no cache. You then own breakage where a recipe borrows nixpkgs' recipe text, patches or fetchers: tsgolint's patch list, kiro-cli's install step, the pnpm fetcher versions, Go vendorHash.aarch64-darwin and x86_64-linux), cross builds, and musl or static package sets, the overlay builds on your package set.Delegate routing for models and effort, plus stacked commit workflows using git-branchless, git-absorb, and git-revise.
<!-- prettier-ignore -->
| Skill | Description |
|---|---|
/delegate-routing | Size model and effort before calling subagents or building workflows |
/kimchi-docs | Search the pinned Kimchi docs snapshot and independently pinned workflows source, docs and examples; enable via ai.programs.kimchi-docs.enable |
/peer-communication | Write replies a person reads: answer first, plain words, easy to scan |
/stack-fix | Absorb fixes into correct stack commits |
/stack-plan | Plan and build a commit stack from description or existing commits |
/stack-split | Split a large commit into reviewable atomic commits |
/stack-submit | Sync, validate, push stack, and create stacked PRs |
/stack-summary | Analyze stack quality, flag violations, produce planner-ready summary |
/stack-test | Run tests or formatters across commits in a stack |
<!-- prettier-ignore -->
| Server | Description | Credentials |
|---|---|---|
aihubmix-mcp | AIHubMix image and video generation | Required |
context7-mcp | Library documentation lookup | None |
effect-mcp | Effect-TS documentation | None |
git-intel-mcp | Git repository analytics | None |
github-mcp | GitHub platform integration | Required |
gitlab-mcp | GitLab platform integration | Required |
kagi-mcp | Kagi search and summarization | Required |
mcp-language-server | LSP-to-MCP bridge | None |
mcp-proxy | stdio-to-HTTP bridge proxy | None |
nixos-mcp | NixOS and Nix documentation | None |
semble-mcp | Local semantic and lexical code search | None |
nix build .#github-mcp
<!-- prettier-ignore -->
| Package | Description |
|---|---|
agnix | Linter, LSP, and MCP for AI config files |
git-absorb | Automatic fixup commit routing |
git-branchless | Anonymous branching, in-memory rebases |
git-revise | In-memory commit rewriting |
nix build .#git-absorb
The same git.* options exist on Home Manager and devenv. Each tool's settings are typed from a census of its source and mirror the git key; enable installs the tool:
git = {
absorb = {
enable = true;
settings.maxStack = 50; # absorb.maxStack
};
branchless = {
enable = true; # devenv also runs `git branchless init` on entry
scopedSync = true; # bare `git sync` moves the current stack only
settings.test.strategy = "worktree"; # branchless.test.strategy
};
settings.merge.conflictStyle = "zdiff3"; # any other git key
};
Home Manager delivers them through programs.git.settings (git.settings is an alias of it). devenv writes a repository-local include kept after every other repository setting, so its values win key by key over user-global ones and hand edits, while keys it does not set fall through.
Agent-adjacent development utilities exposed as pkgs.ai.devTools.*.
<!-- prettier-ignore -->
| Package | Description |
|---|---|
beads | Graph-based issue tracker for AI coding agents |
gh | GitHub CLI |
glab | GitLab CLI |
markdownlint-cli2 | Configuration-based markdown linter (markdownlint) |
microvm | The microvm.nix CLI for managing declared MicroVMs |
oxlint | Fast JS/TS linter with type-aware (tsgo) linting and JS plugins |
rumdl | Fast Rust markdown linter (markdownlint-compatible rules) |
tsgolint | Type-aware linting backend for oxlint (typescript-go) |
nix build .#oxlint
Temporarily unclassified supporting packages live in the split-ready packages/<owner>/packages/ai/generic/ trees and are exposed as pkgs.ai.generic.*.
<!-- prettier-ignore -->
| Package | Description |
|---|---|
arkenfox | Hardened Firefox user.js preference set |
bruno | Open-source IDE for exploring and testing APIs |
btop | Resource monitor for processes, CPU, memory, disks and network |
bun | JavaScript runtime, bundler, transpiler and package manager |
catppuccin-btop | Catppuccin theme files for btop |
dns-root-hints | IANA DNS root name server hints (named.root) |
fblog | Command-line JSON log viewer |
gluetun | VPN client for multiple providers (Linux only) |
iron-proxy | Egress proxy for sandboxed agents: allowlisted hosts and secret injection |
oh-my-posh | Prompt theme engine for any shell |
otel-tui | Terminal OpenTelemetry viewer |
pipelock | Agent egress firewall: forward proxy with hostname, SSRF and DLP checks |
pnpm_10 | Fast, disk-space-efficient JavaScript package manager (10.x) |
pnpm_11 | Fast, disk-space-efficient JavaScript package manager (11.x) |
pnpm_12 | Fast, disk-space-efficient JavaScript package manager (12.x) |
nix build .#dns-root-hints
<!-- prettier-ignore -->
| Package | Description |
|---|---|
chatgpt-codex | OpenAI Codex CLI |
claude-code | Claude Code CLI |
copilot-cli | GitHub Copilot CLI |
kimchi | Kimchi CLI with optional external source-built workflows (ai.kimchi.extensions.workflows) |
kiro-cli | Kiro CLI |
kiro-gateway | Python proxy API for Kiro |
semble | Local semantic and lexical code-search CLI |
<!-- prettier-ignore -->
| Package | Description |
|---|---|
coding-standards | Reusable coding standard fragments (DRY, conventional commits, etc.) |
delegate-routing-content | Per-runtime model/effort sizing skills and a short routing rule |
stacked-workflows-content | Skills, references, and skill-routing fragment |
Content packages are derivations with passthru.fragments for composable instruction building.
<!-- prettier-ignore -->
| Feature | Without Nix | Home-Manager | DevEnv |
|---|---|---|---|
| Delegate routing | Copy a generated runtime skill | ai.programs.delegate-routing.enable (Claude + Codex + Kimchi + Kiro) | Same; project-native paths |
| Peer communication | Copy skills/ | On by default; ai.programs.peer-communication.enable = false or .runtimes.<runtime>.enable = false turns it off | Same; project-native paths |
| Stacked workflow skills | Copy skills/ | ai.programs.stacked-workflows.enable | ai.programs.stacked-workflows.enable |
| MCP server packages | Install manually | nix build .#<server> | nix build .#<server> |
| Unified MCP config | Manual native config | ai.mcpServers.* (all five CLIs) | ai.mcpServers.* (all five CLIs) |
| Typed MCP settings | N/A | Shared schema + native extensions | Shared schema + native extensions |
| MCP credentials | Manual env vars | plain, file, or helper | plain, file, or helper |
| Semble search integrations | Manual install | ai.programs.semble (Claude + Codex + Kiro) | Same; project-native paths |
| Git tool packages | Install manually | Overlay + nix build | Overlay + nix build |
| Git configuration | git config | git.settings + typed git.{branchless,absorb,revise}.settings → programs.git.settings | Same options; a repository-local include that wins key by key |
| GitLab CLI config | glab config set | glab.* | glab.* |
| GitLab CLI credentials | Manual env vars | plain, file or helper | plain, file or helper |
| Context and rules | Copy native files | ai.{context,rules} (runtime capability-gated) | Same; project-native paths. Files a repository commits (AGENTS.md, .github/ instructions) and Kiro steering are read-only copies, not store links |
| Generated files | N/A | ai.formatter, ai.guards.<name>, and ai.checks (Nix-owned build-time files; formatters exclude supplied skill trees, checks include their generated entries; runtime-rendered files excluded) | Same; project-native static files included |
| Skills | Copy native directories | ai.skills.* (all five CLIs) | Same; project-native paths |
| Portable reasoning effort | Per-CLI config | ai.settings.reasoningEffort (Claude + Codex + Copilot + Kimchi) | Same; Copilot's lands in .github/copilot/settings.json, which only its interactive session reads, Kimchi's in its project harness settings (see below). Kiro has only per-model native effort |
| Semantic agents | Per-CLI config | ai.agents.* (Claude + Codex + Copilot + Kimchi + Kiro) | Same; project-native paths |
| Portable lifecycle hooks | Per-CLI config | ai.hooks.* (Claude + Codex) | Same, plus Kimchi's project .kimchi/hooks.json |
| LSP server config | Per-CLI config | ai.lspServers.* (Claude + Copilot + Kiro) | Copilot + Kiro; Claude has no project LSP route (warns); Codex has no native LSP registry |
| CLI process environment | Shell config | ai.environmentVariables (Codex + Copilot + Kimchi + Kiro) | Same; baked into each launcher wrapper, never the shell. Claude uses ai.claude.native.settings.env |
| Command shell | Per-CLI config or $SHELL | ai.shell / per-runtime shell (ai.kiro.cli.shell for Kiro; Claude + Codex + Kiro) | Same; takes a package. Copilot and Kimchi are explicit exclusions |
| Fragment composition | N/A | lib.ai.compose | lib.ai.compose |
| Pool | devenv delivery | Boundary |
|---|---|---|
| Context | root AGENTS.md | Available without project trust; reader walks ancestors, but the wrapper remains root-only |
| MCP servers | .kimchi/mcp.json | Requires project trust and launch from the devenv root |
| Kimchi settings | .kimchi/config.json | Requires project trust and launch from the devenv root; an owner-only copy. region and telemetry.enabled reach Kimchi through the launcher environment instead |
| Skills | .kimchi/skills | Requires project trust; nearest ancestor wins, but the wrapper remains root-only |
| Project harness settings | .config/kimchi/harness/settings.json | Requires project trust and launch from the devenv root; user-scope-only keys are rejected during evaluation. ai.settings.reasoningEffort lands here as defaultThinkingLevel, so setting it alone creates the file |
| Agents | .kimchi/agents/<name>.md | Requires project trust and launch from the devenv root; Kimchi's /agents commands cannot edit a declared agent |
| Permissions | .kimchi/permissions.json | Requires project trust and launch from the devenv root |
| Hooks | .kimchi/hooks.json | Requires project trust and launch from the devenv root; PermissionRequest is not a Kimchi event and is left out. Home Manager has no user-scope hook file it can own, so shared ai.hooks do not reach Kimchi there (silently) and ai.kimchi.hooks warns |
devenv rejects Kimchi's user-scope-only harness settings: defaultProjectTrust, fermentV2, hidePhaseChanges, httpProxy, lastTerminalWarnings, modelMetadata, modelRoles, multiModel, resources, shellProfileApiKeyMigrationDismissed, statusLine. Set those with Home Manager or through Kimchi itself. Every project file is a read-only copy, written only when something is declared; an in-app change Kimchi renames over one is backed up and replaced at the next shell entry.
Kimchi's user-global files live under ~/.config/kimchi, which devenv never writes. Who manages each setting there depends on whether you use Home Manager:
| User-global setting | Home Manager | devenv only |
|---|---|---|
Harness settings.json | Nix owns the keys it declares inside Kimchi's own file; /model and other in-app writes persist | Kimchi |
mcp.json, permissions.json | Nix, as read-only copies; in-app changes are reset at the next activation | Kimchi |
Project trust (harness/trust.json) | Nix (ai.kimchi.projectTrust); defaultProjectTrust = "never", and /trust cannot persist (it may exit) | Kimchi's trust prompt |
telemetry.enabled, region | Nix; telemetry defaults off, region must be declared, and Kimchi reads both from global config.json | Kimchi, unless declared: then the launcher passes the Nix value |
skillPaths | Nix; default skill paths include the ai.* directory | Kimchi |
| API key | Nix when ai.kimchi.apiKey is set (read from its secret at launch, so an in-app login has no effect); otherwise /login, which persists in config.json beside the Nix-owned keys | Same as Home Manager |
| Git tokens | Nix for each host in ai.kimchi.gitTokens (read from secrets at activation), which makes config.json a read-only copy again, so /login stops persisting; Kimchi for the rest | Kimchi |
config.json migration, onboarding and survey markers | Kimchi; they persist beside the Nix-owned keys | Kimchi |
| Device id | Kimchi; none is written while telemetry is off | Kimchi |
Project settings, MCP servers, harness settings, permissions, agents, and hooks resolve under the exact working directory. The devenv wrapper rejects descendant launches instead of silently missing them. Context and skills walk ancestors, so a devenv that declares none of the exact-cwd files leaves the launch directory unrestricted. skillPaths defau
hooks/register.ts 26 lines1import type { Register } from "claude-code";
2
3// Probe mod for the delegate map. prompt.compose: appends one session section
4// (COMPOSE-4242) after what the engine composed. prompt.section: appends a
5// SECTION-4343 marker naming each section the engine resolves.
6export const register: Register = (on) => {
7 on("prompt.compose", async ($, e, next) => {
8 const result = await next(e);
9 return {
10 sections: [
11 ...result.sections,
12 {
13 id: "prompt-mw:tail",
14 text: `Compose sentinel: COMPOSE-4242 traits=${e.traits.join(",")}.`,
15 scope: "session",
16 },
17 ],
18 };
19 });
20 on("prompt.section", async ($, e, next) => {
21 const result = await next(e);
22 if (result.text === null) return result;
23 return { text: `${result.text}\n[SECTION-4343 ${e.name}]` };
24 });
25};
26