You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Extract the install-method classifier that #115 (GitNexus companion) scoped narrowly to one
tool into a small, reusable module that any current or future Agentic Kit-managed package can use to
tell the truth about how it is installed before deciding whether Agentic Kit owns it.
#115 named this exact extraction and deliberately deferred it:
Building a generic "any npm package under mise" adapter as part of this issue. The install-method
classifier this proposal needs should be written narrowly enough to serve GitNexus (and be
reusable later), but generalizing it into a public "mise-aware package manager" abstraction is a
separate, larger refactor and out of scope here.
This issue is that separate, larger refactor. It is not a hard prerequisite for #115 — #115's
own Phase 1 can still ship a GitNexus-narrow classifier on its own schedule; this module is designed
so that classifier can be swapped for the shared one in a small follow-up diff whenever both have
landed, in whichever order that happens (see Phase 3 and the Consumers section below). Related: #115.
This is not a hypothetical generalization. Checking mise's own registry against the five packages
Agentic Kit already manages shows the blind spot is live today, not just plausible for a future
companion:
Package
Agentic Kit's current detection
mise npm: backend (generic, always available)
mise dedicated short-name plugin
ruflo
global npm package.json read
✅ resolvable (npm:ruflo)
— none registered
agentic-qe
global npm package.json read
✅ resolvable (npm:agentic-qe)
— none registered
@anthropic-ai/claude-code
presence/--version probe, external-disowned
✅ resolvable (npm:@anthropic-ai/claude-code)
claude / claude-code → aqua:anthropics/claude-code or http:claude
(mise registry and mise ls-remote npm:<pkg> output for each row captured 2026-08-05 on this
machine; see Evidence below.)
Three distinct gaps fall out of that table, and this issue's design has to answer all three, not
just the one #115 needed:
mise's generic npm: backend installs a package into an isolated ~/.local/share/mise/installs/npm-<pkg>/<version>/ directory, invisible to installedVersion()'s global-npm-root read. This is gitnexus's actual state on this machine
today, and any of the other four packages could just as easily be installed the same way — the
user already uses this exact backend for five other npm packages
(npm:@gastown/gt, npm:@northflank/cli, npm:markdownlint-cli2, npm:pnpm, npm:gitnexus) in
the same ~/.config/mise/config.toml.
mise ships 19 built-in backends (mise backends ls: aqua, asdf, cargo, conda, core, dotnet, forgejo, gem, github, gitlab, go, npm, pipx, pkgx, spm, http, s3, ubi, vfox), and mise's own registry already defaults Claude Code and OpenCode to aqua/http
rather than npm. Any tool Agentic Kit manages or might manage could, in principle, arrive via any
of these — a classifier that only recognizes npm: and hardcodes one or two others as a special
case will keep missing cases as mise's registry grows. This proposal's design (below) sidesteps
that by asking mise itself which backend owns a tool, rather than trying to enumerate/hardcode all
19 in Agentic Kit's own code — see "Design."
mise-managed runtimes hosting a normal global npm install are already fine and must not be
"fixed." This machine's codex and opencode-ai are npm-global packages installed under
mise's node backend (~/.local/share/mise/installs/node/26.4.0/lib/node_modules/...), and npm root -g already resolves there correctly regardless of who manages the active Node version.
Confirmed by direct inspection (see Evidence). A generalized classifier must not reclassify this
already-correct case as a new "mise" bucket — that would be a regression dressed up as a fix.
Why this belongs in Agentic Kit
docs/MANAGED-TOOLS.md's hosts row already states the policy in prose: "npm @latest — only when
npm-managed... explicitly disowned if brew/mise/native... disk: global package.json, else --version probe." That policy is correct and should not change here — hosts remain
never-touched when externally installed (per this machine's own CLAUDE.md: "Externally-installed
CLIs are never touched... it will not shadow it with an npm copy or try to update it"). What's
missing is specificity. Today "external" is one undifferentiated bucket. A user whose Claude Code
came from mise's aqua:anthropics/claude-code plugin and a user who ran the native installer both
get the identical external — self-managed status line, with no way to know which remediation
applies (mise upgrade claude vs claude update vs brew upgrade).
src/lib/versions.mjs's installedVersion(pkg) (line 9-17) reads only path.join(globalRoot(), pkg, 'package.json'). driftReport()'s own comment (line 62-66) already
documents the consequence: mise/native/brew installs resolve to null and are filtered out of
drift entirely, "so ak never claims to manage an update it doesn't own." That's the right
fail-closed behavior for an unclassified external install. It becomes an unnecessarily blunt
behavior once a classifier can tell mise's npm: backend (which Agentic Kit can safely drive,
per #115) apart from mise's aqua:/http: backend (which it cannot, without taking on GitHub-release
version comparison and binary-replacement semantics that are a separate, larger decision) apart from
a genuinely unrelated installer.
src/lib/natives.mjs is the existing precedent for a small, focused, disk-truth-reading module
serving multiple call sites without becoming a framework — the right shape to copy here, not src/lib/adapters/* (which model an entire tool's lifecycle, not a shared detection primitive).
Evidence (captured 2026-08-05, this machine)
$ mise registry | grep -iE "claude|codex|opencode|ruflo|agentic"
claude aqua:anthropics/claude-code http:claude
claude-code aqua:anthropics/claude-code http:claude
codex aqua:openai/codex npm:@openai/codex
opencode aqua:anomalyco/opencode
# no entry for ruflo or agentic-qe — npm: backend only, generic
$ mise ls-remote npm:ruflo | tail -1 # 3.34.0 — resolvable
$ mise ls-remote npm:agentic-qe | tail -1 # 3.13.9 — resolvable
$ mise ls-remote npm:@anthropic-ai/claude-code | tail -1 # 2.1.221 — resolvable
$ mise ls-remote npm:@openai/codex | tail -1 # 0.146.0 — resolvable
$ mise ls-remote npm:opencode-ai | tail -1 # 1.18.13 — resolvable
$ readlink -f "$(which claude)"
/Users/cphillipson/.local/share/claude/versions/2.1.222 # native installer, NOT mise
$ readlink -f "$(which codex)"
/Users/cphillipson/.local/share/mise/installs/node/26.4.0/lib/node_modules/@openai/codex/bin/codex.js
# npm-global under mise's node — already handled correctly
$ npm root -g
/Users/cphillipson/.local/share/mise/installs/node/26.4.0/lib/node_modules # includes codex/opencode, NOT gitnexus
$ npm ls -g gitnexus
/Users/.../node/26.4.0/lib
└── (empty) # confirms the actual blind spot
$ ls ~/.local/share/mise/installs/ | grep '^npm-'
npm-beads-bd npm-gastown-gt npm-gitnexus npm-markdownlint-cli2 npm-northflank-cli npm-pnpm
$ mise backends ls
aqua asdf cargo conda core dotnet forgejo gem github gitlab go npm pipx pkgx spm http s3 ubi vfox
# 19 backends total, not just npm/aqua/http
$ mise ls --json | jq '."npm:gitnexus", ."asdf:Banno/asdf-kustomize", .node[0].install_path'
[
{
"version": "1.6.9",
"install_path": "/Users/.../mise/installs/npm-gitnexus/1.6.9",
"source": { "type": "mise.toml", "path": "~/.config/mise/config.toml" },
"installed": true, "active": true
}
]
[
{
"version": "5.8.1",
"install_path": "/Users/.../mise/installs/asdf-banno-asdf-kustomize/5.8.1",
...
}
]
"/Users/.../mise/installs/node/26.4.0" # core-backend tools get a BARE
# directory name — no prefix at all,
# unlike npm-/asdf-/github- prefixed
# explicit-backend directories
mise ls --json already returns, for every configured tool, an exact backend-qualified key
(npm:gitnexus, asdf:Banno/asdf-kustomize, or a bare core name like node) and its resolved install_path — the authoritative answer to "which backend owns this" with no need to reverse-engineer
directory-naming conventions per backend (which aren't uniform: explicit-backend tools get a <backend>-<slug> directory name, core tools don't).
Goals
Add a small, reusable classifier — e.g. src/lib/install-method.mjs — that, given a binary name
and/or npm package name, resolves it on PATH and returns a labeled result covering any of
mise's 19 backends, not a hardcoded shortlist, plus npm-global, pnpm-global, external, and absent for non-mise cases.
Delegate backend identity to mise itself instead of hardcoding a prefix list. Directory-naming
is not uniform across backends (explicit-backend tools get a <backend>-<slug> directory; core
tools like node/go get a bare name) and mise's registry/backend set changes over time. mise ls --json already reports, per configured tool, the exact backend-qualified key and install_path — resolve the target binary's realpath, match it against install_path values from mise ls --json, and read the backend straight off the matching key's <backend>: prefix (or core when unprefixed). This means Agentic Kit never needs its own copy of mise's backend list to
keep in sync.
Make the classification disk-first (resolve the real binary path; a mise.toml/config.toml
entry alone doesn't prove the tool is actually installed or that PATH resolves to it) per docs/MANAGED-TOOLS.md invariant Activate ruvector self-learning, agentic-qe, security + activation status line #1. Falling back to mise ls --json still starts from the
resolved realpath, not from reading the config file's intent.
Retrofit driftReport()'s host-CLI filtering (the HOST_PKGS null-fallback in src/lib/versions.mjs) to use the classifier for labeling only — ak status's external — self-managed line gains a specific backend (e.g. external — self-managed (mise: aqua:anthropics/claude-code) vs external — self-managed (native installer)) without changing which hosts get updated.
Support driving an update through the classified backend only where the version namespace is
npm semver and Agentic Kit already knows how to compare/drive it: mise upgrade npm:<pkg> for a
mise npm backend install, npm install -g <pkg>@latest for npm-global, pnpm update -g <pkg>
for pnpm-global. Every other backend (aqua, asdf, cargo, go, gem, pipx, spm, ubi, vfox, http, dotnet, conda, gitlab, forgejo, s3, pkgx) is detected and labeled but
never treated as updatable — see Non-goals.
Keep the same-namespace comparison invariant (docs/MANAGED-TOOLS.mdinstall.sh fills Claude Code sandbox tmpfs, causing session crashes (ENOSPC) #3) intact: npm dist-tag vs
npm dist-tag for the three updatable methods above; no other backend gets a version comparison
in this issue, since each has its own release/versioning model (GitHub tags for aqua/github,
crates.io semver for cargo, etc.) that a "latest" comparison can't be generalized across safely
without backend-specific work this issue doesn't scope.
Non-goals
Do not change the host-CLI never-touch policy. Claude Code, Codex, and OpenCode remain
self-managed when externally installed, exactly as CLAUDE.md and docs/MANAGED-TOOLS.md already
state. This issue only makes the label more specific; it does not expand ak sync to start
updating mise- or Homebrew-installed hosts.
Do not implement update-driving for any backend other than mise's npm, plain npm-global, and pnpm-global. Detecting and correctly labeling an aqua, asdf, cargo, go, gem, pipx, spm, ubi, vfox, http, dotnet, conda, gitlab, forgejo, s3, or pkgx install is in
scope; comparing its version against "latest" or driving an update for it is a separate, larger
decision (each has a different version-provenance model) and explicitly out of scope here.
Do not build a general "scan the whole PATH/system for every tool" inventory. The classifier
answers "how is this specific, named package/binary installed?" on request from a caller that
already knows what it's looking for (an adapter, driftReport(), ak host status) — it is not a
standalone discovery/audit command.
Do not retrofit ak host status's existing "self-managed" wording/behavior for hosts beyond
adding the backend qualifier described in Goal 4. Any broader host-management UX change is a
separate proposal.
// src/lib/install-method.mjs// classifyInstall(binName, { npmPkg } = {}) -> {// method: 'npm-global' | 'pnpm-global' | 'mise' | 'external' | 'absent',// backend: string | null, // when method === 'mise': the exact backend mise reported// // ('npm', 'aqua', 'asdf', 'github', 'cargo', ... — whatever// // `mise ls --json` returns, never a hardcoded enum)// backendRef: string | null, // the full qualified key, e.g. 'npm:gitnexus',// // 'asdf:Banno/asdf-kustomize', or the bare core name// path: string | null, // resolved realpath, or null if absent// version: string | null, // read from the method-appropriate source// versionNamespace: 'npm' | 'unknown',// updatable: boolean, // true only for npm-global | pnpm-global | mise+backend==='npm'// }
Detection order (disk-first, cheapest-first, mise-delegated rather than self-maintained):
Resolve binName on PATH; if unresolvable, return { method: 'absent', ... }.
Resolve the realpath under npm root -g / npm config get prefix first — this covers both plain
npm-global and npm-global-under-a-mise-managed-Node (Gap 3), which must classify as npm-global regardless of who manages the active Node runtime. Same check for pnpm root -g → pnpm-global.
If the realpath falls under mise's data directory (mise root/MISE_DATA_DIR, default ~/.local/share/mise) but wasn't caught by step 2, ask mise which tool that is instead of
guessing from the directory name: run mise ls --json (cached per invocation — this already
returns every configured tool's install_path) and find the entry whose install_path is a
prefix of the resolved realpath. Split that entry's key on the first : to get backend (or core if there's no colon) and the tool reference; method: 'mise'; version comes straight
from the matched entry's version field; updatable: backend === 'npm'; versionNamespace: backend === 'npm' ? 'npm' : 'unknown'.
If mise itself isn't invokable (not on PATH, or the query fails) but the realpath still falls
under mise's data directory, degrade to method: 'mise', backend: null, updatable: false —
report "mise-managed, backend unknown" rather than guessing from directory-naming conventions
that aren't uniform across backends (see Evidence: core tools are unprefixed, explicit-backend
tools are <backend>-<slug>-prefixed, and that mapping isn't guaranteed stable).
Anything else on disk → external; versionNamespace: 'unknown'; updatable: false.
Never write, never invoke an updater — this module only reads.
This design means Agentic Kit's own code never enumerates mise's 19 backends (today's count — mise
adds more over time); it only special-cases the one backend (npm) whose version namespace it
already knows how to compare and drive, and passes every other backend's identity straight through
from mise's own answer.
Consumers
feat: manage GitNexus as an optional local code-intelligence companion #115 (GitNexus): its adapter's detect step calls classifyInstall('gitnexus', { npmPkg: 'gitnexus' }) instead of writing gitnexus-specific path matching, and its apply step branches
on method/backend/updatable to pick mise upgrade npm:gitnexus vs npm install -g gitnexus@latest.
driftReport() / ak host status (this issue): calls classifyInstall for each HOST_PKGS
entry; when method !== 'npm-global', the status line gains the specific backendRef mise
reported (or 'native installer'/'homebrew'/'unknown' for the plain external case,
best-effort) instead of just "external."
Implement classifyInstall() against real fixture directory trees and mocked mise ls --json
output covering: mise npm backend, at least two non-npm mise backends with different
directory-naming shapes (e.g. aqua/http-style bare short-name directory and asdf-style <backend>-<slug> directory, both drawn from this issue's own Evidence section),
mise-managed-but-mise-unqueryable degraded path, npm-global-under-mise-node, npm-global-under-
system-node, pnpm-global, and unresolvable/absent.
Explicitly test Gap 3 (npm-global under a mise-managed Node classifies as npm-global, not mise) as a named regression-guard case, since it's easy to get wrong.
No production call sites changed yet.
QA gate: every classification outcome in the Test strategy table is fixture-covered; a case
that doesn't match any known shape returns external/unknown, never a guessed method or backend.
Phase 2 — host-CLI status labeling
Wire classifyInstall into the existing host-CLI detection path used by ak status/ak host status for the label-only enhancement in Goal 4.
No behavior change to which hosts ak sync updates — verify with a regression test that asserts driftReport()'s update decisions are identical before/after for every fixture.
QA gate:ak status shows a specific backend (e.g. aqua:anthropics/claude-code) for a
mise-installed host in a fixture environment; sync behavior is provably unchanged.
method: 'npm-global', updatable: true — must not be misclassified as mise
npm-global under system/nvm-managed Node
method: 'npm-global', updatable: true
pnpm-global
method: 'pnpm-global', updatable: true
Homebrew cellar / hand-downloaded binary
method: 'external', updatable: false
unresolvable on PATH
method: 'absent'
resolvable on PATH but realpath unreadable (permissions)
method: 'external', never a crash
Acceptance criteria
src/lib/install-method.mjs exists, is read-only, and covers every classification outcome in
the table above with fixture tests, including at least three distinct mise backends
(npm, one GitHub-release-shaped backend such as aqua or http, and one plugin-shaped
backend such as asdf) to prove the design generalizes rather than special-casing exactly two.
Gap 3 (npm-global under a mise-managed Node) is explicitly regression-tested and continues to
classify as npm-global, never mise.
ak status/ak host status shows the specific mise backend for host CLIs where classifiable,
with zero change to which hosts ak sync updates (regression-tested).
No backend other than npm (mise or otherwise) is ever treated as updatable.
Agentic Kit's own source contains no hardcoded list of mise backend names used for
classification decisions — backend identity always comes from mise ls --json's own output.
pnpm run check passes.
Risks and mitigations
Risk
Mitigation
Misclassifying npm-global-under-mise-node as a mise bucket (Gap 3), causing a false "unowned" or a wrong update command
Named regression fixture in Phase 1; classify by resolution root (npm root -g/pnpm root -g) before checking mise's data directory at all
mise adds/renames/removes backends upstream
Not a maintenance burden by design — the classifier never hardcodes the 19-backend list; it passes through whatever mise ls --json reports for the matched install_path
mise ls --json shape changes across mise versions
Parse defensively (missing/renamed fields degrade to backend: null, not a crash); pin the mise version used to generate fixtures in the test file and note it in a comment
Scope creep into "manage every mise backend"
Non-goals section is explicit; only npm (mise or plain) and pnpm-global are ever updatable
Silent policy change to host-CLI ownership
Phase 2's QA gate requires a regression test proving sync decisions are byte-identical before/after
Performance: shelling out to mise ls --json on every classification call
Cache the parsed result for the duration of one detect/status pass (already the pattern driftReport() uses for its own TTL cache) rather than re-invoking per binary
Definition of done
This issue is complete when Agentic Kit can classify, for any named binary/package, whether it is
npm-global, pnpm-global, mise-managed (with the exact backend mise itself reports — covering all 19
of mise's current backends without Agentic Kit hardcoding that list), external, or absent — using
disk truth, not configuration-file inference — and when that classification is (a) available as a
shared module #115's GitNexus adapter can depend on, and (b) surfaced as a more specific label for
already-disowned host CLIs in ak status, without changing which artifacts Agentic Kit actually
updates.
Executive brief
Extract the install-method classifier that
#115 (GitNexus companion) scoped narrowly to one
tool into a small, reusable module that any current or future Agentic Kit-managed package can use to
tell the truth about how it is installed before deciding whether Agentic Kit owns it.
#115 named this exact extraction and deliberately deferred it:
This issue is that separate, larger refactor. It is not a hard prerequisite for #115 — #115's
own Phase 1 can still ship a GitNexus-narrow classifier on its own schedule; this module is designed
so that classifier can be swapped for the shared one in a small follow-up diff whenever both have
landed, in whichever order that happens (see Phase 3 and the Consumers section below). Related: #115.
This is not a hypothetical generalization. Checking mise's own registry against the five packages
Agentic Kit already manages shows the blind spot is live today, not just plausible for a future
companion:
npm:backend (generic, always available)ruflopackage.jsonreadnpm:ruflo)agentic-qepackage.jsonreadnpm:agentic-qe)@anthropic-ai/claude-code--versionprobe, external-disownednpm:@anthropic-ai/claude-code)claude/claude-code→aqua:anthropics/claude-codeorhttp:claude@openai/codexpackage.jsonreadnpm:@openai/codex)codex→aqua:openai/codexornpm:@openai/codexopencode-aipackage.jsonreadnpm:opencode-ai)opencode→aqua:anomalyco/opencodegitnexus(#115, proposed)(
mise registryandmise ls-remote npm:<pkg>output for each row captured 2026-08-05 on thismachine; see Evidence below.)
Three distinct gaps fall out of that table, and this issue's design has to answer all three, not
just the one #115 needed:
npm:backend installs a package into an isolated~/.local/share/mise/installs/npm-<pkg>/<version>/directory, invisible toinstalledVersion()'s global-npm-root read. This is gitnexus's actual state on this machinetoday, and any of the other four packages could just as easily be installed the same way — the
user already uses this exact backend for five other npm packages
(
npm:@gastown/gt,npm:@northflank/cli,npm:markdownlint-cli2,npm:pnpm,npm:gitnexus) inthe same
~/.config/mise/config.toml.mise backends ls:aqua,asdf,cargo,conda,core,dotnet,forgejo,gem,github,gitlab,go,npm,pipx,pkgx,spm,http,s3,ubi,vfox), and mise's own registry already defaults Claude Code and OpenCode toaqua/httprather than npm. Any tool Agentic Kit manages or might manage could, in principle, arrive via any
of these — a classifier that only recognizes
npm:and hardcodes one or two others as a specialcase will keep missing cases as mise's registry grows. This proposal's design (below) sidesteps
that by asking mise itself which backend owns a tool, rather than trying to enumerate/hardcode all
19 in Agentic Kit's own code — see "Design."
"fixed." This machine's
codexandopencode-aiare npm-global packages installed undermise's
nodebackend (~/.local/share/mise/installs/node/26.4.0/lib/node_modules/...), andnpm root -galready resolves there correctly regardless of who manages the active Node version.Confirmed by direct inspection (see Evidence). A generalized classifier must not reclassify this
already-correct case as a new "mise" bucket — that would be a regression dressed up as a fix.
Why this belongs in Agentic Kit
docs/MANAGED-TOOLS.md's hosts row already states the policy in prose: "npm@latest— only whennpm-managed... explicitly disowned if brew/mise/native... disk: global
package.json, else--versionprobe." That policy is correct and should not change here — hosts remainnever-touched when externally installed (per this machine's own CLAUDE.md: "Externally-installed
CLIs are never touched... it will not shadow it with an npm copy or try to update it"). What's
missing is specificity. Today "external" is one undifferentiated bucket. A user whose Claude Code
came from mise's
aqua:anthropics/claude-codeplugin and a user who ran the native installer bothget the identical
external — self-managedstatus line, with no way to know which remediationapplies (
mise upgrade claudevsclaude updatevsbrew upgrade).src/lib/versions.mjs'sinstalledVersion(pkg)(line 9-17) reads onlypath.join(globalRoot(), pkg, 'package.json').driftReport()'s own comment (line 62-66) alreadydocuments the consequence: mise/native/brew installs resolve to
nulland are filtered out ofdrift entirely, "so ak never claims to manage an update it doesn't own." That's the right
fail-closed behavior for an unclassified external install. It becomes an unnecessarily blunt
behavior once a classifier can tell mise's
npm:backend (which Agentic Kit can safely drive,per #115) apart from mise's
aqua:/http:backend (which it cannot, without taking on GitHub-releaseversion comparison and binary-replacement semantics that are a separate, larger decision) apart from
a genuinely unrelated installer.
src/lib/natives.mjsis the existing precedent for a small, focused, disk-truth-reading moduleserving multiple call sites without becoming a framework — the right shape to copy here, not
src/lib/adapters/*(which model an entire tool's lifecycle, not a shared detection primitive).Evidence (captured 2026-08-05, this machine)
mise ls --jsonalready returns, for every configured tool, an exact backend-qualified key(
npm:gitnexus,asdf:Banno/asdf-kustomize, or a bare core name likenode) and its resolvedinstall_path— the authoritative answer to "which backend owns this" with no need to reverse-engineerdirectory-naming conventions per backend (which aren't uniform: explicit-backend tools get a
<backend>-<slug>directory name, core tools don't).Goals
src/lib/install-method.mjs— that, given a binary nameand/or npm package name, resolves it on PATH and returns a labeled result covering any of
mise's 19 backends, not a hardcoded shortlist, plus
npm-global,pnpm-global,external, andabsentfor non-mise cases.is not uniform across backends (explicit-backend tools get a
<backend>-<slug>directory; coretools like
node/goget a bare name) and mise's registry/backend set changes over time.mise ls --jsonalready reports, per configured tool, the exact backend-qualified key andinstall_path— resolve the target binary's realpath, match it againstinstall_pathvalues frommise ls --json, and read the backend straight off the matching key's<backend>:prefix (orcorewhen unprefixed). This means Agentic Kit never needs its own copy of mise's backend list tokeep in sync.
mise.toml/config.tomlentry alone doesn't prove the tool is actually installed or that PATH resolves to it) per
docs/MANAGED-TOOLS.mdinvariant Activate ruvector self-learning, agentic-qe, security + activation status line #1. Falling back tomise ls --jsonstill starts from theresolved realpath, not from reading the config file's intent.
driftReport()'s host-CLI filtering (theHOST_PKGSnull-fallback insrc/lib/versions.mjs) to use the classifier for labeling only —ak status'sexternal — self-managedline gains a specific backend (e.g.external — self-managed (mise: aqua:anthropics/claude-code)vsexternal — self-managed (native installer)) without changing which hosts get updated.gitnexus-specific classifier, and design the API so feat: manage deja-vu as an optional cross-host session-memory companion #114 (deja-vu) could adopt it later if a user
installs deja-vu via any mise backend too.
npm semver and Agentic Kit already knows how to compare/drive it:
mise upgrade npm:<pkg>for amise
npmbackend install,npm install -g <pkg>@latestfornpm-global,pnpm update -g <pkg>for
pnpm-global. Every other backend (aqua,asdf,cargo,go,gem,pipx,spm,ubi,vfox,http,dotnet,conda,gitlab,forgejo,s3,pkgx) is detected and labeled butnever treated as
updatable— see Non-goals.docs/MANAGED-TOOLS.mdinstall.sh fills Claude Code sandbox tmpfs, causing session crashes (ENOSPC) #3) intact: npm dist-tag vsnpm dist-tag for the three
updatablemethods above; no other backend gets a version comparisonin this issue, since each has its own release/versioning model (GitHub tags for
aqua/github,crates.io semver for
cargo, etc.) that a "latest" comparison can't be generalized across safelywithout backend-specific work this issue doesn't scope.
Non-goals
self-managed when externally installed, exactly as CLAUDE.md and
docs/MANAGED-TOOLS.mdalreadystate. This issue only makes the label more specific; it does not expand
ak syncto startupdating mise- or Homebrew-installed hosts.
npm, plainnpm-global, andpnpm-global. Detecting and correctly labeling anaqua,asdf,cargo,go,gem,pipx,spm,ubi,vfox,http,dotnet,conda,gitlab,forgejo,s3, orpkgxinstall is inscope; comparing its version against "latest" or driving an update for it is a separate, larger
decision (each has a different version-provenance model) and explicitly out of scope here.
answers "how is this specific, named package/binary installed?" on request from a caller that
already knows what it's looking for (an adapter,
driftReport(),ak host status) — it is not astandalone discovery/audit command.
ak host status's existing "self-managed" wording/behavior for hosts beyondadding the backend qualifier described in Goal 4. Any broader host-management UX change is a
separate proposal.
provides the shared primitive feat: manage GitNexus as an optional local code-intelligence companion #115 already named as a future extraction.
Design
Module shape
Detection order (disk-first, cheapest-first, mise-delegated rather than self-maintained):
binNameon PATH; if unresolvable, return{ method: 'absent', ... }.npm root -g/npm config get prefixfirst — this covers both plainnpm-global and npm-global-under-a-mise-managed-Node (Gap 3), which must classify as
npm-globalregardless of who manages the active Node runtime. Same check forpnpm root -g→pnpm-global.mise root/MISE_DATA_DIR, default~/.local/share/mise) but wasn't caught by step 2, ask mise which tool that is instead ofguessing from the directory name: run
mise ls --json(cached per invocation — this alreadyreturns every configured tool's
install_path) and find the entry whoseinstall_pathis aprefix of the resolved realpath. Split that entry's key on the first
:to getbackend(orcoreif there's no colon) and the tool reference;method: 'mise';versioncomes straightfrom the matched entry's
versionfield;updatable: backend === 'npm';versionNamespace: backend === 'npm' ? 'npm' : 'unknown'.under mise's data directory, degrade to
method: 'mise',backend: null,updatable: false—report "mise-managed, backend unknown" rather than guessing from directory-naming conventions
that aren't uniform across backends (see Evidence: core tools are unprefixed, explicit-backend
tools are
<backend>-<slug>-prefixed, and that mapping isn't guaranteed stable).external;versionNamespace: 'unknown';updatable: false.This design means Agentic Kit's own code never enumerates mise's 19 backends (today's count — mise
adds more over time); it only special-cases the one backend (
npm) whose version namespace italready knows how to compare and drive, and passes every other backend's identity straight through
from mise's own answer.
Consumers
detectstep callsclassifyInstall('gitnexus', { npmPkg: 'gitnexus' })instead of writing gitnexus-specific path matching, and itsapplystep brancheson
method/backend/updatableto pickmise upgrade npm:gitnexusvsnpm install -g gitnexus@latest.driftReport()/ak host status(this issue): callsclassifyInstallfor eachHOST_PKGSentry; when
method !== 'npm-global', the status line gains the specificbackendRefmisereported (or
'native installer'/'homebrew'/'unknown'for the plainexternalcase,best-effort) instead of just "external."
under any backend; not implemented as part of this issue, just unblocked by it.
Phased implementation plan
Phase 1 — the classifier module and fixtures
classifyInstall()against real fixture directory trees and mockedmise ls --jsonoutput covering: mise
npmbackend, at least two non-npm mise backends with differentdirectory-naming shapes (e.g.
aqua/http-style bare short-name directory andasdf-style<backend>-<slug>directory, both drawn from this issue's own Evidence section),mise-managed-but-mise-unqueryable degraded path, npm-global-under-mise-node, npm-global-under-
system-node, pnpm-global, and unresolvable/absent.
npm-global, notmise) as a named regression-guard case, since it's easy to get wrong.QA gate: every classification outcome in the Test strategy table is fixture-covered; a case
that doesn't match any known shape returns
external/unknown, never a guessed method or backend.Phase 2 — host-CLI status labeling
classifyInstallinto the existing host-CLI detection path used byak status/ak host statusfor the label-only enhancement in Goal 4.ak syncupdates — verify with a regression test that assertsdriftReport()'s update decisions are identical before/after for every fixture.QA gate:
ak statusshows a specific backend (e.g.aqua:anthropics/claude-code) for amise-installed host in a fixture environment; sync behavior is provably unchanged.
Phase 3 — hand-off to #115
in flight. If feat: manage GitNexus as an optional local code-intelligence companion #115 lands first, its narrow classifier gets replaced by this module in a small
follow-up diff rather than duplicated.
QA gate: #115's install-method test matrix passes unchanged when backed by this module.
Test strategy
classifyInstall()resultnpmbackend (npm-gitnexus-style dir;mise ls --jsonkeynpm:gitnexus)method: 'mise',backend: 'npm',updatable: true,versionNamespace: 'npm'aqua/httpbackend (claude-style dir; keyaqua:anthropics/claude-codeorhttp:claude)method: 'mise',backend: 'aqua'(or'http'),updatable: false,versionNamespace: 'unknown'asdfbackend (asdf-banno-asdf-kustomize-style dir; keyasdf:Banno/asdf-kustomize)method: 'mise',backend: 'asdf',updatable: falsecorebackend (barenode/go-style dir, no prefix; key is the bare tool name)method: 'mise',backend: 'core',updatable: falsemise ls --jsonunqueryable (mise absent/broken)method: 'mise',backend: null,updatable: false— degrade, don't guessmethod: 'npm-global',updatable: true— must not be misclassified asmisemethod: 'npm-global',updatable: truemethod: 'pnpm-global',updatable: truemethod: 'external',updatable: falsemethod: 'absent'method: 'external', never a crashAcceptance criteria
src/lib/install-method.mjsexists, is read-only, and covers every classification outcome inthe table above with fixture tests, including at least three distinct mise backends
(
npm, one GitHub-release-shaped backend such asaquaorhttp, and one plugin-shapedbackend such as
asdf) to prove the design generalizes rather than special-casing exactly two.classify as
npm-global, nevermise.ak status/ak host statusshows the specific mise backend for host CLIs where classifiable,with zero change to which hosts
ak syncupdates (regression-tested).of duplicating classification logic.
npm(mise or otherwise) is ever treated asupdatable.classification decisions — backend identity always comes from
mise ls --json's own output.pnpm run checkpasses.Risks and mitigations
npm root -g/pnpm root -g) before checking mise's data directory at allmise ls --jsonreports for the matchedinstall_pathmise ls --jsonshape changes across mise versionsbackend: null, not a crash); pin the mise version used to generate fixtures in the test file and note it in a commentnpm(mise or plain) andpnpm-globalare everupdatablemise ls --jsonon every classification calldetect/statuspass (already the patterndriftReport()uses for its own TTL cache) rather than re-invoking per binaryDefinition of done
This issue is complete when Agentic Kit can classify, for any named binary/package, whether it is
npm-global, pnpm-global, mise-managed (with the exact backend mise itself reports — covering all 19
of mise's current backends without Agentic Kit hardcoding that list), external, or absent — using
disk truth, not configuration-file inference — and when that classification is (a) available as a
shared module #115's GitNexus adapter can depend on, and (b) surfaced as a more specific label for
already-disowned host CLIs in
ak status, without changing which artifacts Agentic Kit actuallyupdates.