Skip to content

feat(doctor): add Catalog Sources section showing every resolution layer#32

Merged
kevinelliott merged 1 commit intomainfrom
feat/doctor-catalog-sources
Apr 21, 2026
Merged

feat(doctor): add Catalog Sources section showing every resolution layer#32
kevinelliott merged 1 commit intomainfrom
feat/doctor-catalog-sources

Conversation

@kevinelliott
Copy link
Copy Markdown
Owner

Summary

`agentmgr doctor` now reports the status of every catalog source the manager consults, in resolution order, so users can tell at a glance which layer will serve their catalog.

Output

```
Catalog Sources

✓ SQLite Catalog Cache: v1.0.11, cached 81h ago
ℹ User override (dotdir): not present: ~/.agentmgr/catalog.json
ℹ User override (XDG): not present: ~/.config/agentmgr/catalog.json
ℹ System share: not present: /usr/local/share/agentmgr/catalog.json
ℹ System etc: not present: /etc/agentmgr/catalog.json
✓ Embedded baseline: v1.0.29 (100264 bytes)
```

Each present source reports its version (parsed from the file / cache row). Missing sources render as Skipped (`ℹ`) rather than Warning — absence is the expected state for most paths on a typical machine.

The check flags a Fatal if the embedded bytes are empty, since that's a build misconfiguration (build ran without `make sync-catalog`).

Exposes

`catalog.EmbeddedJSON() []byte` was added in #28; this PR uses it from `doctor` and leaves it available for future admin tooling that wants to inspect the baseline without constructing a Manager.

Test plan

  • `./bin/agentmgr doctor` renders the new section cleanly
  • `./bin/agentmgr doctor` still passes overall (sources-present implies catalog will load)
  • `make lint` clean, `go test ./... -race -short` green

🤖 Generated with Claude Code

Lists each catalog source the manager consults so users can see at
a glance which layer will serve their catalog:

  Catalog Sources
  ---------------
  ✓ SQLite Catalog Cache: v1.0.11, cached 81h ago
  ℹ User override (dotdir): not present: ~/.agentmgr/catalog.json
  ℹ User override (XDG): not present: ~/.config/agentmgr/catalog.json
  ℹ System share: not present: /usr/local/share/agentmgr/catalog.json
  ℹ System etc: not present: /etc/agentmgr/catalog.json
  ✓ Embedded baseline: v1.0.29 (100264 bytes)

Each present source reports its version (parsed from the file / cache
row). Missing sources render as Skipped, not Warning — absence is the
expected state for most paths on a typical machine.

Exposes catalog.EmbeddedJSON() to let callers (doctor, potential
future admin tools) inspect the baked-in baseline without constructing
a Manager. The check flags a Fatal if the embedded bytes are empty —
that's a build misconfiguration (build ran without `make sync-catalog`).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 21, 2026 13:27
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new “Catalog Sources” section to agentmgr doctor that reports catalog resolution layers (SQLite cache, user overrides, system paths, embedded baseline) so users can see which source will serve the catalog.

Changes:

  • Add “Catalog Sources” output section to the doctor command.
  • Implement runCatalogSourceChecks to probe cache, file overrides, and embedded catalog, including version/age reporting.
  • Surface embedded-catalog “empty bytes” as a fatal doctor error.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/cli/doctor.go
// 4. Embedded baseline (//go:embed'd into the binary)
//
// The CWD is intentionally never probed.
func runCatalogSourceChecks(ctx context.Context, cfg *config.Config, _ bool) []CheckResult {
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

runCatalogSourceChecks takes cfg *config.Config but never uses it, which will fail compilation in Go (unused parameter). Rename the parameter to _ or use cfg (e.g., for platform/config-derived paths) so the file builds.

Suggested change
func runCatalogSourceChecks(ctx context.Context, cfg *config.Config, _ bool) []CheckResult {
func runCatalogSourceChecks(ctx context.Context, _ *config.Config, _ bool) []CheckResult {

Copilot uses AI. Check for mistakes.
Comment thread internal/cli/doctor.go
case data == nil:
results = append(results, CheckResult{
Name: "SQLite Catalog Cache",
Status: CheckOK,
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the SQLite catalog cache is empty (data == nil), the check is marked CheckOK. This is effectively a missing source (common on first run) and per the section semantics should be CheckSkipped/info rather than a success checkmark so users don’t read it as “cache is healthy and in use”.

Suggested change
Status: CheckOK,
Status: CheckSkipped,

Copilot uses AI. Check for mistakes.
Comment thread internal/cli/doctor.go
Comment on lines +503 to +511
msg := fmt.Sprintf("cached %s ago", time.Since(cachedAt).Round(time.Second))
if err := json.Unmarshal(data, &c); err == nil {
msg = fmt.Sprintf("v%s, cached %s ago", c.Version, time.Since(cachedAt).Round(time.Second))
}
results = append(results, CheckResult{
Name: "SQLite Catalog Cache",
Status: CheckOK,
Message: msg,
})
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the cache row exists but can’t be unmarshaled as a catalog.Catalog, the code still reports CheckOK and just omits the version. That hides a corrupt/invalid cache entry (which catalog.Manager.loadFromCache will reject). Consider reporting CheckWarning (or CheckError) on unmarshal failure and include a remediation hint (e.g., clear the DB/cache).

Suggested change
msg := fmt.Sprintf("cached %s ago", time.Since(cachedAt).Round(time.Second))
if err := json.Unmarshal(data, &c); err == nil {
msg = fmt.Sprintf("v%s, cached %s ago", c.Version, time.Since(cachedAt).Round(time.Second))
}
results = append(results, CheckResult{
Name: "SQLite Catalog Cache",
Status: CheckOK,
Message: msg,
})
age := time.Since(cachedAt).Round(time.Second)
if err := json.Unmarshal(data, &c); err != nil {
results = append(results, CheckResult{
Name: "SQLite Catalog Cache",
Status: CheckWarning,
Message: fmt.Sprintf("cache entry is invalid: %v (cached %s ago)", err, age),
Fix: "Clear the catalog cache/DB and let it be rebuilt.",
})
} else {
results = append(results, CheckResult{
Name: "SQLite Catalog Cache",
Status: CheckOK,
Message: fmt.Sprintf("v%s, cached %s ago", c.Version, age),
})
}

Copilot uses AI. Check for mistakes.
Comment thread internal/cli/doctor.go
paths = append(paths,
struct{ label, path string }{"User override (dotdir)", filepath.Join(home, ".agentmgr", "catalog.json")},
struct{ label, path string }{"User override (XDG)", filepath.Join(home, ".config", "agentmgr", "catalog.json")},
)
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If os.UserHomeDir() fails, the user-override layers are silently omitted from the report, which can mislead users about what was checked. Consider appending explicit CheckWarning/CheckSkipped results for the user-override layers indicating that $HOME could not be determined (and therefore the paths couldn’t be evaluated).

Suggested change
)
)
} else {
results = append(results,
CheckResult{
Name: "User override (dotdir)",
Status: CheckWarning,
Message: fmt.Sprintf("skipped: could not determine $HOME, so %s could not be evaluated", filepath.Join("$HOME", ".agentmgr", "catalog.json")),
},
CheckResult{
Name: "User override (XDG)",
Status: CheckWarning,
Message: fmt.Sprintf("skipped: could not determine $HOME, so %s could not be evaluated", filepath.Join("$HOME", ".config", "agentmgr", "catalog.json")),
},
)

Copilot uses AI. Check for mistakes.
Comment thread internal/cli/doctor.go
)
}
paths = append(paths,
struct{ label, path string }{"System share", "/usr/local/share/agentmgr/catalog.json"},
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The system catalog path checked here is /usr/local/share/agentmgr/catalog.json, but packaging installs the catalog to /usr/share/agentmgr/catalog.json (see .goreleaser.yml nfpms contents). As written, doctor will incorrectly report “not present” on package installs. Add /usr/share/agentmgr/catalog.json to the checked paths (and ideally align catalog.Manager.loadEmbedded to consult the same location).

Suggested change
struct{ label, path string }{"System share", "/usr/local/share/agentmgr/catalog.json"},
struct{ label, path string }{"System share", "/usr/local/share/agentmgr/catalog.json"},
struct{ label, path string }{"System share (packaged)", "/usr/share/agentmgr/catalog.json"},

Copilot uses AI. Check for mistakes.
Comment thread internal/cli/doctor.go
Comment on lines +550 to +560
var msg string
if err := json.Unmarshal(data, &c); err == nil {
msg = fmt.Sprintf("%s (v%s)", p.path, c.Version)
} else {
msg = fmt.Sprintf("%s (invalid JSON)", p.path)
}
results = append(results, CheckResult{
Name: p.label,
Status: CheckOK,
Message: msg,
})
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When a file-based catalog source exists but contains invalid JSON, the code still marks it CheckOK. Since this layer will be ignored by catalog.Manager.loadEmbedded, it should be reported as CheckWarning (with an actionable fix like “repair or delete the file”) so users understand why it isn’t being used.

Suggested change
var msg string
if err := json.Unmarshal(data, &c); err == nil {
msg = fmt.Sprintf("%s (v%s)", p.path, c.Version)
} else {
msg = fmt.Sprintf("%s (invalid JSON)", p.path)
}
results = append(results, CheckResult{
Name: p.label,
Status: CheckOK,
Message: msg,
})
if err := json.Unmarshal(data, &c); err == nil {
results = append(results, CheckResult{
Name: p.label,
Status: CheckOK,
Message: fmt.Sprintf("%s (v%s)", p.path, c.Version),
})
} else {
results = append(results, CheckResult{
Name: p.label,
Status: CheckWarning,
Message: fmt.Sprintf("%s (invalid JSON: %v)", p.path, err),
Fix: "repair the JSON in this file or delete it so it is not treated as a catalog override",
})
}

Copilot uses AI. Check for mistakes.
Comment thread internal/cli/doctor.go
Comment on lines +574 to +582
msg := fmt.Sprintf("%d bytes", len(embeddedJSON))
if err := json.Unmarshal(embeddedJSON, &c); err == nil {
msg = fmt.Sprintf("v%s (%d bytes)", c.Version, len(embeddedJSON))
}
results = append(results, CheckResult{
Name: "Embedded baseline",
Status: CheckOK,
Message: msg,
})
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the embedded baseline, JSON unmarshal failure currently still reports CheckOK with a byte count. If the embedded bytes are present but invalid, catalog loading will fail at runtime (invalid embedded catalog). This should be surfaced as CheckError (or at least CheckWarning) so doctor accurately flags the build/runtime misconfiguration.

Suggested change
msg := fmt.Sprintf("%d bytes", len(embeddedJSON))
if err := json.Unmarshal(embeddedJSON, &c); err == nil {
msg = fmt.Sprintf("v%s (%d bytes)", c.Version, len(embeddedJSON))
}
results = append(results, CheckResult{
Name: "Embedded baseline",
Status: CheckOK,
Message: msg,
})
if err := json.Unmarshal(embeddedJSON, &c); err != nil {
results = append(results, CheckResult{
Name: "Embedded baseline",
Status: CheckError,
Message: fmt.Sprintf("invalid embedded catalog JSON (%d bytes): %v", len(embeddedJSON), err),
Fix: "run `make sync-catalog` before `go build` (or use `make build`)",
})
} else {
results = append(results, CheckResult{
Name: "Embedded baseline",
Status: CheckOK,
Message: fmt.Sprintf("v%s (%d bytes)", c.Version, len(embeddedJSON)),
})
}

Copilot uses AI. Check for mistakes.
@kevinelliott kevinelliott merged commit b92836f into main Apr 21, 2026
19 checks passed
@kevinelliott kevinelliott deleted the feat/doctor-catalog-sources branch April 21, 2026 13:38
kevinelliott added a commit that referenced this pull request Apr 24, 2026
Toolchain + dependency refresh. Minimum Go bumped to 1.25 (forced by
x/sync v0.20.0), golangci-lint migrated to v2.11.4, 5 deps bumped
plus 3 patch-level ones from v1.2.0-era. Also captures post-v1.2.0
polish: TUI storage reuse perf win (#33), doctor catalog-source
breakdown (#32), catalog ctx-scoped logger (#31), README refresh (#30).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants