Expose gitea-axi skill for project delivery #57
17
INSTALL.md
17
INSTALL.md
@@ -69,31 +69,32 @@ Use the module instead ([ADR 0020](.claude/adr/0020-home-manager-module-for-decl
|
|||||||
```
|
```
|
||||||
|
|
||||||
`programs.gitea-axi.enable` installs the CLI, always.
|
`programs.gitea-axi.enable` installs the CLI, always.
|
||||||
The Claude Code context — the Agent Skill and the SessionStart hook — follows the harness: it is declared under a single toggle, `programs.gitea-axi.enableClaudeCodeIntegration`, on by default, and lands only when `programs.claude-code.enable` is also on.
|
The Claude Code SessionStart hook follows the harness: it is declared under `programs.gitea-axi.enableClaudeCodeIntegration`, on by default, and lands only when `programs.claude-code.enable` is also on.
|
||||||
Enable gitea-axi on a host without Claude Code and you get the CLI and nothing else — no assertion, no toggles to turn off.
|
Enable gitea-axi on a host without Claude Code and you get the CLI and nothing else — no assertion, no toggles to turn off.
|
||||||
Importing the module without setting `enable` changes nothing at all.
|
Importing the module without setting `enable` changes nothing at all.
|
||||||
|
|
||||||
The hook is declared through `programs.claude-code`'s own settings option, so a configuration that already sets its own `settings.hooks.SessionStart` gets gitea-axi's merged in alongside rather than colliding.
|
The hook is declared through `programs.claude-code`'s own settings option, so a configuration that already sets its own `settings.hooks.SessionStart` gets gitea-axi's merged in alongside rather than colliding.
|
||||||
The Skill is written straight into Claude Code's skills directory, so it composes with your own skills whether you declare them as `programs.claude-code.skills.<name> = ...` or as a single path standing for a whole directory — there is no form you have to switch to.
|
The Agent Skill is exposed as `packages.<system>.gitea-axi-skill` for project-local delivery through an agent-agnostic dev-shell helper.
|
||||||
|
|
||||||
#### Options
|
#### Options
|
||||||
|
|
||||||
| Option | Default | Meaning |
|
| Option | Default | Meaning |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `programs.gitea-axi.enable` | `false` | Install the CLI. |
|
| `programs.gitea-axi.enable` | `false` | Install the CLI. |
|
||||||
| `programs.gitea-axi.package` | your `pkgs`' build of the package | The package to install, or `null` to declare the context without the binary. |
|
| `programs.gitea-axi.package` | your `pkgs`' build of the package | The package to install, or `null` to declare the hook without the binary. |
|
||||||
| `programs.gitea-axi.enableClaudeCodeIntegration` | `true` | Declare the Agent Skill and SessionStart hook when Claude Code is enabled. |
|
| `programs.gitea-axi.enableClaudeCodeIntegration` | `true` | Declare the SessionStart hook when Claude Code is enabled. |
|
||||||
|
|
||||||
`package = null` declares the context without installing the binary — for instance when you install gitea-axi system-wide through `environment.systemPackages`.
|
`package = null` declares the hook without installing the binary — for instance when you install gitea-axi system-wide through `environment.systemPackages`.
|
||||||
The hook records the bare name `gitea-axi` and lets `PATH` resolve it ([ADR 0019](.claude/adr/0019-hook-records-search-path-name.md)), so a binary installed anywhere on `PATH` satisfies it, and the Skill is still taken from the default build.
|
The hook records the bare name `gitea-axi` and lets `PATH` resolve it ([ADR 0019](.claude/adr/0019-hook-records-search-path-name.md)), so a binary installed anywhere on `PATH` satisfies it.
|
||||||
|
|
||||||
Turn `enableClaudeCodeIntegration` off to keep the CLI declarative while writing the Skill and hook yourself with `setup`.
|
Turn `enableClaudeCodeIntegration` off to keep the CLI declarative while writing the hook yourself with `setup`.
|
||||||
|
|
||||||
#### What the module does not cover
|
#### What the module does not cover
|
||||||
|
|
||||||
Only the Claude Code integration is declarative.
|
Only the Claude Code SessionStart hook is declarative.
|
||||||
The Codex and OpenCode files that `setup hooks` also writes have no home-manager module owning them, so gitea-axi does not write them declaratively either.
|
The Codex and OpenCode files that `setup hooks` also writes have no home-manager module owning them, so gitea-axi does not write them declaratively either.
|
||||||
On a declarative system those targets are unmanaged and therefore writable, so `gitea-axi setup hooks` still installs them.
|
On a declarative system those targets are unmanaged and therefore writable, so `gitea-axi setup hooks` still installs them.
|
||||||
|
The Agent Skill is packaged for project-local delivery, not installed globally by this module.
|
||||||
|
|
||||||
## Verifying an install
|
## Verifying an install
|
||||||
|
|
||||||
|
|||||||
@@ -8,11 +8,11 @@
|
|||||||
# the file mechanism, only which files a generation contains. Building the home
|
# the file mechanism, only which files a generation contains. Building the home
|
||||||
# files derivation needs neither the Claude Code binary nor a running agent.
|
# files derivation needs neither the Claude Code binary nor a running agent.
|
||||||
#
|
#
|
||||||
# The configurations cover the composition risks the reshape introduced: the
|
# The configurations cover the remaining composition risks: the explicit
|
||||||
# Skill coexisting with an operator's own skills in both the attribute-set and
|
# sibling-enable gate that keeps the hook off a host without Claude Code, the
|
||||||
# whole-directory forms, the explicit sibling-enable gate that keeps the Skill
|
# hook merging into an operator's own SessionStart list rather than replacing it,
|
||||||
# off a host without Claude Code, and the hook merging into an operator's own
|
# and the Agent Skill staying out of global harness directories because project
|
||||||
# SessionStart list rather than replacing it.
|
# dev shells deliver it through the agent-agnostic skills helper.
|
||||||
{
|
{
|
||||||
pkgs,
|
pkgs,
|
||||||
home-manager,
|
home-manager,
|
||||||
@@ -20,25 +20,6 @@
|
|||||||
package,
|
package,
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
inherit (pkgs) lib;
|
|
||||||
|
|
||||||
# A skill the operator declares as their own, as one attribute-set entry. Its
|
|
||||||
# SKILL.md is what the attribute-set assertion looks for beside the module's.
|
|
||||||
operatorAttrSkill = pkgs.runCommandLocal "operator-attr-skill" { } ''
|
|
||||||
mkdir -p "$out"
|
|
||||||
printf '%s\n' "the operator's own attribute-set skill" > "$out/SKILL.md"
|
|
||||||
'';
|
|
||||||
|
|
||||||
# A whole directory of skills, one folder per skill, for the path form of the
|
|
||||||
# operator's own `programs.claude-code.skills`. The Claude Code module installs
|
|
||||||
# this recursively; the module's own nested Skill entry has to drop in beside
|
|
||||||
# its contents rather than collide with a single link over the directory.
|
|
||||||
operatorSkillsDir = pkgs.runCommandLocal "operator-skills-dir" { } ''
|
|
||||||
mkdir -p "$out/operator-dir-skill"
|
|
||||||
printf '%s\n' "the operator's own whole-directory skill" \
|
|
||||||
> "$out/operator-dir-skill/SKILL.md"
|
|
||||||
'';
|
|
||||||
|
|
||||||
# A distinctive command so the merged-hook assertion can tell the operator's
|
# A distinctive command so the merged-hook assertion can tell the operator's
|
||||||
# own SessionStart hook apart from gitea-axi's in the generated settings.json.
|
# own SessionStart hook apart from gitea-axi's in the generated settings.json.
|
||||||
operatorHook = {
|
operatorHook = {
|
||||||
@@ -74,29 +55,18 @@ let
|
|||||||
];
|
];
|
||||||
}).config.home-files;
|
}).config.home-files;
|
||||||
|
|
||||||
# An operator on Claude Code who declares their own skill as an attribute-set
|
# Claude Code disabled: the sibling-enable gate in the Claude Code module must
|
||||||
# entry: the module's Skill and theirs must both land, each at its own name.
|
# leave no settings file or Skill entry in the generation.
|
||||||
attrSetSkills = homeFiles {
|
|
||||||
programs.claude-code.enable = true;
|
|
||||||
# A store path (not a bare derivation): the skills value type takes a path,
|
|
||||||
# and a derivation would be read as the attribute-set branch of the option.
|
|
||||||
programs.claude-code.skills.operator-attr-skill = "${operatorAttrSkill}";
|
|
||||||
};
|
|
||||||
|
|
||||||
# An operator who declares their own skills as a whole directory (path form):
|
|
||||||
# the module's Skill lands beside the directory's contents. A regression to a
|
|
||||||
# non-recursive path-form install would fail this build as a file collision.
|
|
||||||
wholeDirSkills = homeFiles {
|
|
||||||
programs.claude-code.enable = true;
|
|
||||||
programs.claude-code.skills = "${operatorSkillsDir}";
|
|
||||||
};
|
|
||||||
|
|
||||||
# Claude Code disabled: the explicit sibling-enable gate must leave no
|
|
||||||
# gitea-axi Skill entry in the generation.
|
|
||||||
claudeCodeOff = homeFiles {
|
claudeCodeOff = homeFiles {
|
||||||
programs.claude-code.enable = false;
|
programs.claude-code.enable = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# Claude Code enabled: the module contributes only the SessionStart hook, not
|
||||||
|
# a global Skill entry.
|
||||||
|
claudeCodeOn = homeFiles {
|
||||||
|
programs.claude-code.enable = true;
|
||||||
|
};
|
||||||
|
|
||||||
# An operator with their own SessionStart hook: the module's hook must merge
|
# An operator with their own SessionStart hook: the module's hook must merge
|
||||||
# into that list rather than replace it.
|
# into that list rather than replace it.
|
||||||
mergedHook = homeFiles {
|
mergedHook = homeFiles {
|
||||||
@@ -107,27 +77,22 @@ in
|
|||||||
pkgs.runCommandLocal "gitea-axi-home-manager-module-check"
|
pkgs.runCommandLocal "gitea-axi-home-manager-module-check"
|
||||||
{
|
{
|
||||||
# Forcing each derivation as a build input is what actually builds the home
|
# Forcing each derivation as a build input is what actually builds the home
|
||||||
# files tree under every configuration; the whole-directory build would fail
|
# files tree under every configuration.
|
||||||
# here, before any assertion runs, on a non-recursive-install regression.
|
|
||||||
inherit
|
inherit
|
||||||
attrSetSkills
|
|
||||||
wholeDirSkills
|
|
||||||
claudeCodeOff
|
claudeCodeOff
|
||||||
|
claudeCodeOn
|
||||||
mergedHook
|
mergedHook
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
''
|
''
|
||||||
echo "attribute-set skills: module's Skill and operator's both land"
|
echo "Claude Code disabled: no hook settings or global Skill entry is written"
|
||||||
test -f "$attrSetSkills/.claude/skills/gitea-axi/SKILL.md"
|
test ! -e "$claudeCodeOff/.claude/settings.json"
|
||||||
test -f "$attrSetSkills/.claude/skills/operator-attr-skill/SKILL.md"
|
|
||||||
|
|
||||||
echo "whole-directory skills: module's Skill lands beside the directory"
|
|
||||||
test -f "$wholeDirSkills/.claude/skills/gitea-axi/SKILL.md"
|
|
||||||
test -f "$wholeDirSkills/.claude/skills/operator-dir-skill/SKILL.md"
|
|
||||||
|
|
||||||
echo "Claude Code disabled: no gitea-axi Skill entry is written"
|
|
||||||
test ! -e "$claudeCodeOff/.claude/skills/gitea-axi"
|
test ! -e "$claudeCodeOff/.claude/skills/gitea-axi"
|
||||||
|
|
||||||
|
echo "Claude Code enabled: hook lands, but the Skill is not globally installed"
|
||||||
|
grep -q '"gitea-axi"' "$claudeCodeOn/.claude/settings.json"
|
||||||
|
test ! -e "$claudeCodeOn/.claude/skills/gitea-axi"
|
||||||
|
|
||||||
echo "operator's own SessionStart hook: the module's hook merges in"
|
echo "operator's own SessionStart hook: the module's hook merges in"
|
||||||
# Match the commands as quoted JSON string values, not by their position or
|
# Match the commands as quoted JSON string values, not by their position or
|
||||||
# the emitter's colon spacing: both must be present for a merge (rather than
|
# the emitter's colon spacing: both must be present for a merge (rather than
|
||||||
|
|||||||
@@ -47,6 +47,15 @@
|
|||||||
packages = forAllSystems (
|
packages = forAllSystems (
|
||||||
{ pkgs, ... }: rec {
|
{ pkgs, ... }: rec {
|
||||||
gitea-axi = pkgs.callPackage ./package.nix { };
|
gitea-axi = pkgs.callPackage ./package.nix { };
|
||||||
|
gitea-axi-skill = pkgs.runCommandLocal "gitea-axi-skill"
|
||||||
|
{
|
||||||
|
passthru.skillName = "gitea-axi";
|
||||||
|
}
|
||||||
|
''
|
||||||
|
mkdir -p "$out"
|
||||||
|
cp -R ${gitea-axi.skill}/. "$out/"
|
||||||
|
test -f "$out/SKILL.md"
|
||||||
|
'';
|
||||||
default = gitea-axi;
|
default = gitea-axi;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,17 +1,9 @@
|
|||||||
# A home-manager module installing gitea-axi and, when a harness is present,
|
# A home-manager module installing gitea-axi and, when Claude Code is present,
|
||||||
# its ambient context — the bundled Agent Skill and the SessionStart hook
|
# its SessionStart hook.
|
||||||
# (ADR 0020, reshaped by ADR 0021).
|
|
||||||
#
|
#
|
||||||
# `programs.gitea-axi.enable` installs the CLI, always. The Claude Code context
|
# `programs.gitea-axi.enable` installs the CLI, always.
|
||||||
# follows the harness: it is declared under one per-harness toggle and lands
|
# The Agent Skill is exposed as a package output for agent-agnostic delivery by
|
||||||
# only when `programs.claude-code.enable` is also on. gitea-axi is a working CLI
|
# a project's dev shell or an operator's shared skill delivery module.
|
||||||
# without a harness, so enabling it on a host with no Claude Code installs the
|
|
||||||
# binary and nothing else, with no assertion.
|
|
||||||
#
|
|
||||||
# The module is a wiring layer and nothing more. Both artefacts come from the
|
|
||||||
# package's published attributes, so what a declarative configuration installs
|
|
||||||
# and what `gitea-axi setup` writes imperatively are the same two artefacts, and
|
|
||||||
# neither is restated here.
|
|
||||||
#
|
#
|
||||||
# Importing this module changes nothing until `programs.gitea-axi.enable` is set.
|
# Importing this module changes nothing until `programs.gitea-axi.enable` is set.
|
||||||
{
|
{
|
||||||
@@ -24,15 +16,13 @@ let
|
|||||||
cfg = config.programs.gitea-axi;
|
cfg = config.programs.gitea-axi;
|
||||||
|
|
||||||
# The package the declarations are read out of. `package = null` opts out of
|
# The package the declarations are read out of. `package = null` opts out of
|
||||||
# putting the binary on PATH, not out of the configuration — an operator who
|
# putting the binary on PATH, not out of the hook declaration — an operator who
|
||||||
# installs gitea-axi system-wide still wants the Skill — so the declarations
|
# installs gitea-axi system-wide still wants the hook — so the declarations
|
||||||
# fall back to the default build, which in that arrangement is already in the
|
# fall back to the default build, which in that arrangement is already in the
|
||||||
# closure anyway.
|
# closure anyway.
|
||||||
sourcePackage = if cfg.package != null then cfg.package else defaultPackage;
|
sourcePackage = if cfg.package != null then cfg.package else defaultPackage;
|
||||||
|
|
||||||
defaultPackage = pkgs.callPackage ./package.nix { };
|
defaultPackage = pkgs.callPackage ./package.nix { };
|
||||||
|
|
||||||
claudeCode = config.programs.claude-code;
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.programs.gitea-axi = {
|
options.programs.gitea-axi = {
|
||||||
@@ -44,12 +34,11 @@ in
|
|||||||
defaultText = lib.literalExpression "pkgs.callPackage ./package.nix { }";
|
defaultText = lib.literalExpression "pkgs.callPackage ./package.nix { }";
|
||||||
description = ''
|
description = ''
|
||||||
The gitea-axi package to install, or `null` to declare the ambient
|
The gitea-axi package to install, or `null` to declare the ambient
|
||||||
context without installing the binary — for an operator who supplies it
|
Claude Code hook without installing the binary — for an operator who
|
||||||
another way, such as `environment.systemPackages`.
|
supplies it another way, such as `environment.systemPackages`.
|
||||||
|
|
||||||
The SessionStart hook records a name resolved on `PATH`, so a binary
|
The SessionStart hook records a name resolved on `PATH`, so a binary
|
||||||
installed elsewhere satisfies it. With `null` the Agent Skill is still
|
installed elsewhere satisfies it.
|
||||||
taken from the default build.
|
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -57,16 +46,15 @@ in
|
|||||||
type = lib.types.bool;
|
type = lib.types.bool;
|
||||||
default = true;
|
default = true;
|
||||||
description = ''
|
description = ''
|
||||||
Whether to declare gitea-axi's Claude Code context — the bundled Agent
|
Whether to declare gitea-axi's Claude Code SessionStart hook alongside
|
||||||
Skill and the SessionStart hook — alongside the CLI.
|
the CLI.
|
||||||
|
|
||||||
Both artefacts land only when `programs.claude-code.enable` is also on;
|
The hook lands only when `programs.claude-code.enable` is also on;
|
||||||
with it off they are silently absent, matching how home-manager's own
|
with it off it is silently absent, matching how home-manager's own
|
||||||
`enableBashIntegration`-style toggles behave against a disabled sibling.
|
`enableBashIntegration`-style toggles behave against a disabled sibling.
|
||||||
|
|
||||||
Turn this off to install gitea-axi declaratively while writing the
|
Turn this off to install gitea-axi declaratively while writing the
|
||||||
Claude Code context by hand or with `gitea-axi setup`. The toggle
|
Claude Code hook by hand or with `gitea-axi setup`.
|
||||||
generalises: a future harness reads as `enableCodexIntegration`.
|
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -75,44 +63,13 @@ in
|
|||||||
lib.mkMerge [
|
lib.mkMerge [
|
||||||
(lib.mkIf (cfg.package != null) { home.packages = [ cfg.package ]; })
|
(lib.mkIf (cfg.package != null) { home.packages = [ cfg.package ]; })
|
||||||
|
|
||||||
# The two Claude Code artefacts move together under one per-harness
|
(lib.mkIf cfg.enableClaudeCodeIntegration {
|
||||||
# toggle. They are gated by different mechanisms internally — the Skill by
|
|
||||||
# an explicit `claude-code.enable` condition, the hook by the Claude Code
|
|
||||||
# module's own gate — because of how each is declared, below.
|
|
||||||
(lib.mkIf cfg.enableClaudeCodeIntegration (
|
|
||||||
lib.mkMerge [
|
|
||||||
# The hook is declared through the Claude Code module's own settings
|
# The hook is declared through the Claude Code module's own settings
|
||||||
# option. That composes it with an operator's own SessionStart hooks
|
# option. That composes it with an operator's own SessionStart hooks
|
||||||
# instead of colliding, and the module drops the declaration for free
|
# instead of colliding, and the module drops the declaration for free
|
||||||
# when it is disabled — so no explicit `claude-code.enable` gate here.
|
# when it is disabled.
|
||||||
{ programs.claude-code.settings.hooks.SessionStart = [ sourcePackage.sessionStartHook ]; }
|
programs.claude-code.settings.hooks.SessionStart = [ sourcePackage.sessionStartHook ];
|
||||||
|
|
||||||
# The Skill is written as an ordinary file into Claude Code's skills
|
|
||||||
# directory, rather than contributed to `programs.claude-code.skills`.
|
|
||||||
# That composes with both forms of the operator's own skills option —
|
|
||||||
# an attribute set and a single path for a whole directory — because
|
|
||||||
# it never touches that option's type, so the path-form collision
|
|
||||||
# disappears at its root instead of being escaped by a toggle.
|
|
||||||
#
|
|
||||||
# Writing through home.file does not inherit the Claude Code module's
|
|
||||||
# `enable`-gate the way declaring through its options does, so the
|
|
||||||
# Skill is gated on `claude-code.enable` explicitly. The gate is also
|
|
||||||
# what keeps package realisation lazy: home.file reads the Skill's
|
|
||||||
# source directory while evaluating, so an ungated write would realise
|
|
||||||
# the package on every host — even one with no Claude Code that
|
|
||||||
# installs nothing from it.
|
|
||||||
#
|
|
||||||
# `configDir` is read from the Claude Code module rather than
|
|
||||||
# hardcoded, so the Skill lands beside that module's own skills
|
|
||||||
# wherever the operator points it.
|
|
||||||
(lib.mkIf claudeCode.enable {
|
|
||||||
home.file."${claudeCode.configDir}/skills/gitea-axi" = {
|
|
||||||
source = sourcePackage.skill;
|
|
||||||
recursive = true;
|
|
||||||
};
|
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
))
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -179,9 +179,10 @@ buildNpmPackage (finalAttrs: {
|
|||||||
# different majors.
|
# different majors.
|
||||||
inherit nodejs;
|
inherit nodejs;
|
||||||
|
|
||||||
# The bundled Agent Skill's directory, for a configuration that installs it
|
# The bundled Agent Skill's directory, for a configuration that packages it
|
||||||
# declaratively. A directory rather than the SKILL.md inside it, so a Skill
|
# into an agent-agnostic project-local delivery mechanism. A directory rather
|
||||||
# that grows helper files stays one reference.
|
# than the SKILL.md inside it, so a Skill that grows helper files stays one
|
||||||
|
# reference.
|
||||||
skill = "${finalAttrs.finalPackage}/${skillSubdir}";
|
skill = "${finalAttrs.finalPackage}/${skillSubdir}";
|
||||||
|
|
||||||
# The SessionStart hook entry, verbatim as it belongs in a Claude Code
|
# The SessionStart hook entry, verbatim as it belongs in a Claude Code
|
||||||
|
|||||||
Reference in New Issue
Block a user