feat(cli/dir): dirctl install upgrade, builtin record tracking, and highest-version name resolution - #2156
Conversation
Signed-off-by: András Jáky <ajaky@cisco.com>
Signed-off-by: András Jáky <ajaky@cisco.com>
Signed-off-by: András Jáky <ajaky@cisco.com>
Signed-off-by: András Jáky <ajaky@cisco.com>
Signed-off-by: András Jáky <ajaky@cisco.com>
Signed-off-by: András Jáky <ajaky@cisco.com>
e09a1db to
e1801dd
Compare
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
🟡 Changes recommended
Upgrade grouping, shared artifacts, pin preservation, and recorded removal contain correctness issues.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Adds package upgrades, built-in install tracking, authoritative skill replacement, and consistent highest-version resolution.
Changes:
- Adds manifest-driven
install upgradereconciliation. - Tracks
dirctl initartifacts and centralizes built-in package derivation. - Resolves bare names to the highest semantic version.
File summaries
| File | Description |
|---|---|
CHANGELOG.md |
Documents user-facing and breaking changes. |
docs/content/dir/dir-cli-reference.md |
Documents upgrade and resolution behavior. |
cli/util/reference/reference.go |
Selects the highest semantic version. |
cli/util/reference/reference_test.go |
Tests version selection. |
cli/internal/pkgstate/pkgstate.go |
Adds artifact subtraction and manifest updates. |
cli/internal/pkgstate/pkgstate_test.go |
Tests manifest update behavior. |
cli/internal/dirpkg/dirpkg.go |
Builds the built-in DIR package and MCP environment. |
cli/internal/dirpkg/dirpkg_test.go |
Tests built-in environment derivation. |
cli/internal/agentinstall/upgrade_test.go |
Tests end-to-end reconciliation. |
cli/internal/agentinstall/record.go |
Centralizes manifest recording and reconciliation. |
cli/internal/agentinstall/record_test.go |
Tests record lifecycle behavior. |
cli/internal/agentinstall/orphan.go |
Removes obsolete artifacts during upgrades. |
cli/internal/agentinstall/orphan_test.go |
Tests orphan removal. |
cli/internal/agentcfg/skill_folder_test.go |
Tests authoritative skill folders. |
cli/internal/agentcfg/skill_engine.go |
Replaces whole skill folders. |
cli/config/config.go |
Centralizes client-config resolution. |
cli/cmd/root.go |
Uses centralized config resolution. |
cli/cmd/pull/pull.go |
Updates resolution help text. |
cli/cmd/naming/verify.go |
Updates verification help text. |
cli/cmd/install/upgrade.go |
Implements package upgrades. |
cli/cmd/install/upgrade_test.go |
Tests upgrade grouping and filtering. |
cli/cmd/install/manifest.go |
Uses shared manifest helpers. |
cli/cmd/install/install.go |
Registers and documents upgrade. |
cli/cmd/init/context.go |
Shares local connection defaults. |
cli/cmd/init/agents.go |
Records built-in agent installations. |
cli/cmd/init/agents_test.go |
Tests built-in manifest tracking. |
cli/cmd/info/info.go |
Updates lookup help text. |
Review details
Suppressed comments (1)
cli/cmd/init/agents.go:246
init --removederives the current binary's artifact names instead of removing what the manifest row records. If a newer binary renamed or dropped the built-in MCP key, this sees the new key as absent, removes the row viaForget, and leaves the old key orphaned in the agent config. Load the built-in rows and use the manifest-driven removal path so the recorded v1 keys are removed before their rows are forgotten.
arts, err := dirpkg.Artifacts(dirConfig(cmd))
if err != nil {
return err
}
plan := agentinstall.Uninstall(env, arts, selected, agentcfg.Global, true)
- Files reviewed: 27/27 changed files
- Comments generated: 6
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| if len(chosen) > 0 && !chosen[row.Entry.Agent] { | ||
| continue |
There was a problem hiding this comment.
Fixed in 6013301, by refusing the split rather than reconciling every row.
rejectSharedSkillSplits runs before anything is fetched and mirrors rejectSharedSkillSplit on the uninstall side, including the error text pointing at the agents that have to go together. Reconciling the unselected rows instead would mean ignoring --agents, which is worse: the flag would silently touch agents it was used to exclude.
Only --agents can produce the split, since every row of a package is upgraded together otherwise.
| group.rows = append(group.rows, row) | ||
| group.pinned = group.pinned || row.Pinned |
There was a problem hiding this comment.
Fixed in 6013301. groupByScope no longer carries a pin at all; the group carries its rows, and recordUpgrades calls Reconcile one agent at a time with pinnedFor(write.rows, agent.ID).
Reachable exactly as you say — install <name> --pin --agents a followed by install <name> --agents b leaves one row held and one loose at the same scope.
Four correctness fixes from review, all in `install upgrade`. Targets were keyed on the record name, so the first row's CID decided for every row of that name. Rows of one name do not always agree: without --pre, a row on a release follows the highest release while a row already on a prerelease follows its own track, so this could install a prerelease over a release track, or rebuild a Directory row from this binary when a built-in row shared the name. Keyed on the resolved target now — name, version, CID and origin — so rows that agree still share one fetch and rows that do not are kept apart. Claude Code and Claude Desktop share one skills folder, as do Zed and Codex CLI, so --agents could narrow a run to one of a pair and still rewrite the other's copy — or, when the new version drops the skill, delete the folder its row still pointed at. Refused now, the way uninstall already refuses the same split. HasChanges is false for a plan of skips and failures too, not only for byte-identical artifacts, so the version-only path claimed success for work that never happened and could bump a row whose other artifact had failed. A write now has to be wholly unchanged to count, and blocked groups keep their versions and say so. A scope-wide OR collapsed per-row pins, so `--include-pinned` pinned rows that were never held. Recording is per agent, keyed on that agent's own row. Refs #2030 Signed-off-by: András Jáky <ajaky@cisco.com>
`init --remove` derived the built-in artifacts from the running binary, so it only ever removed what *this* version writes. An MCP key an earlier dirctl wrote under another name read as already absent, Forget dropped the row as cleared, and the old key stayed in the agent's config with nothing left pointing at it. Removal reads the manifest now, which also reaches an agent no longer detected here — its files are still on disk. pkgstate.Read is the read-only companion to Update, so init and install share one way in rather than duplicating the load. Refs #2030 Signed-off-by: András Jáky <ajaky@cisco.com>
The audience is a non-secret connection field, and client/oidc_grpc.go needs it to mint GitHub Actions tokens, so a context using that flow lost it when `dirctl mcp serve` started. The two secrets, auth_token and spiffe_token, stay excluded. Refs #2030 Signed-off-by: András Jáky <ajaky@cisco.com>
Left behind when the MCP server & skills prose was added above the flag table, rendering an empty table mid-paragraph. Refs #2030 Signed-off-by: András Jáky <ajaky@cisco.com>
|
All six inline findings were legitimate and are fixed, one commit per theme, each replied to in its thread. The suppressed comment on The four fixes:
The four One correction to the PR body, which no longer mentions it: the note that |
The write side of package management: actually move packages to a new version, and make the three notions of "latest" in the codebase agree.
Builds on #2132 and #2134, both merged, which made the install manifest the single source of truth and added the commands that read it.
dirctl install upgrade [name...]Installs the newer version of every package that has one, in place. What counts as upgradable is exactly what
dirctl install outdatedreports, decided by the samepkgupdate.Checkcall — so the two commands can never disagree about what is stale. A downgrade is never offered, and a package whose versions carry no ordering is never moved.With no arguments it upgrades everything upgradable and skips pinned rows. Naming a package upgrades it even if pinned, and releases the pin — asking for it by name is a clearer statement than the hold it overrides.
--include-pinnedupgrades held packages without releasing anything, so the hold lands on the new version. Accepts--pre,--include-pinned,--agents,--project,--dry-run,--yes, and reusesagentcfg.FormatPlan/FormatSummaryso its output is the same shape asinstall.Resolution is one call per distinct name, not per row, so a package installed into three agents costs one call and one derive.
An upgrade is a reconcile, not an overwrite
The order is load-bearing:
dirctl's alone, so installing a skill replaces its whole contents. An MCP entry sits in a config file shared with the user and other tools and cannot be replaced wholesale, so a server key the new version renamed is stripped by name from the manifest row. Derive it from the new record instead and v1's key survives in.claude.jsonforever — only the row knows what v1 wrote.Reconcileexists rather than a plainRecord: carrying a just-removed key forward would leave the row claiming something gone, and send a later uninstall hunting for it.One package failing does not strand the rest of the run; it is reported under Skipped records and the others go ahead.
A new version can also derive byte-identical artifacts — an author bumps a version while changing only fields no artifact is built from. The plan is then empty, but the row still moves, or
outdatedwould go on offering the same upgrade forever. The command says so rather than appearing to do nothing:Skill installs are authoritative
InstallSkillBundlealready replaced the skill directory outright.InstallSkilldid not: it wrote one file into the folder and never looked at the rest, so a record that shipped a bundle in v1 and a plainSKILL.mdin v2 orphaned v1'sreferences/for good. Single-file skills now match the bundle path, and an otherwise-identicalSKILL.mdonly counts asunchangedwhen the folder holds nothing beside it.The folder is
dirctl's, deliberately and throughout: nothing added inside it survives a reinstall. A skill is a package pulled from the Directory, and the way to change one is to push a new version and upgrade to it, not to edit the installed copy. Preserving files added next toSKILL.mdwhile silently overwriting an editedSKILL.mdwould be a half-promise.Manifest rows for
dirctl initinitinstallsorg.agntcy/directoryand recorded nothing, so the package was invisible to every command that reads the manifest:install listdid not show it, and since #2134 made uninstall manifest-driven,dirctl uninstall org.agntcy/directoryreported it as not installed. It is now recorded withorigin: builtin— the upstream is this binary, so a version check makes no network call — and no CID, because the record is rebuilt on demand with the current timestamp and a recorded content address would look like a change on every build.init --removeclears the rows with the artifacts.The derivation moves to
cli/internal/dirpkg, becauseupgradehas to re-derive the built-in package locally rather than pull the published record of the same name. That distinction is load-bearing and would be invisible otherwise: the published record's MCP module carries no environment, so installing it would silently repointdirctl mcp serveat the default address. Re-deriving also recomputes theDIRECTORY_CLIENT_*overlay from the current client context instead of replaying stale values, and removes any possibility of offering a downgrade when the local binary leads the server's published build.Row writing moves to
agentinstall.Record/Forget, soinstall, its piped form,init, andupgradecannot drift apart on what a row means.Highest-version name resolution
reference.ResolveToCIDtookrecords[0], and the naming service orders bycreated_at DESC, so a bare name resolved to the newest-pushed record. Push v1.9.0 after v2.0.0 anddirctl install cisco.com/agentinstalled v1.9.0 whileinstall outdatedcalled v2.0.0 the latest — so a package one version behind reported as up to date against a versioninstallwould never have chosen.reference.Highestpicks the highest orderable version instead, preferring releases over prereleases unless prereleases are all that exist, and falling back to the server's order when nothing is comparable so names taggedlatestordevkeep working. Text ordering would have got it wrong anyway: the server comparesversionas a SQL text column, where1.10.0sorts below1.9.0.Fixed along the way
dirpkg.MCPServerEnvresolved the client config with an emptyContext, so it readcurrent_contextno matter what the invocation asked for.dirctl --context prod inittherefore wrote an MCP entry pointingdirctl mcp serveat a different Directory than the command itself used — the same silent repointing that installing the published record would cause, arriving by another door. The resolution moves tocliconfig.ResolveClient, which root already did inline, anddirpkgnow takes the resolved config rather than finding one.ResolveClientLenientkeeps theSkipValidationthe old call had, so a partially-set context still yields its address instead of falling back to the local default.pull,info, andnaming verifystill told users a bare name resolves to the most recently created record.Breaking change
dirctl install <name>,dirctl pull <name>,info,export, andnaming verifyresolve a bare name to the highest semantic version rather than the most recently pushed record. Versions that are not valid semver still fall back to newest-pushed. Pin an exact version with<name>:<version>to get a specific record.Installing a skill now replaces its folder's whole contents, so anything added inside a skill folder no longer survives a reinstall.
Closes #2030