Documentation
¶
Overview ¶
Package skillinstall owns two questions about gadak's agent skill: *where does it go for this host*, and *is the copy already there gadak's own*.
It sits outside package main because more than one caller needs the answers. `gadak skill install` and `gadak doctor` live in cmd/gadak; the desktop app's integrations list (internal/integrations) cannot import package main at all, and today it re-derives the Claude path by hand with a comment apologising for the duplication. One table, one classifier, one owner.
The bytes never change per host. Codex loads gadak's SKILL.md verbatim — the investigation for GDK-1508 copied it in byte-for-byte and read it back out of `codex debug prompt-input` — so the "renderer" for every skill-capable host is the identity function and a destination path. Hosts that need a different *format* (Copilot, Windsurf, Cline, Kiro, Amp: an always-loaded rules file, not a skill directory) are deliberately not in this table.
Index ¶
- Constants
- Variables
- func BuildRevision() string
- func DestStatus(dest string, content []byte) (status string, existing []byte, err error)
- func Digest(content []byte) string
- func DirDest(dir string) (string, error)
- func FrontmatterDescription(content []byte) (string, bool)
- func FrontmatterName(content []byte) string
- func IsDevBuild(version string) bool
- func IsOSMetadata(name string) bool
- func IsOurs(dir string, existing []byte) bool
- func Names() []string
- func SourceFor(version string) string
- func StatusWord(installStatus string) string
- func TextDigest(content []byte) (string, bool)
- func UnknownClientError(name string) error
- func WriteReceipt(dir string, content []byte, version string) error
- type Client
- func (c Client) ConfigDir(env Env) (string, error)
- func (c Client) Dest(env Env, project bool) (string, error)
- func (c Client) HasProjectScope() bool
- func (c Client) HomeDest(env Env) (string, error)
- func (c Client) HomeDoc() string
- func (c Client) NoProjectWhy() string
- func (c Client) Present(env Env) bool
- func (c Client) ProjectDest(env Env) (string, error)
- func (c Client) ProjectDoc() string
- func (c Client) ProjectRefusal() error
- func (c Client) ProjectRelDir() string
- type Env
- type Receipt
Constants ¶
const ( // SkillDir is the folder gadak occupies inside a host's skills root, and // SkillFile the file inside it. Every host in the table reads the same two // names, which is why a single `--dir PATH` means PATH/gadak/SKILL.md // regardless of who is going to load it. SkillDir = "gadak" SkillFile = "SKILL.md" )
const ( SourceRelease = "release" // a cut release wrote this copy SourceDevTree = "dev-tree" // a binary built from a checkout wrote it )
The provenance words a receipt records. They are wire format — `gadak doctor --json` prints them — so the strings are contract.
const ( StatusMissing = "missing" // nothing there StatusIdentical = "identical" // byte-equal to the embedded skill StatusStale = "stale" // gadak wrote it, and it has fallen behind StatusConflict = "conflict" // someone else's file, or gadak's after an edit )
The four words every caller uses for "what is at this destination". They are the wire format of `gadak doctor --json`, so the strings are contract.
const DefaultClient = "claude"
DefaultClient is the client `gadak skill install` uses when none is named. It was the only client before GDK-1508 and stays the default.
const DevVersion = "0.0.0-dev"
DevVersion is the version string cmd/gadak carries when the release ldflag (-X main.version=…, see .goreleaser.yaml) was not applied.
const ReceiptName = ".gadak-skill.json"
ReceiptName sits next to SKILL.md. It is a disposable cache, not data: delete it and the worst that happens is the next upgrade asks for --force.
Variables ¶
var LegacyDigests = map[string]string{
"37c489c4475984c1a9c33852828640c4833dda7f20d939063af6f944ccd40565": "79a70f3",
"a00da5247df29926d88d4948f1ba16e36ea1c9cda1eb8728a2a9cc2d2ff1b594": "3d7a65b",
"be5be92dfc76faed5a330dd905895efcbc6a433fc782fa566c6c1653956e9a32": "c7628ef",
"5a6ca6f702ade9f91fa740f80b1600fe781508c82b17dfbee242b5f506d9b3ab": "eed711e",
"5a6d63ae45af97344c0b91052ef59abbc763de815e277aa6a12bd2d8981f06fd": "1096106",
"1f7000999eaebdeade1995b97373a083a3cc9f02673a8798020f463e3b7d27d8": "f2b8d94",
}
LegacyDigests are the SHA-256 digests of every skills/gadak/SKILL.md gadak shipped *before* installs started leaving a receipt.
This set is FROZEN — it is the backfill for pre-receipt installs only, and it must not grow. Every release from this one on writes ReceiptName, so its own body is recognised by the receipt rather than by a new entry here. (An append-only table that a release could forget to update is exactly the bug that round was closing, so there is nothing to append to.)
Derived 2026-08-16 from `git log --follow -- skills/gadak/SKILL.md`: seven revisions, of which the newest is the current embed and is deliberately absent — that one classifies as "identical", not "stale". The two oldest lived at skills/scry/SKILL.md, before the rename to gadak.
Functions ¶
func BuildRevision ¶
func BuildRevision() string
BuildRevision is the short git hash this binary was built from, or "" when the toolchain stamped none (-buildvcs=false, or a build from outside a checkout). A tree with uncommitted changes gets a trailing "+", the same convention as `git describe --dirty` — and the copy that started GDK-1531 came from exactly such a tree, so that marker is the whole point.
func DestStatus ¶
DestStatus classifies dest relative to content.
missing nothing at dest
identical byte-equal to content — nothing to do
stale gadak wrote it and it has since fallen behind: either it still
matches the receipt gadak left beside it, or its digest is one of
the copies gadak shipped before receipts existed
conflict anything else — someone else's file, or gadak's file after a hand
edit. Only --force replaces it.
Identity is the content hash, never mtime: `brew upgrade` rewrites timestamps and `git checkout` restores them, so neither says who wrote the bytes.
func DirDest ¶
DirDest is the `--dir PATH` destination: PATH/gadak/SKILL.md. It overrides every host in the table, which is the honest seam for a test or for a host gadak has not measured.
func FrontmatterDescription ¶
FrontmatterDescription returns the `description:` value of the leading YAML frontmatter, folded to the single line a host actually loads.
This is the field with a hard budget. Every host keeps `name` + `description` resident in the system prompt and reads the body only when the skill fires, and Codex truncates the description at 1024 characters — measured, not documented — so a description that runs long is silently cut mid-sentence. The parse is deliberately small: gadak's own frontmatter is the only input, and pulling in a YAML dependency for two fields would make desktop/'s separate module a gate on this file.
func FrontmatterName ¶
FrontmatterName returns the `name:` value of a leading YAML frontmatter block, or "". It is used only to sharpen a refusal message, never as a licence to overwrite: a user who edits gadak's own skill keeps `name: gadak` in it, so trusting that line would delete exactly the edits the refusal exists to protect.
func IsDevBuild ¶
IsDevBuild reports whether version identifies a binary built from a checkout rather than cut as a release.
An empty version — a binary carrying no build information at all — counts as a dev build. "Unknown provenance" must not be trusted with the developer's agent configuration; the failure mode of guessing wrong the other way is exactly the incident this closes.
func IsOSMetadata ¶
IsOSMetadata reports whether name is a file the operating system authored rather than the user or gadak: Finder's .DS_Store, Windows Explorer's thumbs.db and desktop.ini, and the ._ AppleDouble sidecars a copy onto a non-HFS volume leaves behind.
Why this exists: orca compares a whole skill directory against a manifest, and one Finder visit was enough to make an untouched copy read as "unrecognized" — the user was asked to --force over their own unmodified files. gadak's identity is a single file's hash, so no current judgment can be broken this way; this is the guard that keeps it true when a caller does start reading directory contents. Present() is the first such caller.
func StatusWord ¶ added in v0.22.0
StatusWord renames the classifier's "identical" to the word a person reads better — "current" — and passes the other three through unchanged (GDK-1534). doctor and the desktop integrations list both print these words; this is the one owner of the renaming, so a third caller cannot diverge from the two that exist.
func TextDigest ¶ added in v0.22.0
TextDigest is Digest over the line-ending-folded form. The bool is false exactly when the bytes are not valid UTF-8.
func UnknownClientError ¶
UnknownClientError is the refusal for a name outside the table. It lists the tokens that do work, because "unknown client" without the list is one more round trip for a user who is already guessing.
func WriteReceipt ¶
WriteReceipt records what gadak just wrote into dir: the digest of the exact bytes, and — because a checkout with autocrlf=true rewrites the file the moment the repo touches it — the digest of the same text with line endings folded (GDK-1520). It takes the bytes, not a digest, because the text digest can only be computed from them. version is the binary's version string, kept for the human who opens the file — and, through SourceFor, the single input to the provenance word. The caller never decides "is this a dev build": it passes its version and this file answers, so the receipt and the auto-sync gate can never disagree (GDK-1531).
Types ¶
type Client ¶
type Client struct {
// Name is the CLI token (`gadak skill install <name>`).
Name string
// Label is the host's own name, for messages and docs.
Label string
// CardLabel is the host's name as a card title — Label without the
// parenthetical qualifiers that make a narrow card wrap (GDK-1534). Same
// value as Label for every host whose label has no parenthetical; the
// table test keeps the two honest against each other.
CardLabel string
// Universal marks the one row the desktop integrations list always offers,
// whether or not the host looks installed: the cross-host .agents
// convention is a directory any agentskills.io reader loads, so absence
// of the host is not a reason to hide the install (GDK-1534). Exactly one
// client may set this — the table test pins which.
Universal bool
// contains filtered or unexported fields
}
Client is one agent host that loads a skill from a directory of SKILL.md files. Everything host-specific is a path; the content is not.
func (Client) ConfigDir ¶
ConfigDir is the host's configuration root. Its existence, not a binary on PATH, is the signal that the host is on this machine: several of these hosts ship as IDE extensions or apps with no command of their own.
func (Client) HasProjectScope ¶
HasProjectScope reports whether this host reads a skill directory from the working directory.
func (Client) NoProjectWhy ¶
NoProjectWhy is the one-line reason this host has no project skill directory, or "" when it has one.
func (Client) Present ¶
Present reports whether this host looks installed: its configuration root exists and holds something the operating system did not write.
The second half is orca's lesson, ported. A single Finder visit leaves a .DS_Store behind, and a directory that contains nothing else is not evidence of anything — reporting a host on that basis puts a row in `gadak doctor` that the user cannot act on. Note this is deliberately stricter than the plain existence check that gates *auto-install*: writing into a directory the user made is fine, listing a host we cannot justify is not.
func (Client) ProjectDest ¶
ProjectDest is the working-directory SKILL.md path, or the refusal for a host whose project scope is a different file format.
func (Client) ProjectDoc ¶
ProjectDoc is the project-scope directory as documentation writes it, or "" for a host that has none.
func (Client) ProjectRefusal ¶
ProjectRefusal explains, in one line, why --project does nothing for this host. Naming the format is the point: the user is not being told "no", they are being told the file they want is a different kind of file.
func (Client) ProjectRelDir ¶
ProjectRelDir is the project skills root as the user typed it — the literal relative path, with no working directory in it. `gadak doctor` prints this rather than an absolute path so its report stays safe to paste in public.
type Env ¶
type Env struct {
// Home is the user's home directory (os.UserHomeDir).
Home string
// Cwd is the working directory; only project scope reads it.
Cwd string
// Getenv looks up an environment variable. A nil Getenv reads nothing,
// which is the right default for a hermetic test.
Getenv func(key string) string
// contains filtered or unexported fields
}
Env is the process environment the destination table reads: the home directory, the working directory, and the environment lookup.
It is a parameter and not package state on purpose. Tests build an Env literal, so nothing in this package can ever resolve to the developer's real home — and in particular no test has to set CODEX_HOME to stay contained. That matters: orca's tripwire records that the Codex binary ignores the USERPROFILE sandbox on Windows, so CODEX_HOME is not a containment mechanism. Here it is read as a *user preference*, never trusted as a fence.
type Receipt ¶
type Receipt struct {
SHA256 string `json:"sha256"`
GadakVersion string `json:"gadak_version"`
InstalledAt string `json:"installed_at"`
// TextSHA256 is the same bytes hashed after folding line endings (GDK-1520):
// a checkout with autocrlf=true rewrites the file the moment the repo
// touches it, and a copy that differs from the receipt only in \r\n is
// still the copy gadak wrote. omitempty because the digest only exists for
// valid UTF-8 — a receipt for anything else has no text form to record.
TextSHA256 string `json:"text_sha256,omitempty"`
// Source and Revision say which *kind* of binary wrote the copy
// (GDK-1531): a cut release, or one built from a checkout, and in the
// second case the short git hash it came from. Before these fields a
// working-tree SKILL.md installed by a `go run` build was indistinguishable
// from a shipped one — `gadak doctor` called it "current" and nobody could
// see that what the agent loaded had never been reviewed.
//
// Both are omitempty: a receipt written before this release has neither,
// and SourceWord backfills the first from GadakVersion.
Source string `json:"source,omitempty"`
Revision string `json:"revision,omitempty"`
}
Receipt records what gadak last wrote at a destination. Only SHA256 is compared; the other fields are there so a human who opens the file can tell what put it there and when.
func ReadReceipt ¶
ReadReceipt returns the receipt in dir. A missing, unreadable, corrupt or digest-less receipt is simply "no receipt" — it degrades to the legacy digest table and, failing that, to the refusal.
func (Receipt) SourceWord ¶
SourceWord is the receipt's provenance, backfilled for the receipts that predate the field: those recorded a version, and the version already says whether the binary was a release. "" only for a receipt with neither.