Stand it up on your project
One install, one bootstrap, then a small loop you run after each feature. This page is the whole operational surface: what to type, what lands on disk, what each of the ten skills is actually for, what a security run costs, and the six failures you will hit that are worth recognising on sight.
Install
There are two paths. The first works on any agent that loads the Agent Skills standard; the
second is a Claude Code convenience. Pick exactly one. Run both and Claude
registers every skill twice — once namespaced by the plugin, once from your personal skills
directory — and you get two subtly different copies competing for the same request.
freya doctor warns when it sees this, but it is far easier not to cause it.
git clone https://github.com/AlexSendula/freya-devkit.git
cd freya-devkit
./install.sh # macOS / Linux
.\install.ps1 # Windows (PowerShell)
freya doctor # verify what landed where
There is no copy step, because the checkout is the store. The
installer symlinks each skill directory into your agent's skills directory and writes the
freya launcher into ~/.local/bin. No file is ever rewritten at install
time — which is only possible because the repo's directory names are already the installed
names: the directory is skills/freya-code-graph/, not skills/code-graph/,
and its name: field matches. The Agent Skills specification requires that match, and a
symlinked file cannot be rewritten per-agent, so the prefix had to move into the repo itself.
Four flags matter. --agent claude --agent copilot chooses which agents to install
for (repeatable). --copy copies instead of linking. --dry-run prints the
plan and changes nothing. --uninstall removes what this store put there.
freya doctor says command not found
~/.local/bin is not on PATH in a stock macOS shell, and the
installer deliberately does not edit your shell profile. Editing a login file
on a user's behalf is not a thing an install script gets to do quietly. Instead it prints the
exact export line for the shell you are actually in —
export PATH="$HOME/.local/bin:$PATH" on a POSIX shell, the PowerShell equivalent on
Windows. Run it, then put it in your profile so it survives the session.
On Windows the installer does not wait to be told: it probes whether it can
actually create a symlink before it changes anything, and switches itself to --copy
when creation is refused (Developer Mode off, or an unelevated shell). The launcher is always
written rather than linked, with a freya.cmd beside it, because an extensionless
freya is not runnable on Windows.
/plugin marketplace add AlexSendula/freya-devkit
/plugin install freya-devkit@freya-devkit
The freya-devkit@freya-devkit string is
<plugin>@<marketplace>: the repo hosts its own single-plugin marketplace,
so both halves happen to be the same word. For local development, point the marketplace at a
checkout path instead.
The launcher is not optional on this path either. Measured across the shipped skill layer,
7 of the 10 SKILL.md files invoke freya <command>,
102 times in total — spec-manager 38, wrap-up 28, the security scan 14. Without
freya on PATH, those steps fail with command not found. Claude
Code supplies it: it adds every installed plugin's bin/ directory to PATH
automatically, so a plugin shipping bin/freya puts freya on
PATH with no extra step. The entry is version-stamped, but Claude Code re-adds it for
whichever version is installed, so /plugin update does not break it.
That host behaviour is undocumented by Claude Code and nothing in this repo tests it. It works, it was checked by hand, and it is a dependency worth knowing you have.
On Windows, a marketplace install has no runnable launcher at all.
bin/freya is an extensionless shebang script, which Windows cannot execute, and the
freya.cmd shim is generated by the installer — which a marketplace install
never runs. On Windows, use the clone path and run .\install.ps1 — it takes
the same flags and delegates to the same bin/installer.py.
What the installer refuses to do
This code deletes things inside a home directory, so every destination is classified before anything is touched, and the plan is applied only if all of it can be.
| Status | What is there | What happens |
|---|---|---|
create | nothing | the link is made |
ok | our link, pointing at this store | nothing to do |
foreign | a link into another checkout, or a copy marked as another store's | replaced only with --force |
occupied | a real file or directory | never touched. Blocks even with --force |
stale-store | our link, pointing at a store that has moved | reported by doctor; repaired by re-installing |
orphan-skill | our link to a skill that no longer exists | reported by doctor; pruned by update |
apply_plan raises before mutating anything if any target blocks, which makes
a half-applied install impossible — you either get all ten links or none. Uninstall removes only
symlinks that resolve back into this store, so another checkout's links and anything you
created survive. A --copy install writes a .freya-install marker naming its
source store, and a recursive delete is reachable only for a directory carrying that marker and
naming this store; a marker-less directory survives both a forced install and an uninstall.
The install contract is written as a decision record, clause by clause — what may be replaced,
what may be deleted, and what the two install modes each promise. Reach for it before changing
anything in bin/installer.py, or if you want the guarantees rather than the summary.
The one entry point: bootstrap
One command, one time. freya-spec-manager bootstrap replaces running
init, code-graph build and scan by hand, and after it
everything is incremental.
- Initialise the structure — the
knowledge-base/layout andprinciples.md. Idempotent; it never clobbers an existing file. - Build the code graph — cheap, useful regardless, and the shape detector cannot run without it.
- Detect the project's shape and recommend — it shows you the recommendation
and its evidence (source-file count, internal-edge count, detected stack) and asks you to
confirm or override. On
unknownit asks outright, with no recommendation. - Branch — greenfield or brownfield, below.
- Summarise — what was created, and for brownfield a count of proposed candidates by category, with the reminder that nothing needs review now.
The shape detector
The greenfield/brownfield call is deterministic and inspectable: it counts internal import edges in the graph — real feature wiring — not raw files. A bare scaffold can have hundreds of boilerplate files and zero wiring, and would be misread by any file-count heuristic. Try it; this reproduces the detector's own logic and its own reason strings.
Zero edges → greenfield, unless the backend
could not read part of the repository — then unknown, because a codebase the tool
cannot see is not an empty one. Any positive edge count → brownfield, and it now
says what it could not read alongside the verdict. No graph at all → unknown,
meaning build the graph first.
The detector never forces a branch: a one-time onboarding decision benefits from a human glance, so
an unusually structured repo can be overridden on sight rather than silently misclassified.
On a polyglot repo — Java, Kotlin, Swift, Rust — the built-in resolver produces zero internal edges, because it scrapes imports for TypeScript, JavaScript, Python and Go and nothing else. The detector then said greenfield on a codebase with a decade of history in it. That wall was hit on the first real attempt to use the toolkit on a work laptop.
It is closed. The graph is produced through a contract, and an opt-in backend reads 40
languages across 93 extensions. More to the point, an answer now says what it could
not read: a repository the backend cannot see reports unknown and names
the extensions, rather than reporting an empty graph as an empty project. The remaining
honest limit is that a curated list decides what counts as source, so a language nobody
listed is still silent.
Your path: greenfield or brownfield
There is no code to infer intent from, so bootstrap skips inference and you author intent forward as you build. This is the easier path.
- Bootstrap —
freya-spec-manager bootstrapdetects greenfield, skipsscan, and builds an empty behavior graph so the machinery is initialised. It prints: "Greenfield project — no inference run. Author behaviors forward as you build withspec-manager create." - Scaffold the docs —
freya-docs-manager init. On a near-empty project most of them will be[TODO:]markers you resolve as it grows; that is the intended state, not a failed run. - Author intent — one spec per feature, as you build it. A spec you wrote yourself starts at certainty 100; add behavior records for anything observable.
- Implement, then link a test — write or link the test, and once it passes,
bump the behavior to
accepted. Acceptance is a byproduct of implementing, not a separate ceremony. - Wrap up —
freya-wrap-up. Two commits: code, then artifacts.
There is real code carrying intent nobody wrote down. Bootstrap reverse-engineers a review queue of proposed candidates — never authoritative, never files in the code tree.
- Bootstrap —
freya-spec-manager bootstrapdetects brownfield and runsscanto infer candidate behaviors, then builds the behavior graph. It warns first:scanspawns a discovery task per area and over a large repo it takes a while. - Docs from the code —
freya-docs-manager init, a separate step because bootstrap does not run it. The reference docs are what give the security scan and spec inference their architectural context. - A security baseline — the first scan has no prior report to diff against, so it becomes the baseline.
- Drain the queue lazily — nothing needs review now. Work the tail through
freya-status'sreview intentworklist (proposed → confirm) andreview tests(confirmed → accept), or let wrap-up's validate-on-hit surface behaviors as you touch their code. - Go incremental —
freya-wrap-upafter each feature,freya-statuswhenever you want to know where you stand.
behavior.json is correct
The single most likely "is this broken?" moment on a brownfield repo. scan only
ever mints proposed records, and the graph projects only accepted and
confirmed ones. So a fresh brownfield behavior graph is near-empty by design
while the proposed corpus sits in specs/. That is the system working.
The obvious objection is that inference floods you. It was measured: a full brownfield scan over ~224 files produced ~383 candidate behaviors at roughly 260k tokens and ~65 seconds. That is a flood if you review it eagerly, and fine because nothing asks you to — each feature area came out individually manageable at 35–63 candidates, and the executable-versus- declarative split landed around 88/12. Lazy review is what makes a corpus that size tractable; it is load-bearing, not a convenience.
Why inference produces only proposals, and why the queue is drained lazily rather than up front, is the decision this whole onboarding arc rests on. Read it if you are tempted to bulk-accept.
The ten skills, and what each is actually for
Before the roster, the distinction that changes what you can expect of each one. Five
ship a real, stdlib-only Python engine that does deterministic work — code-graph,
spec-manager, behavior-graph, behavior-runner, status — and docs-manager has one detection helper.
Three are pure prose the agent follows, with no binary at all:
codebase-security-resolver, dependency-vulnerability-check, wrap-up. One sits
between: codebase-security-scan is prose for the report and a Python driver for the
fan-out. An engine's output is a program's output. A prose skill's output is a model following
instructions, which is a different kind of guarantee. Every engine is reached the same way —
freya <command> through the launcher, never a filesystem path.
Each card below gives the purpose in a line and then spends its space on the limits, because the limits are the part the reference markdown does not carry. For commands, arguments and outputs, follow the link in the card.
code-graph Foundation Python engine The dependency graph and blast radius every other skill queries.
Builds a reverse-dependency graph so consumers can process a change's whole blast
radius rather than only the file that changed. The graph is produced by a
backend behind a fixed contract. homegrown ships with the
toolkit, needs nothing but Python, and scrapes imports for TypeScript/JavaScript, Python
and Go — 4 languages, 6 extensions. graphify is opt-in and needs its binary
on PATH: tree-sitter ASTs across 40 languages and 93
extensions, plus calls/inherits/references
relations the built-in one has no notion of.
# the package is graphifyy, two y's; the command it installs is graphify, one y
uv tool install "graphifyy[sql,terraform]==0.9.47" # or pip, on Python 3.10+
freya code-graph --use graphify # this project
freya code-graph --use graphify --global # and every future one
Pinned deliberately, and it is the same line
freya install prints. graphifyy is pre-1.0, and
0.9.47 is the release this toolkit was measured against. The
[sql,terraform] extras are not optional decoration either: without them
graphify still declares .sql, .tf and
.tfvars and parses none of them, and the coverage census — which filters by
the running backend's declared extensions — would then affirm that nothing went unread
while the graph held no nodes for those files.
freya install asks this once and records the answer as your machine
default; the first build in a project writes it into that project's committed
knowledge-base/settings.json, so a clone and CI resolve the same backend you
do. It is never chosen automatically — installing a binary on PATH must not
silently change every blast radius on the machine.
Limits
- The built-in backend is regex, not an AST. A dynamic
import(variable)is missed; imports inside comments or strings can mismatch.graphifydoes not have this limit. - Every answer says what it could not read. A build, query or impact
answer may carry an
unmapped_sourceblock naming the in-scope source the backend could not parse, and the directories to grep instead. It is absent when there is nothing to say — so its presence means the answer above it is incomplete. - External packages are recorded as
external:leaves and never traversed. - Bare Go imports and absolute-Python module specifiers often fall through to
external:. Internal-edge resolution is strongest for TS/JS relative and aliased imports. - Monorepos want one graph per subproject, and a
tsconfigextendschain is not followed. - Nothing outside the project root is reached unless you declare it.
An import's
.., a tsconfigpathsescape and an absolute import all come backunresolved:, and a committed symlink pointing out of the project is refused rather than followed. A symlink that stays inside the project still works. Naming a sibling directory underoutsideinknowledge-base/settings.json—{"ui": "../packages/ui"}— makes imports that land there resolve tooutside:ui/<path>instead of disappearing into a package tag. That is all it buys: the directory is never scanned, walked or globbed, the declaration causes no file under it to be read (the whole reach is arealpath, oneis_file()and one cachedlistdirof the named file's own directory), no file under it becomes a node, and there is still no reverse edge or blast radius on the far side. Relative paths only, and it is a per-project setting — a machine-wide one would point every repository on the machine at one directory. Only the built-inhomegrownresolver honours declarations: on thegraphifybackend a declared root reportscrossings: 0whether or not anything crossed it, because that backend never consults them. - On
buildit classifies directories in the order rules → AI → user, and non-interactively an uncertain directory defaults to source — so real code is never silently dropped, at the cost of occasionally graphing something you would have excluded.
Commands and output schema: knowledge-base/reference/SKILL_REFERENCE.md#code-graph
docs-manager Knowledge Prose + 1 helper Standardized project docs, impact-aware, in knowledge-base/reference/.
Creates and maintains a standard documentation set: a coordinator detects the stack and
gathers business context, then one worker per doc type runs the write. Undetectable details
become [TODO:] markers resolved later in batched questions.
Limits
- Almost entirely prose-driven. Its only real code is stack and test-runner detection; doc quality is the model following instructions.
- Certainty scoring is not one of its features — that is spec-manager. Its
analog is
[TODO:]markers plus a check / warn / fail review pass. - Some of its templates and evals still name the older
docs/layout;knowledge-base/is authoritative.
Commands and outputs: knowledge-base/reference/SKILL_REFERENCE.md#docs-manager
spec-manager Knowledge Python engine Specs, intentional-design decisions, the behavior lifecycle, and governance.
The intent owner: what a feature does, why it was designed that way, and — the part that pays for itself — which of its oddities are deliberate.
A spec's Intentional Design Decisions section is what a security scan reads to tell a real vulnerability from an on-purpose choice: "No password fallback — a scanner flagging missing password auth should be ignored."
Limits
scanonly ever produces proposed candidates — neveraccepted, never files in the code tree. Acceptance is a human act.- Authority order for contradictions is fixed: principle > ADR > spec. A spec never wins against a principle.
- Certainty (0–100) gates review of inferred specs only. Trust in an executable behavior is its lifecycle state, not that number.
Commands and outputs: knowledge-base/reference/SKILL_REFERENCE.md#spec-manager
behavior-graph Knowledge Python engine Owns behavior.json; answers both blast-radius directions.
Projects your specs' behavior records, merges in coverage from behavior-runner, and answers impact both ways: which behaviors a code change affects, and which code implements a behavior.
Limits
- Only
acceptedandconfirmedbehaviors are projected;proposedlives only in the specs. - Only
acceptedbehaviors can gate a change. - Queries reflect the last
--buildsnapshot. Change specs or code and the answer is stale until you rebuild. - Coverage merges by trust: observed beats static. A red test invalidates prior coverage; any other unknown preserves it, so a runner that could not run does not erase what was already known.
Commands and outputs: knowledge-base/reference/SKILL_REFERENCE.md#behavior-graph
behavior-runner Knowledge Python engine Runs accepted behaviors; emits observed coverage fingerprints.
A producer only. It runs accepted, non-quarantined behaviors through their adapter and prints
TEST → CODE coverage fingerprints as JSON. It never writes
behavior.json.
Limits
- Only the vitest unit path is implemented. Other adapters are allow-listed
but return
unknownwith a reason. - The vitest invocation is hardcoded to
pnpm. confirmedbehaviors are never executed — a test is still owed — so they can never be test-failed and never gate.- Coverage is
observedat 0.8 (real V8 runtime),staticat 0.5 (graph closure of a declared entry point), orunknownwith a reason. It is never faked, and there is no fourth value.
Commands and outputs: knowledge-base/reference/SKILL_REFERENCE.md#behavior-runner
codebase-security-scan Analysis Prose + Python driver Six parallel finders plus adversarial verification; specs cut false positives.
Six specialised finders scan in parallel — auth/authz, injection, secrets, API/network, config/deps, file/resource — and every finding is validated and then adversarially attacked before it is allowed into the report.
Limits
- Only
acceptedbehaviors downgrade a finding; proposed and confirmed add an advisory note at most. The query checks that the behavior's state isacceptedin the specs, that its locator resolves to a file in the project, and that the exercised path wasobservedrather than inferred from the import graph — all read from artifacts the scanned repository commits — and returns a sentence saying so, which the report must carry verbatim. The scan passes--verify, so the linked test is re-run; on the plain query, which runs nothing, that sentence is a label on evidence and not a verification. - On the driver path, exactly three refutation lenses run per finding. (The
"2–3 lenses" wording elsewhere describes the in-loop
updatepass, not the driver.) - A finding is dropped only on a unanimous refutation. A split verdict stays needs review — which is why the lens count is never the thing that gets cut to save money.
- Neither driver mode is wired into
wrap-up. See Security below for what wrap-up runs instead, and why.
Commands, modes and the exit-code table: skills/freya-codebase-security-scan/SKILL.md
dependency-vulnerability-check Analysis Prose-driven Supply-chain CVE audit for Node projects.
The supply-chain half of security — its sibling covers your code. It detects the package manager from the lock file, runs that manager's audit, then enriches and validates each finding with web searches to strip false positives and note applicability.
Limits
- Node ecosystem only — npm, yarn, pnpm. No pip, no cargo, no Go modules. (The quick-reference entry for this skill claims otherwise; the skill file is the accurate one.)
- It reports, it does not fix. It suggests the upgrade; you run it.
- It is not invoked by
wrap-up. Nothing schedules it — you do, with whatever recurring mechanism your agent offers.
Commands and outputs: skills/freya-dependency-vulnerability-check/SKILL.md
codebase-security-resolver Resolution Prose-driven Interactive fixer; routes every finding to a terminal state.
The actor half of the security loop: list → select → validate → confirm → plan → implement →
commit the code, then hand off to wrap-up for the artifacts commit. Its signature move is that
skipping is never a no-op — every skipped finding still gets routed to a
terminal state. Already resolved becomes RESOLVED; intentional gets a spec entry and
becomes INTENTIONAL; code that no longer exists becomes OBSOLETE; not
actually a security issue is downgraded to INFO. Nothing rots in the report as a
permanent open item nobody will ever look at again.
Limits
- When a finding's file has zero dependents, it recommends deleting the dead code rather than fixing it — occasionally the right answer, always worth a second look before you agree.
- Prose-only: its tables and status lines are templates the agent reproduces, not program output. They look like a tool speaking and they are not.
Commands and outputs: knowledge-base/reference/SKILL_REFERENCE.md#codebase-security-resolver
wrap-up Orchestration Prose orchestrator Runs the whole team in order under a two-commit pattern.
The post-implementation orchestrator: commit the code, refresh the graph, then docs, then specs, then the behavior-integrity and governance gates, then an in-loop security pass, then commit the artifacts. Five steps between two commits.
Limits
- It warns and skips any missing skill rather than failing. A partial install produces a partial run that tells you so.
- A behavior scaffold's commit class follows its lifecycle state, not its file
location: a
proposed.featurescaffold rides the artifacts commit even though it sits among tests, and anacceptedbehavior's test rides the code commit. - Its security step is the in-loop
updatemode, never the paid driver — see Security.
Pipeline, flags and staging rules: knowledge-base/reference/SKILL_REFERENCE.md#wrap-up
status Orchestration Python engine Read-only backlog: what intent, tests, and findings are outstanding.
The check-counterpart of wrap-up. Where wrap-up does and syncs, status only reports — and on
request regenerates the git-tracked BACKLOG.md, which diffs in a pull request so a
reviewer sees outstanding work without running anything.
Limits
- Read-only. The one file it writes is the backlog.
- The intent worklist is sorted lowest-certainty first, so the least trustworthy proposals reach you first.
- It drains the cold tail — behaviors no change ever touches, which wrap-up's validate-on-hit will therefore never surface.
- It never auto-authors a test. That is the engineer's work, on purpose.
- Every source degrades to a note independently, so a missing or corrupt input narrows the
census rather than blocking the command — and, new since 0.3.0, those notes are carried into
the generated
BACKLOG.mdas well, under a banner saying a section may be empty because its input was missing rather than because it is clean. - An unreadable finding counts as open, never as absent.
findings.json'sstatusvocabulary isopen/resolved/intentional, and both ends of that file are written by hand. A fourth value — a capitalisation, a synonym, a missing key — used to be dropped silently, so a file holding three high-severity findings could report zero open findings and say nothing about it. It is now counted as open and named in the note. A silently-zero security bucket reads as clean, not as never scanned, and those are the same number and opposite facts.
Commands and outputs: knowledge-base/reference/SKILL_REFERENCE.md#status
What it writes, and where
Everything durable lands under one project-local, version-controlled root:
knowledge-base/. It travels with your branch and diffs in code review. The four
directories you will actually open are specs/ (per-feature intent),
decisions/ (ADRs), security/ (dated reports plus a machine-readable
findings.json) and reference/ (the generated docs) — alongside two files at
the root, principles.md, which is the project's constitution, and
BACKLOG.md, which is generated and says so.
The prose and the machine indexes are committed. Inside
.graph/ it is split: the parse cache is ignored, and
behavior.json is not. The directory auto-writes a .gitignore
naming the regenerable files individually — graph.json,
graph.*.json, classifications.json, docs.json —
rather than a blanket *, because behavior.json sits beside them
and has to survive a clone: its observed coverage comes from running your test suite and
cannot be recovered by re-reading source. Treat the cache as a cache; rebuilding it is
cheap. An adopting project never has to touch its own root .gitignore.
The path-by-path table — every artifact, its owning skill, and whether it is committed — lives in the repo, along with the conventions each skill follows when writing into that tree. Reach for these when you need to know who owns a file rather than what the tree is for.
The daily loop: wrap-up and status
Bootstrap was a one-time act. From here the loop is two commands, and only one of them changes anything.
freya-wrap-up
After each feature. Refreshes the graph, then docs, then specs, then the behavior-integrity and governance gates, then an in-loop security pass — and lands two commits. The mutating, syncing path.
freya-status
Any time, including mid-change. A read-only census of outstanding intent, tests
owed, coverage gaps and open findings, refreshing BACKLOG.md. Because it mutates
nothing, "where do I stand?" is always a safe question.
What you see in your own git log afterwards is two commits, in this order:
Every incremental update assumes a prior sync — it works from the commit it last
processed. On a project with no tracking files there is no such commit, and the naive
reading of "changed since never" is "the entire codebase". So wrap-up must not let
update silently trigger a full-codebase generation: it reports that the project is
unsynced and defers to an explicit first scan and build. Running bootstrap first is exactly what
makes your first wrap-up behave.
Security: what to run, and what it costs
Three modes, and the difference between them is money.
| Mode | Discovery rounds | Verification lenses | Runs in wrap-up? |
|---|---|---|---|
freya security scan --yes | 1 | 3 | no — paid, on request |
freya security audit --yes | up to 5 (stops after 2 dry rounds) | 3 | no — paid, on demand |
no driver — the skill's update mode | — | in-loop | yes — git-diff scoped, spends nothing |
Neither driver mode is wired into wrap-up, and that includes a project's first
wrap-up, where there is no previous scan to diff against — that case is covered by a full in-loop
pass rather than by silently falling into the paid driver. Both driver modes spawn headless agent
workers and cost real money, so both confirm before spending unless --yes is passed; an
agent shell has no tty to answer that prompt with, which is why the skill's prescribed invocations
carry --yes and put the money gate in the conversation instead.
The cost is not hypothetical. A spike measured $0.396 for a single finder worker on a
trivial fixture, and a real audit's worst case is one context call plus five rounds of six
finders plus three skeptics per finding. So the guard is not advisory —
--max-calls (default 200 attempts) is the single cost knob, every attempt is counted
before the subprocess runs so retries cannot overrun the ceiling, and setting it too low
makes the driver refuse to start rather than run a scan whose only possible output is a
false clean. Look before you spend:
mode: audit — exhaustive loop-until-dry discovery
agent: claude
project: /path/to/your/project
call ceiling: 200 attempts (each of 100 tasks may retry once)
worst case: 200 attempts (1 context + 5x6 finders + 3 skeptics x 23 findings)
buys you: up to 23 findings discovered and verified
This spends real money. One worker measured ~$0.40 on a trivial fixture.
Read the exit code before you read the findings
A misread exit code on a security scan produces a silent false clean, which is the worst
failure this toolkit can have, so the codes are narrow on purpose. 0 means complete:
the result is the whole result. 3 means incomplete — the call ceiling stopped the run
early, some tasks got no usable answer, or discovery found more than --max-findings and
discarded the rest; the findings it did verify are kept, coverage was truncated, and it is never
described as clean. 2 means failed, and no report is written at all.
4 means declined: confirmation was refused, or there was no tty and no
--yes. Nothing ran and nothing was spent — re-run with --yes, and do
not fall back to the in-loop scan. 1 means there is no agent CLI on
PATH, and nothing else; that is the only case where the in-loop fallback is the right
move.
An empty result with exit 0 means clean. An empty result with any other
exit code means the scan did not run. The driver refuses to exit 0 when no task got a
usable answer, precisely so a broken run cannot be mistaken for a clean codebase.
4 was split out of 1 because 1 used to mean three things
at once — no CLI, declined, and no tty — which is how a perfectly healthy driver read as a missing
CLI, and the fan-out quietly reverted to the prose version the driver exists to replace.
Two more flags worth knowing: --concurrency sets the worker-pool
width, and --agent and --model must be passed together or not at
all, because the two CLIs' model vocabularies do not overlap and a model name from one is
meaningless to the other.
The canonical exit-code table, the mode presets and the in-loop fallback procedure live in the skill file the agent actually reads. Reach for it if you are scripting around the driver.
Upgrading
freya update is fast-forward only. Preconditions first, then a
fetch, then an explicit "can this fast-forward?" before the merge — so a diverged store gets its own
message rather than git's. Then it re-links, because a pull is not an install: a
symlinked skill picks up edits for free, but a skill added upstream has no link at all, a
deleted one leaves a dangling link behind, and a --copy install tracks
nothing.
The notify check
At most one ls-remote a day, bounded at 2 s, one line to
stderr so stdout stays parseable for the agent that ran the command. It never
applies anything: auto-update was rejected on purpose for a toolkit that gates
wrap-up.
freya init
Writes a marker-delimited section into a project's AGENTS.md, with
the skill table generated from each SKILL.md so it cannot drift. Idempotent,
CRLF-preserving, atomic, and it never rewrites a byte outside its own markers.
freya doctor
Reports the install mode per agent, and warns on the two failures that look fine from outside: a link left behind when the checkout moved, and the same skill registered twice because both install paths were used.
Coming from 0.1.0
Every skill directory and every name: gained the
freya- prefix, and there is no alias — the old names are
directories that no longer exist. There is a runnable recipe for it; see below.
This is the one you will hit within a week and be unable to diagnose. Agents read their skill list once, at session start. So an update that lands mid-session changes the disk without changing what the open session believes.
A mid-session freya update… | Seen by the open session? |
|---|---|
| edited an existing skill | Sometimes — one host re-reads the body from disk; another serves a skill it has already loaded from its cache |
| added a skill | No — it is not in the start-up snapshot |
| removed or renamed a skill | Worse than no — still offered, then a raw ENOENT at the point of use |
That third row is how the mechanism is known rather than assumed: with a skill's link moved out
from under a live Copilot session, the session still listed it, then failed to load it with
ENOENT: no such file or directory. The registry is snapshotted at session start; the
body is read from disk at invocation. The fix is a reload, not a restart —
/reload-skills in Claude Code, /skills in Copilot —
and freya update now prints that reminder whenever it actually moves the store.
The migrations are runnable recipes, not history: kept current against the shipped CLI, idempotent, and safe to re-run. Run the rename first if you need both — the knowledge-base move assumes the launcher and the renamed skills.
When something goes wrong
Six symptoms that are real, diagnosable, and easy to misread as something worse.
| Symptom | What it is, and what to do |
|---|---|
| A skill vanished, or fails right after an update | The session's skill registry is a start-up snapshot. Reload, don't restart
— /reload-skills in Claude Code, /skills in Copilot. A renamed or
removed skill is the bad case: it is still offered and then fails with a filesystem error. |
freya: command not found |
~/.local/bin is not on your PATH. The installer printed the
exact export line for your shell and deliberately did not add it for you. Add it, then put
it in your shell profile. |
| Every skill appears twice | Both install paths were run — a marketplace install and a symlink install.
freya doctor confirms it and names which. Remove one. |
| Blast radius comes back empty on a project that uses path aliases | Usually a stale graph: queries answer from the last build. Rebuild it. If it is still empty, that is a defect rather than an answer — an empty result is treated as a bug here, for the reason set out in How it works. |
| The security report is empty but the command exited non-zero | The scan did not run. Empty plus 0 is clean; empty plus
anything else is a run that failed, was truncated, or was declined. Read the code, then
re-run — do not record it as a clean result. |
| wrap-up says the project is unsynced | Working as designed. Incremental update has no prior sync to work from, and it refuses to
turn that into a silent full-codebase generation. Run
freya-spec-manager bootstrap once, then wrap up. |
Known defects that are open rather than mysterious — including a
--copy install being re-copied on every update — are tracked in
knowledge-base/roadmap.md,
which is the file that actually gets updated. If your symptom is on that list, it is not you.