skillinstall

package
v0.22.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 12, 2026 License: Apache-2.0 Imports: 15 Imported by: 0

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

View Source
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"
)
View Source
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.

View Source
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.

View Source
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.

View Source
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.

View Source
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

View Source
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

func DestStatus(dest string, content []byte) (status string, existing []byte, err error)

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 Digest

func Digest(content []byte) string

Digest is the content hash the whole provenance story is keyed on.

func DirDest

func DirDest(dir string) (string, error)

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

func FrontmatterDescription(content []byte) (string, bool)

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

func FrontmatterName(content []byte) string

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

func IsDevBuild(version string) bool

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

func IsOSMetadata(name string) bool

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 IsOurs

func IsOurs(dir string, existing []byte) bool

IsOurs reports whether gadak wrote these exact bytes.

func Names

func Names() []string

Names returns the client tokens in listing order.

func SourceFor

func SourceFor(version string) string

SourceFor is the receipt word for a version string.

func StatusWord added in v0.22.0

func StatusWord(installStatus string) string

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

func TextDigest(content []byte) (string, bool)

TextDigest is Digest over the line-ending-folded form. The bool is false exactly when the bytes are not valid UTF-8.

func UnknownClientError

func UnknownClientError(name string) error

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

func WriteReceipt(dir string, content []byte, version string) error

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 Clients

func Clients() []Client

Clients returns the table in listing order.

func Lookup

func Lookup(name string) (Client, bool)

Lookup finds a client by token, case-insensitively.

func (Client) ConfigDir

func (c Client) ConfigDir(env Env) (string, error)

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) Dest

func (c Client) Dest(env Env, project bool) (string, error)

Dest is HomeDest or ProjectDest.

func (Client) HasProjectScope

func (c Client) HasProjectScope() bool

HasProjectScope reports whether this host reads a skill directory from the working directory.

func (Client) HomeDest

func (c Client) HomeDest(env Env) (string, error)

HomeDest is the user-scope SKILL.md path for this host.

func (Client) HomeDoc

func (c Client) HomeDoc() string

HomeDoc is the user-scope directory as help and documentation write it.

func (Client) NoProjectWhy

func (c Client) NoProjectWhy() string

NoProjectWhy is the one-line reason this host has no project skill directory, or "" when it has one.

func (Client) Present

func (c Client) Present(env Env) bool

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

func (c Client) ProjectDest(env Env) (string, error)

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

func (c Client) ProjectDoc() string

ProjectDoc is the project-scope directory as documentation writes it, or "" for a host that has none.

func (Client) ProjectRefusal

func (c Client) ProjectRefusal() error

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

func (c Client) ProjectRelDir() string

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.

func OSEnv

func OSEnv() Env

OSEnv reads the real process environment. It never fails here: a missing home or working directory surfaces at the destination that needed it.

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

func ReadReceipt(dir string) (Receipt, bool)

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

func (r Receipt) SourceWord() string

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL