agents-inc 0.170.1 → 0.170.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/{chunk-BMRUNMPV.js → chunk-2YA4DDRM.js} +35 -35
  3. package/dist/chunk-2YA4DDRM.js.map +1 -0
  4. package/dist/{chunk-44TE3RKH.js → chunk-3ED3KHHD.js} +5 -4
  5. package/dist/chunk-3ED3KHHD.js.map +1 -0
  6. package/dist/{chunk-3G7X2WKI.js → chunk-5BNX73OU.js} +18 -38
  7. package/dist/chunk-5BNX73OU.js.map +1 -0
  8. package/dist/{chunk-7RDMTR64.js → chunk-66AEVERM.js} +785 -256
  9. package/dist/chunk-66AEVERM.js.map +1 -0
  10. package/dist/{chunk-PJWGTNOX.js → chunk-7C3PFRBU.js} +144 -186
  11. package/dist/chunk-7C3PFRBU.js.map +1 -0
  12. package/dist/{chunk-52HDHJ7I.js → chunk-AUNRHWH4.js} +32 -21
  13. package/dist/chunk-AUNRHWH4.js.map +1 -0
  14. package/dist/{chunk-KOLDW6SF.js → chunk-BBDGYJHX.js} +2 -2
  15. package/dist/chunk-BBDGYJHX.js.map +1 -0
  16. package/dist/{chunk-SZYQISA2.js → chunk-ELV5WDS3.js} +43 -29
  17. package/dist/chunk-ELV5WDS3.js.map +1 -0
  18. package/dist/{chunk-MQ6CGZNJ.js → chunk-GAKVZNTX.js} +1879 -2019
  19. package/dist/chunk-GAKVZNTX.js.map +1 -0
  20. package/dist/chunk-GEMW5RQ2.js +1169 -0
  21. package/dist/chunk-GEMW5RQ2.js.map +1 -0
  22. package/dist/{chunk-NQW25DCF.js → chunk-GHHJYNAN.js} +1 -7
  23. package/dist/chunk-GHHJYNAN.js.map +1 -0
  24. package/dist/{chunk-WTWMGH2C.js → chunk-HACRR422.js} +2 -2
  25. package/dist/chunk-HACRR422.js.map +1 -0
  26. package/dist/{chunk-F2BHT5C4.js → chunk-IWY5IR4F.js} +129 -151
  27. package/dist/chunk-IWY5IR4F.js.map +1 -0
  28. package/dist/{chunk-JFLVEFFP.js → chunk-KIK5EZYD.js} +1 -1
  29. package/dist/chunk-KIK5EZYD.js.map +1 -0
  30. package/dist/{chunk-USVBYQSQ.js → chunk-O4LN3HFI.js} +16 -6
  31. package/dist/chunk-O4LN3HFI.js.map +1 -0
  32. package/dist/{chunk-RIPSGNLU.js → chunk-OCAVW6N5.js} +3 -3
  33. package/dist/chunk-OCAVW6N5.js.map +1 -0
  34. package/dist/{chunk-4AAOOAIN.js → chunk-P6CYS4JK.js} +2 -2
  35. package/dist/chunk-P6CYS4JK.js.map +1 -0
  36. package/dist/chunk-PLFANHRZ.js +35 -0
  37. package/dist/chunk-PLFANHRZ.js.map +1 -0
  38. package/dist/{chunk-6ZRH5RHC.js → chunk-PMUZR4XO.js} +4 -4
  39. package/dist/chunk-PMUZR4XO.js.map +1 -0
  40. package/dist/{chunk-F5EBEATU.js → chunk-PSUTNOQ2.js} +34 -46
  41. package/dist/chunk-PSUTNOQ2.js.map +1 -0
  42. package/dist/chunk-RIK45Q4I.js +103 -0
  43. package/dist/chunk-RIK45Q4I.js.map +1 -0
  44. package/dist/{chunk-HED2DAXZ.js → chunk-THO7LO5B.js} +5 -5
  45. package/dist/chunk-THO7LO5B.js.map +1 -0
  46. package/dist/{chunk-OYJQUA3D.js → chunk-YGDWHDOY.js} +31 -13
  47. package/dist/chunk-YGDWHDOY.js.map +1 -0
  48. package/dist/{chunk-NCWDK6L7.js → chunk-YXKTY5WM.js} +254 -379
  49. package/dist/chunk-YXKTY5WM.js.map +1 -0
  50. package/dist/{chunk-NEZ4OAOL.js → chunk-ZEGM3MVR.js} +3 -5
  51. package/dist/chunk-ZEGM3MVR.js.map +1 -0
  52. package/dist/commands/build/marketplace.js +46 -46
  53. package/dist/commands/build/marketplace.js.map +1 -1
  54. package/dist/commands/build/plugins.js +25 -28
  55. package/dist/commands/build/plugins.js.map +1 -1
  56. package/dist/commands/compile.js +87 -178
  57. package/dist/commands/compile.js.map +1 -1
  58. package/dist/commands/doctor.js +510 -360
  59. package/dist/commands/doctor.js.map +1 -1
  60. package/dist/commands/edit.js +451 -357
  61. package/dist/commands/edit.js.map +1 -1
  62. package/dist/commands/eject.js +144 -125
  63. package/dist/commands/eject.js.map +1 -1
  64. package/dist/commands/init.js +20 -27
  65. package/dist/commands/list.js +39 -55
  66. package/dist/commands/list.js.map +1 -1
  67. package/dist/commands/new/marketplace.js +6 -11
  68. package/dist/commands/new/marketplace.js.map +1 -1
  69. package/dist/commands/search.js +35 -32
  70. package/dist/commands/search.js.map +1 -1
  71. package/dist/commands/share.js +17 -77
  72. package/dist/commands/share.js.map +1 -1
  73. package/dist/commands/uninstall.js +159 -222
  74. package/dist/commands/uninstall.js.map +1 -1
  75. package/dist/commands/update.js +9 -32
  76. package/dist/commands/update.js.map +1 -1
  77. package/dist/components/common/prompt-confirm.js +2 -2
  78. package/dist/components/common/removal-plan-confirm.js +2 -2
  79. package/dist/components/common/scroll-affordance.js +2 -2
  80. package/dist/components/common/select-list.js +2 -2
  81. package/dist/components/common/skill-agent-summary.js +4 -4
  82. package/dist/components/common/spinner.js +2 -4
  83. package/dist/components/common/summary-panel.js +5 -5
  84. package/dist/components/common/summary-panel.js.map +1 -1
  85. package/dist/config-exports.js +7 -3
  86. package/dist/config-exports.js.map +1 -1
  87. package/dist/hooks/init.js +18 -19
  88. package/dist/hooks/init.js.map +1 -1
  89. package/dist/index.js.map +1 -1
  90. package/dist/load-agent-defs-6QUUMGHK.js +14 -0
  91. package/dist/recompile-project-agents-I33KQJFB.js +65 -0
  92. package/dist/recompile-project-agents-I33KQJFB.js.map +1 -0
  93. package/dist/{source-loader-C5SQXZXI.js → source-loader-TTJVW2ER.js} +5 -5
  94. package/dist/src/agents/developer/ai-developer/playbook.md +2 -5
  95. package/dist/src/agents/developer/api-developer/playbook.md +2 -5
  96. package/dist/src/agents/developer/cli-developer/playbook.md +2 -5
  97. package/dist/src/agents/developer/web-developer/playbook.md +2 -5
  98. package/dist/src/agents/meta/agent-summoner/output.md +2 -2
  99. package/dist/src/agents/meta/agent-summoner/playbook.md +1 -13
  100. package/dist/src/agents/meta/codex-keeper/critical-requirements.md +2 -5
  101. package/dist/src/agents/meta/convention-keeper/critical-requirements.md +2 -4
  102. package/dist/src/agents/tester/ai-tester/critical-requirements.md +1 -2
  103. package/dist/src/agents/tester/ai-tester/playbook.md +2 -0
  104. package/dist/src/agents/tester/api-tester/critical-requirements.md +1 -2
  105. package/dist/src/agents/tester/api-tester/playbook.md +2 -0
  106. package/dist/src/agents/tester/cli-tester/critical-requirements.md +1 -2
  107. package/dist/src/agents/tester/cli-tester/playbook.md +2 -0
  108. package/dist/src/agents/tester/web-tester/critical-requirements.md +1 -2
  109. package/dist/src/agents/tester/web-tester/playbook.md +2 -0
  110. package/package.json +9 -8
  111. package/src/agents/developer/ai-developer/playbook.md +2 -5
  112. package/src/agents/developer/api-developer/playbook.md +2 -5
  113. package/src/agents/developer/cli-developer/playbook.md +2 -5
  114. package/src/agents/developer/web-developer/playbook.md +2 -5
  115. package/src/agents/meta/agent-summoner/output.md +2 -2
  116. package/src/agents/meta/agent-summoner/playbook.md +1 -13
  117. package/src/agents/meta/codex-keeper/critical-requirements.md +2 -5
  118. package/src/agents/meta/convention-keeper/critical-requirements.md +2 -4
  119. package/src/agents/tester/ai-tester/critical-requirements.md +1 -2
  120. package/src/agents/tester/ai-tester/playbook.md +2 -0
  121. package/src/agents/tester/api-tester/critical-requirements.md +1 -2
  122. package/src/agents/tester/api-tester/playbook.md +2 -0
  123. package/src/agents/tester/cli-tester/critical-requirements.md +1 -2
  124. package/src/agents/tester/cli-tester/playbook.md +2 -0
  125. package/src/agents/tester/web-tester/critical-requirements.md +1 -2
  126. package/src/agents/tester/web-tester/playbook.md +2 -0
  127. package/src/schemas/project-config.schema.json +0 -3
  128. package/src/schemas/project-source-config.schema.json +0 -6
  129. package/dist/chunk-3G7X2WKI.js.map +0 -1
  130. package/dist/chunk-44TE3RKH.js.map +0 -1
  131. package/dist/chunk-4AAOOAIN.js.map +0 -1
  132. package/dist/chunk-52HDHJ7I.js.map +0 -1
  133. package/dist/chunk-6HNXYYVF.js +0 -373
  134. package/dist/chunk-6HNXYYVF.js.map +0 -1
  135. package/dist/chunk-6ZRH5RHC.js.map +0 -1
  136. package/dist/chunk-7RDMTR64.js.map +0 -1
  137. package/dist/chunk-BMRUNMPV.js.map +0 -1
  138. package/dist/chunk-CLPUWOSA.js +0 -46
  139. package/dist/chunk-CLPUWOSA.js.map +0 -1
  140. package/dist/chunk-F2BHT5C4.js.map +0 -1
  141. package/dist/chunk-F5EBEATU.js.map +0 -1
  142. package/dist/chunk-HED2DAXZ.js.map +0 -1
  143. package/dist/chunk-JFLVEFFP.js.map +0 -1
  144. package/dist/chunk-KOLDW6SF.js.map +0 -1
  145. package/dist/chunk-MQ6CGZNJ.js.map +0 -1
  146. package/dist/chunk-NCWDK6L7.js.map +0 -1
  147. package/dist/chunk-NEZ4OAOL.js.map +0 -1
  148. package/dist/chunk-NIHUOWE4.js +0 -106
  149. package/dist/chunk-NIHUOWE4.js.map +0 -1
  150. package/dist/chunk-NQW25DCF.js.map +0 -1
  151. package/dist/chunk-OYJQUA3D.js.map +0 -1
  152. package/dist/chunk-PJWGTNOX.js.map +0 -1
  153. package/dist/chunk-RIPSGNLU.js.map +0 -1
  154. package/dist/chunk-SZYQISA2.js.map +0 -1
  155. package/dist/chunk-USVBYQSQ.js.map +0 -1
  156. package/dist/chunk-V3KTSP3Q.js +0 -940
  157. package/dist/chunk-V3KTSP3Q.js.map +0 -1
  158. package/dist/chunk-WTWMGH2C.js.map +0 -1
  159. package/dist/load-agent-defs-YMDWBFDP.js +0 -14
  160. package/dist/recompile-project-agents-SZ76WNTW.js +0 -18
  161. package/dist/source-loader-C5SQXZXI.js.map +0 -1
  162. /package/dist/{load-agent-defs-YMDWBFDP.js.map → load-agent-defs-6QUUMGHK.js.map} +0 -0
  163. /package/dist/{recompile-project-agents-SZ76WNTW.js.map → source-loader-TTJVW2ER.js.map} +0 -0
@@ -1,51 +1,45 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- unofferablePlacementsFound
4
- } from "../chunk-WTWMGH2C.js";
5
- import {
6
- detectProject,
7
- loadSource
8
- } from "../chunk-7RDMTR64.js";
9
- import "../chunk-F2BHT5C4.js";
10
- import "../chunk-HED2DAXZ.js";
11
- import "../chunk-3G7X2WKI.js";
2
+ import "../chunk-5BNX73OU.js";
12
3
  import {
13
4
  filterExcludedEntries,
14
5
  listAgentFilesOf,
15
6
  listAgentMdFiles,
16
7
  splitAgentsByProvenance
17
- } from "../chunk-BMRUNMPV.js";
8
+ } from "../chunk-2YA4DDRM.js";
18
9
  import {
19
10
  ambiguityFinding
20
- } from "../chunk-RIPSGNLU.js";
11
+ } from "../chunk-OCAVW6N5.js";
21
12
  import {
22
13
  BaseCommand,
23
14
  EXIT_CODES
24
- } from "../chunk-V3KTSP3Q.js";
25
- import "../chunk-CLPUWOSA.js";
15
+ } from "../chunk-GEMW5RQ2.js";
26
16
  import {
27
17
  ConfigDefaultExportError,
18
+ ConfigLoadError,
28
19
  MarketplaceManifestRequiredError,
29
20
  MarketplaceNameRefusedError,
21
+ MarketplaceRefusedError,
30
22
  SOURCE_ENV_VAR,
31
23
  agentCodec,
32
24
  agentYamlGenerationSchema,
25
+ agentsDir,
33
26
  configDirsInPlay,
34
27
  declaresNoContent,
28
+ detectInstallations,
35
29
  directoryExists,
36
- discoverLocalSkills,
37
30
  fileExists,
38
31
  findConfigLoadFailures,
39
32
  formatZodErrors,
40
33
  getErrorMessage,
41
34
  getInstalledConfigPath,
42
35
  getInstalledPluginsRegistryPath,
36
+ getPluginManifestPath,
43
37
  getProjectPluginsDir,
44
38
  getStackSkillIds,
45
39
  getUserPluginsDir,
46
- getVerifiedPluginInstallPaths,
47
40
  glob,
48
41
  hostCompilesAgent,
42
+ hostFor,
49
43
  installBaseDir,
50
44
  invalidResult,
51
45
  isHomeDirectory,
@@ -53,9 +47,11 @@ import {
53
47
  isRecord,
54
48
  layoutIsCurrent,
55
49
  listDirectories,
50
+ listPluginDirNames,
56
51
  listRegisteredPluginInstalls,
57
52
  loadConfig,
58
53
  loadInstalledConfig,
54
+ loadProjectConfigFromDir,
59
55
  loadSkillsMatrixFromSource,
60
56
  loadSourceRepoConfig,
61
57
  matrixHealthIssues,
@@ -63,17 +59,19 @@ import {
63
59
  metadataValidationSchema,
64
60
  parseFrontmatter,
65
61
  parseMarketplacePluginRef,
66
- plural,
62
+ providerArgument,
67
63
  providerInUse,
68
64
  readFile,
69
65
  readForkedFromMetadata,
70
66
  readSkillMetadata,
71
67
  recreateConfigFrom,
68
+ refuseUnofferedPlacements,
72
69
  relativeConfigPath,
73
70
  resolveInstallPaths,
74
71
  resolvePrimarySourceEntry,
75
72
  scopeLabel,
76
73
  scopeNoun,
74
+ scopeRootsInPlay,
77
75
  skillCategoriesFileSchema,
78
76
  skillRulesFileSchema,
79
77
  skillsDir,
@@ -82,17 +80,20 @@ import {
82
80
  splitMetadataValidationIssues,
83
81
  stackConfigValidationSchema,
84
82
  stacksConfigSchema,
83
+ typedValues,
85
84
  validResult,
86
85
  validateAgentFrontmatter,
87
86
  validateAllPlugins,
88
87
  validatePlugin,
89
88
  validateProjectConfig,
90
89
  validateSkillFrontmatter,
91
- validateSkillMetadata
92
- } from "../chunk-MQ6CGZNJ.js";
90
+ validateSkillMetadata,
91
+ verbose
92
+ } from "../chunk-GAKVZNTX.js";
93
93
  import {
94
- matrix
95
- } from "../chunk-52HDHJ7I.js";
94
+ matrix,
95
+ plural
96
+ } from "../chunk-AUNRHWH4.js";
96
97
  import {
97
98
  CLI_INVOKE_COMMAND,
98
99
  DEFAULT_BRANDING,
@@ -107,12 +108,106 @@ import {
107
108
  STANDARD_FILES,
108
109
  UI_SYMBOLS,
109
110
  effectivelyExcludedSkillIds
110
- } from "../chunk-PJWGTNOX.js";
111
- import "../chunk-NQW25DCF.js";
111
+ } from "../chunk-7C3PFRBU.js";
112
+ import "../chunk-GHHJYNAN.js";
112
113
 
113
114
  // src/cli/commands/doctor.ts
114
- import os2 from "os";
115
+ import os3 from "os";
116
+ import path4 from "path";
117
+
118
+ // src/cli/lib/doctor/installation-reads.ts
119
+ import os from "os";
120
+ function hostsOnDisk(projectDir) {
121
+ return { project: providerInUse(projectDir), global: providerInUse(os.homedir()) };
122
+ }
123
+ function hostsOf(provider) {
124
+ return { project: provider, global: provider };
125
+ }
126
+ async function providersInPlay(projectDir) {
127
+ const found = await detectInstallations(projectDir);
128
+ const inTheProject = found.filter((installation) => installation.scope === "project");
129
+ const inPlay = inTheProject.length > 0 ? inTheProject : found;
130
+ return inPlay.map((installation) => installation.provider);
131
+ }
132
+ async function resolveConfigState({
133
+ projectDir,
134
+ hosts
135
+ }) {
136
+ try {
137
+ return await readConfigState(projectDir, hosts);
138
+ } catch (error) {
139
+ if (error instanceof ConfigLoadError) return { kind: "unreadable", failure: error };
140
+ throw error;
141
+ }
142
+ }
143
+ async function readConfigState(projectDir, hosts) {
144
+ const own = await loadProjectConfigFromDir(projectDir, hosts.project);
145
+ if (own && !declaresNoContent(own.config)) {
146
+ return { kind: "loaded", config: own.config, inherited: false };
147
+ }
148
+ const inherited = isHomeDirectory(projectDir) ? null : await loadProjectConfigFromDir(os.homedir(), hosts.global);
149
+ if (inherited && !declaresNoContent(inherited.config)) {
150
+ return { kind: "loaded", config: inherited.config, inherited: true };
151
+ }
152
+ return own ? { kind: "declares-nothing", config: own.config } : { kind: "absent" };
153
+ }
154
+ async function installedPluginSkillIds(baseDir, provider) {
155
+ const plugins = await verifiedPlugins(baseDir, provider);
156
+ return new Set(plugins.map((plugin) => parseMarketplacePluginRef(plugin.pluginKey)));
157
+ }
158
+ async function verifiedPlugins(baseDir, provider) {
159
+ const enabled = (await pluginsListedBy(provider, baseDir)).filter((plugin) => plugin.enabled);
160
+ const onDisk = await Promise.all(
161
+ enabled.map((plugin) => fileExists(getPluginManifestPath(plugin.installPath)))
162
+ );
163
+ return enabled.filter((_, index) => onDisk[index] === true);
164
+ }
165
+ async function pluginsListedBy(provider, baseDir) {
166
+ try {
167
+ return await hostFor(provider).listPlugins(baseDir);
168
+ } catch (error) {
169
+ verbose(`Failed to list installed plugins: ${getErrorMessage(error)}`);
170
+ return [];
171
+ }
172
+ }
173
+ async function unofferablePlacements({
174
+ projectDir,
175
+ hosts
176
+ }) {
177
+ const projectFirst = [...scopeRootsInPlay(projectDir)].reverse();
178
+ const findings = await Promise.all(
179
+ projectFirst.map(({ kind, root }) => unofferablePlacementIn(root, hosts[kind]))
180
+ );
181
+ return findings.filter((finding) => finding !== null);
182
+ }
183
+ async function unofferablePlacementIn(root, provider) {
184
+ const skills = await configuredSkillsAt(root, provider);
185
+ if (skills === null) return null;
186
+ try {
187
+ refuseUnofferedPlacements(skills, hostFor(provider));
188
+ return null;
189
+ } catch (error) {
190
+ return getErrorMessage(error);
191
+ }
192
+ }
193
+ async function configuredSkillsAt(root, provider) {
194
+ try {
195
+ const loaded = await loadProjectConfigFromDir(root, provider);
196
+ return loaded?.config.skills ?? null;
197
+ } catch (error) {
198
+ if (error instanceof ConfigLoadError) return null;
199
+ throw error;
200
+ }
201
+ }
202
+
203
+ // src/cli/lib/doctor/ownership.ts
115
204
  import path3 from "path";
205
+ import { groupBy } from "remeda";
206
+
207
+ // src/cli/lib/content-validator.ts
208
+ import os2 from "os";
209
+ import path2 from "path";
210
+ import { partition } from "remeda";
116
211
 
117
212
  // src/cli/lib/source-validator.ts
118
213
  import path from "path";
@@ -199,12 +294,7 @@ async function validateSource(sourcePath, reader = "author") {
199
294
  0
200
295
  );
201
296
  }
202
- const skillMdFiles = await glob(`**/${STANDARD_FILES.SKILL_MD}`, skillsDir2);
203
- const metadataFiles = await glob(`**/${STANDARD_FILES.METADATA_YAML}`, skillsDir2);
204
- const skillMdDirs = new Set(skillMdFiles.map((f) => path.dirname(f)));
205
- const metadataDirs = new Set(metadataFiles.map((f) => path.dirname(f)));
206
- const pairIssues = validateSkillFilePairs(skillMdDirs, metadataDirs, skillsDir2);
207
- const validMetadataFiles = metadataFiles.filter((f) => skillMdDirs.has(path.dirname(f)));
297
+ const { pairIssues, validMetadataFiles } = await pairSkillFiles(skillsDir2);
208
298
  const skillIssues = await validateSkillsInTurn(skillsDir2, skillsDirRelPath, validMetadataFiles);
209
299
  const crossReferenceIssues = await checkCrossReferences(resolvedPath, reader);
210
300
  const extraIssues = await Promise.all([
@@ -217,6 +307,16 @@ async function validateSource(sourcePath, reader = "author") {
217
307
  validMetadataFiles.length
218
308
  );
219
309
  }
310
+ async function pairSkillFiles(skillsDir2) {
311
+ const skillMdFiles = await glob(`**/${STANDARD_FILES.SKILL_MD}`, skillsDir2);
312
+ const metadataFiles = await glob(`**/${STANDARD_FILES.METADATA_YAML}`, skillsDir2);
313
+ const skillMdDirs = new Set(skillMdFiles.map((f) => path.dirname(f)));
314
+ const metadataDirs = new Set(metadataFiles.map((f) => path.dirname(f)));
315
+ return {
316
+ pairIssues: validateSkillFilePairs(skillMdDirs, metadataDirs, skillsDir2),
317
+ validMetadataFiles: metadataFiles.filter((f) => skillMdDirs.has(path.dirname(f)))
318
+ };
319
+ }
220
320
  async function validateSkillsInTurn(skillsDir2, skillsDirRelPath, metadataFiles) {
221
321
  const issues = [];
222
322
  for (const metadataFile of metadataFiles) {
@@ -343,10 +443,10 @@ async function validateStacks(resolvedPath) {
343
443
  return [...skillMetaIssues, ...configIssues];
344
444
  }
345
445
  async function validateAgents(resolvedPath) {
346
- const agentsDir = path.join(resolvedPath, DIRS.agents);
347
- if (!await directoryExists(agentsDir)) return [];
446
+ const agentsDir2 = path.join(resolvedPath, DIRS.agents);
447
+ if (!await directoryExists(agentsDir2)) return [];
348
448
  return validateYamlFiles({
349
- baseDir: agentsDir,
449
+ baseDir: agentsDir2,
350
450
  relBaseDir: DIRS.agents,
351
451
  pattern: `**/${STANDARD_FILES.AGENT_METADATA_YAML}`,
352
452
  schema: agentYamlGenerationSchema
@@ -414,11 +514,7 @@ function buildResult(issues, skillCount) {
414
514
  }
415
515
 
416
516
  // src/cli/lib/content-validator.ts
417
- import os from "os";
418
- import path2 from "path";
419
- import { partition } from "remeda";
420
517
  var CWD_SOURCE_NAME = "current directory";
421
- var AGENT_FILE_EXTENSION = ".md";
422
518
  var NOTHING_VALIDATED = { count: 0, issues: [], notes: [] };
423
519
  function mergeValidations(parts) {
424
520
  return parts.reduce(
@@ -431,7 +527,7 @@ function mergeValidations(parts) {
431
527
  );
432
528
  }
433
529
  function displayDir(absolutePath) {
434
- const home = os.homedir();
530
+ const home = os2.homedir();
435
531
  if (absolutePath === home) return "~";
436
532
  if (absolutePath.startsWith(home + path2.sep)) {
437
533
  return `~${path2.sep}${path2.relative(home, absolutePath)}`;
@@ -519,7 +615,7 @@ async function validatePluginsDirectory(pluginsDir) {
519
615
  const registryValidation = await validateRegistryPlugins(pluginsDir);
520
616
  if (registryValidation !== void 0) return registryValidation;
521
617
  }
522
- const pluginDirs = await findPluginDirectories(pluginsDir);
618
+ const pluginDirs = await listPluginDirNames(pluginsDir);
523
619
  if (pluginDirs.length === 0) return NOTHING_VALIDATED;
524
620
  const { results } = await validateAllPlugins(pluginsDir);
525
621
  return {
@@ -553,16 +649,6 @@ async function validateRegistryPlugins(pluginsDir) {
553
649
  );
554
650
  return { count: installs.length, issues: results.flat(), notes: [] };
555
651
  }
556
- async function findPluginDirectories(pluginsDir) {
557
- const entries = await listDirectories(pluginsDir);
558
- const checks = await Promise.all(
559
- entries.map(async (name) => ({
560
- name,
561
- isPlugin: await directoryExists(path2.join(pluginsDir, name, PLUGIN_MANIFEST_DIR))
562
- }))
563
- );
564
- return checks.filter((check) => check.isPlugin).map((check) => check.name);
565
- }
566
652
  async function validateInstalledSkills(projectDir) {
567
653
  const configuredIds = await configuredSkillIds(projectDir);
568
654
  return mergeValidations(
@@ -593,27 +679,26 @@ function installedDirs(projectDir, key) {
593
679
  if (isHomeDirectory(projectDir)) return [global];
594
680
  return [global, resolveInstallPaths(projectDir, "project")[key]];
595
681
  }
596
- async function listInstalledArtifacts(projectDir) {
597
- const [skills, agents] = await Promise.all([
598
- listAcrossScopes(installedDirs(projectDir, "skillsDir"), listSkillDirsWithOurProvenance),
599
- listAcrossScopes(installedDirs(projectDir, "agentsDir"), listAgentFilesWithOurProvenance)
600
- ]);
601
- return { skills, agents };
682
+ async function skillDirsWithOurProvenanceIn(skillsDir2) {
683
+ return listDisplayPaths(skillsDir2, listSkillDirsWithOurProvenance);
684
+ }
685
+ async function agentFilesWithOurProvenanceIn(agentsDir2, codec) {
686
+ return listDisplayPaths(agentsDir2, (dir) => listAgentFilesWithOurProvenance(dir, codec));
602
687
  }
603
- async function listAgentFilesWithOurProvenance(agentsDir) {
604
- const { marked } = await splitAgentsByProvenance(agentsDir);
605
- return marked.map((name) => `${name}${AGENT_FILE_EXTENSION}`);
688
+ async function listAgentFilesWithOurProvenance(agentsDir2, codec) {
689
+ const { marked } = await splitAgentsByProvenance(agentsDir2, codec);
690
+ return marked.map((name) => `${name}${codec.extension}`);
606
691
  }
607
692
  async function listSkillDirsWithOurProvenance(skillsDir2) {
608
- const names = await listDirectories(skillsDir2);
609
- const judged = await Promise.all(
610
- names.map(async (name) => ({ name, ours: await carriesOurProvenance(skillsDir2, name) }))
693
+ const [ours] = await partitionByVerdict(
694
+ await listDirectories(skillsDir2),
695
+ (name) => carriesOurProvenance(skillsDir2, name)
611
696
  );
612
- return judged.filter((entry) => entry.ours).map((entry) => entry.name);
697
+ return ours;
613
698
  }
614
- async function listAcrossScopes(dirs, listEntries) {
615
- const perDir = await Promise.all(dirs.map((dir) => listDisplayPaths(dir, listEntries)));
616
- return perDir.flat();
699
+ async function partitionByVerdict(names, verdict) {
700
+ const verdicts = await Promise.all(names.map(verdict));
701
+ return partition(names, (_, index) => verdicts[index] === true);
617
702
  }
618
703
  async function listDisplayPaths(dir, listEntries) {
619
704
  if (!await directoryExists(dir)) return [];
@@ -624,15 +709,12 @@ async function validateSkillsDirectory(skillsDir2, configuredIds) {
624
709
  if (!await directoryExists(skillsDir2)) return NOTHING_VALIDATED;
625
710
  const skillDirs = await listDirectories(skillsDir2);
626
711
  if (skillDirs.length === 0) return NOTHING_VALIDATED;
627
- const judged = await Promise.all(
628
- skillDirs.map(async (name) => ({
629
- name,
630
- ours: await isOurSkillDirectory(skillsDir2, name, configuredIds)
631
- }))
712
+ const [ours, foreign] = await partitionByVerdict(
713
+ skillDirs,
714
+ (name) => isOurSkillDirectory(skillsDir2, name, configuredIds)
632
715
  );
633
- const [ours, foreign] = partition(judged, (entry) => entry.ours);
634
716
  const results = await Promise.all(
635
- ours.map(async ({ name }) => {
717
+ ours.map(async (name) => {
636
718
  const skillDir = path2.join(skillsDir2, name);
637
719
  return toIssues(displayDir(skillDir), await validateInstalledSkill(skillDir));
638
720
  })
@@ -640,7 +722,7 @@ async function validateSkillsDirectory(skillsDir2, configuredIds) {
640
722
  return {
641
723
  count: ours.length,
642
724
  issues: results.flat(),
643
- notes: foreign.map(({ name }) => foreignSkillNote(skillsDir2, name))
725
+ notes: foreign.map((name) => foreignSkillNote(skillsDir2, name))
644
726
  };
645
727
  }
646
728
  async function isOurSkillDirectory(skillsDir2, name, configuredIds) {
@@ -653,29 +735,23 @@ async function carriesOurProvenance(skillsDir2, name) {
653
735
  function foreignSkillNote(skillsDir2, name) {
654
736
  return `${displayDir(path2.join(skillsDir2, name))} \u2014 not installed by this CLI and named by no configuration here: not validated`;
655
737
  }
656
- async function validateAgentsDirectory(agentsDir) {
657
- if (!await directoryExists(agentsDir)) return NOTHING_VALIDATED;
658
- const agentFiles = await listAgentMdFiles(agentsDir);
738
+ async function validateAgentsDirectory(agentsDir2) {
739
+ if (!await directoryExists(agentsDir2)) return NOTHING_VALIDATED;
740
+ const agentFiles = await listAgentMdFiles(agentsDir2);
659
741
  if (agentFiles.length === 0) return NOTHING_VALIDATED;
660
742
  const results = await Promise.all(
661
743
  agentFiles.map(async (fileName) => {
662
- const agentPath = path2.join(agentsDir, fileName);
744
+ const agentPath = path2.join(agentsDir2, fileName);
663
745
  return toIssues(displayDir(agentPath), await validateAgentFrontmatter(agentPath));
664
746
  })
665
747
  );
666
748
  return { count: agentFiles.length, issues: results.flat(), notes: [] };
667
749
  }
668
750
  async function validateInstalledSkill(skillDir) {
669
- const skillMd = await validateSkillFile(
670
- skillDir,
671
- STANDARD_FILES.SKILL_MD,
672
- validateSkillFrontmatter
673
- );
674
- const metadata = await validateSkillFile(
675
- skillDir,
676
- STANDARD_FILES.METADATA_YAML,
677
- validateInstalledSkillMetadata
678
- );
751
+ const [skillMd, metadata] = await Promise.all([
752
+ validateSkillFile(skillDir, STANDARD_FILES.SKILL_MD, validateSkillFrontmatter),
753
+ validateSkillFile(skillDir, STANDARD_FILES.METADATA_YAML, validateInstalledSkillMetadata)
754
+ ]);
679
755
  return mergeValidationResults([skillMd, metadata]);
680
756
  }
681
757
  async function validateSkillFile(skillDir, fileName, validate) {
@@ -701,25 +777,100 @@ async function validateInstalledSkillMetadata(metadataPath) {
701
777
  };
702
778
  }
703
779
 
704
- // src/cli/commands/doctor.ts
705
- import { countBy, unique } from "remeda";
706
- async function resolveConfigState(detected, projectDir) {
707
- if (detected?.config) {
708
- return {
709
- kind: "loaded",
710
- config: detected.config,
711
- inherited: inheritsTheGlobalConfig(detected.installation.projectDir, projectDir)
712
- };
713
- }
714
- const loaded = await loadInstalledConfig(projectDir);
715
- if (!loaded) return { kind: "absent" };
716
- return declaresNoContent(loaded.config) ? { kind: "declares-nothing", config: loaded.config } : { kind: "loaded", config: loaded.config, inherited: false };
780
+ // src/cli/lib/doctor/ownership.ts
781
+ async function judgeOwnership(state, config, installation) {
782
+ const agentFolders = agentFoldersToJudge(state, config, installation);
783
+ const skillFolders = skillFoldersToJudge(state, config, installation);
784
+ const [unowned, orphans] = await Promise.all([
785
+ unownedIn(agentFolders, skillFolders),
786
+ orphansIn(agentFolders, skillFolders)
787
+ ]);
788
+ return { unowned, orphans };
789
+ }
790
+ function agentFoldersToJudge(state, config, { projectDir, hosts }) {
791
+ return oncePerFolder(
792
+ scopesToJudge(state, hosts).map((scope) => ({
793
+ dir: agentsDir(hosts[scope], scope, projectDir),
794
+ codec: agentCodec(hosts[scope]),
795
+ claimed: claimed(state, scope, config && agentNamesClaimedIn(config, scope))
796
+ }))
797
+ );
798
+ }
799
+ function skillFoldersToJudge(state, config, { projectDir, hosts }) {
800
+ return oncePerFolder(
801
+ scopesToJudge(state, hosts).map((scope) => ({
802
+ dir: skillsDir(hosts[scope], scope, projectDir),
803
+ claimed: claimed(state, scope, config && skillIdsClaimedIn(config, scope))
804
+ }))
805
+ );
717
806
  }
718
- function inheritsTheGlobalConfig(readFrom, projectDir) {
719
- return isHomeDirectory(readFrom) && !isHomeDirectory(projectDir);
807
+ function scopesToJudge(state, hosts) {
808
+ const ownConfig = state.kind !== "absent" && !inherits(state);
809
+ return ownConfig && hosts.global !== hosts.project ? ["project"] : ["project", "global"];
720
810
  }
721
- function checkConfigValid(state, projectDir) {
722
- const theConfig = (verdict) => configRowMessage(state, projectDir, verdict);
811
+ function inherits(state) {
812
+ return state.kind === "loaded" && state.inherited;
813
+ }
814
+ function claimed(state, scope, names) {
815
+ if (names === null || state.kind === "absent") return null;
816
+ if (inherits(state) && scope === "project") return null;
817
+ return new Set(names);
818
+ }
819
+ function agentNamesClaimedIn(config, scope) {
820
+ return config.agents.filter((agent) => claimsAt(agent, scope)).map((agent) => agent.name);
821
+ }
822
+ function skillIdsClaimedIn(config, scope) {
823
+ return config.skills.filter((skill) => claimsAt(skill, scope)).map((skill) => skill.id);
824
+ }
825
+ function claimsAt(entry, scope) {
826
+ return entry.scope === scope && (scope === "global" || !entry.excluded);
827
+ }
828
+ function oncePerFolder(folders) {
829
+ return typedValues(groupBy(folders, (folder) => folder.dir)).map(
830
+ (sharing) => sharing.reduce(joinClaims)
831
+ );
832
+ }
833
+ function joinClaims(into, from) {
834
+ if (into.claimed === null) return from;
835
+ if (from.claimed === null) return into;
836
+ return { ...into, claimed: /* @__PURE__ */ new Set([...into.claimed, ...from.claimed]) };
837
+ }
838
+ async function unownedIn(agentFolders, skillFolders) {
839
+ const [skills, agents] = await Promise.all([
840
+ Promise.all(
841
+ skillFolders.filter(isUnclaimed).map(({ dir }) => skillDirsWithOurProvenanceIn(dir))
842
+ ),
843
+ Promise.all(
844
+ agentFolders.filter(isUnclaimed).map(({ dir, codec }) => agentFilesWithOurProvenanceIn(dir, codec))
845
+ )
846
+ ]);
847
+ return { skills: skills.flat(), agents: agents.flat() };
848
+ }
849
+ async function orphansIn(agentFolders, skillFolders) {
850
+ const [skills, agents] = await Promise.all([
851
+ Promise.all(skillFolders.map(unclaimedSkillDirs)),
852
+ Promise.all(agentFolders.map(unclaimedAgentFiles))
853
+ ]);
854
+ return { skills: skills.flat(), agents: agents.flat() };
855
+ }
856
+ function isUnclaimed(folder) {
857
+ return folder.claimed === null;
858
+ }
859
+ async function unclaimedSkillDirs({ dir, claimed: claimed2 }) {
860
+ if (claimed2 === null) return [];
861
+ const ours = await listSkillDirsWithOurProvenance(dir);
862
+ return ours.filter((name) => !claimed2.has(name));
863
+ }
864
+ async function unclaimedAgentFiles({ dir, codec, claimed: claimed2 }) {
865
+ if (claimed2 === null || !await directoryExists(dir)) return [];
866
+ const files = await listAgentFilesOf(dir, codec);
867
+ return files.filter((file) => !claimed2.has(path3.basename(file, codec.extension)));
868
+ }
869
+
870
+ // src/cli/commands/doctor.ts
871
+ import { countBy, unique } from "remeda";
872
+ function checkConfigValid(state, installation) {
873
+ const theConfig = (verdict) => configRowMessage(state, installation, verdict);
723
874
  if (state.kind === "absent") {
724
875
  return {
725
876
  result: {
@@ -731,6 +882,17 @@ function checkConfigValid(state, projectDir) {
731
882
  config: null
732
883
  };
733
884
  }
885
+ if (state.kind === "unreadable") {
886
+ return {
887
+ result: {
888
+ kind: "config",
889
+ status: "fail",
890
+ message: theConfig("exists but could not be loaded"),
891
+ details: [state.failure.reason]
892
+ },
893
+ config: null
894
+ };
895
+ }
734
896
  if (state.kind === "declares-nothing") {
735
897
  return {
736
898
  result: {
@@ -774,30 +936,30 @@ function checkConfigValid(state, projectDir) {
774
936
  config
775
937
  };
776
938
  }
777
- function configRowMessage(state, projectDir, verdict) {
939
+ function configRowMessage(state, { projectDir, hosts }, verdict) {
778
940
  if (state.kind === "loaded" && state.inherited)
779
- return `${theInheritedConfig()}, which ${verdict}`;
780
- return `${theOwnConfig(projectDir)} ${verdict}`;
941
+ return `${theInheritedConfig(hosts.global)}, which ${verdict}`;
942
+ if (state.kind === "unreadable") {
943
+ const { scopeRoot, scope } = state.failure;
944
+ return `${theConfigAt(scopeRoot, hosts[scope])} ${verdict}`;
945
+ }
946
+ return `${theConfigAt(projectDir, hosts.project)} ${verdict}`;
781
947
  }
782
- function theOwnConfig(projectDir) {
783
- return `${scopeLabel(scopeOf(projectDir))}: ${configNamedFrom(projectDir)}`;
948
+ function theConfigAt(root, provider) {
949
+ return `${scopeLabel(scopeOf(root))}: ${configNamedFrom(root, provider)}`;
784
950
  }
785
- function theInheritedConfig() {
786
- return `${scopeLabel("project")}: no ${STANDARD_FILES.CONFIG_TS}, using ${scopeNoun("global")}'s (${configNamedFrom(os2.homedir())})`;
951
+ function theInheritedConfig(provider) {
952
+ return `${scopeLabel("project")}: no ${STANDARD_FILES.CONFIG_TS}, using ${scopeNoun("global")}'s (${configNamedFrom(os3.homedir(), provider)})`;
787
953
  }
788
954
  function scopeOf(dir) {
789
955
  return isHomeDirectory(dir) ? "global" : "project";
790
956
  }
791
- function configNamedFrom(root) {
792
- const relPath = relativeConfigPath(root, providerInUse(root));
957
+ function configNamedFrom(root, provider) {
958
+ const relPath = relativeConfigPath(root, provider);
793
959
  return isHomeDirectory(root) ? `~/${relPath}` : relPath;
794
960
  }
795
- async function checkSkillsResolved(config, matrix2, projectDir) {
796
- const excludedIds = effectivelyExcludedSkillIds(config.skills);
797
- const stackIds = config.stack ? getStackSkillIds(config.stack) : [];
798
- const uniqueSkills = unique([...stackIds, ...config.skills.map((s) => s.id)]).filter(
799
- (id) => !excludedIds.has(id)
800
- );
961
+ function checkSkillsResolved(config, matrix2) {
962
+ const uniqueSkills = skillIdsToResolve(config);
801
963
  if (uniqueSkills.length === 0) {
802
964
  return {
803
965
  kind: "skills",
@@ -805,17 +967,7 @@ async function checkSkillsResolved(config, matrix2, projectDir) {
805
967
  message: "No skills configured"
806
968
  };
807
969
  }
808
- const [localResult, globalResult] = await Promise.all([
809
- discoverLocalSkills(projectDir),
810
- !isHomeDirectory(projectDir) ? discoverLocalSkills(os2.homedir()) : null
811
- ]);
812
- const localSkillIds = /* @__PURE__ */ new Set([
813
- ...localResult?.skills.map((s) => s.id) ?? [],
814
- ...globalResult?.skills.map((s) => s.id) ?? []
815
- ]);
816
- const missingSkills = uniqueSkills.filter(
817
- (skillId) => !(skillId in matrix2.skills) && !localSkillIds.has(skillId)
818
- );
970
+ const missingSkills = uniqueSkills.filter((skillId) => !(skillId in matrix2.skills));
819
971
  if (missingSkills.length > 0) {
820
972
  return {
821
973
  kind: "skills",
@@ -830,29 +982,27 @@ async function checkSkillsResolved(config, matrix2, projectDir) {
830
982
  message: `${ratio(uniqueSkills.length, uniqueSkills.length, "skill")} found`
831
983
  };
832
984
  }
833
- async function checkAgentsCompiled(config, projectDir) {
834
- const agents = config.agents;
835
- if (agents.length === 0) {
985
+ function skillIdsToResolve(config) {
986
+ const excludedIds = effectivelyExcludedSkillIds(config.skills);
987
+ const stackIds = config.stack ? getStackSkillIds(config.stack) : [];
988
+ return unique([...stackIds, ...config.skills.map((s) => s.id)]).filter(
989
+ (id) => !excludedIds.has(id)
990
+ );
991
+ }
992
+ async function checkAgentsCompiled(config, installation) {
993
+ if (config.agents.length === 0) {
836
994
  return {
837
995
  kind: "agents",
838
996
  status: "pass",
839
997
  message: "No agents configured"
840
998
  };
841
999
  }
842
- const hosts = {
843
- global: providerInUse(installBaseDir(projectDir, "global")),
844
- project: providerInUse(installBaseDir(projectDir, "project"))
845
- };
846
- const dirs = {
847
- global: resolveInstallPaths(projectDir, "global").agentsDir,
848
- project: resolveInstallPaths(projectDir, "project").agentsDir
849
- };
850
1000
  const agentChecks = await Promise.all(
851
- agents.map((agent) => agentCompileCheck(agent, hosts, dirs))
1001
+ config.agents.map((agent) => agentCompileCheck(agent, installation))
852
1002
  );
853
1003
  const unrenderable = agentChecks.filter((c) => c.state === "unrenderable");
854
1004
  const missingAgents = agentChecks.filter((c) => c.state === "missing").map((c) => c.name);
855
- if (unrenderable.length === agents.length) {
1005
+ if (unrenderable.length === config.agents.length) {
856
1006
  const hostsLeavingThemOut = unique(unrenderable.map((a) => a.provider)).join(" or ");
857
1007
  return {
858
1008
  kind: "agents",
@@ -865,96 +1015,71 @@ async function checkAgentsCompiled(config, projectDir) {
865
1015
  return {
866
1016
  kind: "agents",
867
1017
  status: "warn",
868
- message: `${missingAgents.length} agent${missingAgents.length === 1 ? "" : "s"} need${missingAgents.length === 1 ? "s" : ""} recompilation`,
1018
+ message: needRecompilation(missingAgents.length),
869
1019
  details: missingAgents.map((a) => `- ${a} (missing)`)
870
1020
  };
871
1021
  }
872
- const renderable = agents.length - unrenderable.length;
1022
+ const renderable = config.agents.length - unrenderable.length;
873
1023
  return {
874
1024
  kind: "agents",
875
1025
  status: "pass",
876
1026
  message: `${ratio(renderable, renderable, "agent")} compiled`
877
1027
  };
878
1028
  }
879
- async function agentCompileCheck(agent, hosts, agentsDirs) {
1029
+ function needRecompilation(count) {
1030
+ return `${plural(count, "agent")} ${count === 1 ? "needs" : "need"} recompilation`;
1031
+ }
1032
+ async function agentCompileCheck(agent, { projectDir, hosts }) {
880
1033
  const provider = hosts[agent.scope];
881
1034
  if (!hostCompilesAgent(provider, agent.name)) {
882
1035
  return { name: agent.name, provider, state: "unrenderable" };
883
1036
  }
884
1037
  const file = `${agent.name}${agentCodec(provider).extension}`;
885
- const compiled = await fileExists(path3.join(agentsDirs[agent.scope], file));
1038
+ const compiled = await fileExists(path4.join(agentsDir(provider, agent.scope, projectDir), file));
886
1039
  return { name: agent.name, provider, state: compiled ? "compiled" : "missing" };
887
1040
  }
888
- async function checkNoOrphans(config, projectDir) {
889
- const codec = agentCodec(providerInUse(projectDir));
890
- const projectAgentsDir = resolveInstallPaths(projectDir, "project").agentsDir;
891
- const globalAgentsDir = resolveInstallPaths(projectDir, "global").agentsDir;
892
- const scopesShareAgentsDir = isHomeDirectory(projectDir);
893
- const [projectExists, globalExists] = await Promise.all([
894
- directoryExists(projectAgentsDir),
895
- !scopesShareAgentsDir ? directoryExists(globalAgentsDir) : false
896
- ]);
897
- if (!projectExists && !globalExists) {
1041
+ async function checkNoOrphans(state, config, installation) {
1042
+ return ownershipResult(state, await judgeOwnership(state, config, installation));
1043
+ }
1044
+ function ownershipResult(state, { unowned, orphans }) {
1045
+ if (unowned.skills.length > 0 || unowned.agents.length > 0) {
898
1046
  return {
899
- kind: "orphans",
900
- status: "pass",
901
- message: "No agents directory"
1047
+ kind: "orphans-unowned",
1048
+ status: "fail",
1049
+ message: `${countedArtifacts(unowned.skills, unowned.agents)} installed here, and no configuration declares them`,
1050
+ details: [...unowned.skills, ...unowned.agents, ...orphanLines(orphans)].map(
1051
+ (line) => `- ${line}`
1052
+ )
902
1053
  };
903
1054
  }
904
- const projectMdFiles = projectExists ? await listAgentFilesOf(projectAgentsDir, codec) : [];
905
- const globalMdFiles = globalExists ? await listAgentFilesOf(globalAgentsDir, codec) : [];
906
- const activeProjectAgents = new Set(
907
- config.agents.filter((a) => a.scope === "project" && !a.excluded).map((a) => a.name)
908
- );
909
- const knownGlobalAgents = new Set(
910
- config.agents.filter((a) => a.scope === "global").map((a) => a.name)
911
- );
912
- const knownProjectDirAgents = scopesShareAgentsDir ? /* @__PURE__ */ new Set([...activeProjectAgents, ...knownGlobalAgents]) : activeProjectAgents;
913
- const orphanedFiles = [
914
- ...orphanedAgentNames(projectMdFiles, knownProjectDirAgents, codec),
915
- ...orphanedAgentNames(globalMdFiles, knownGlobalAgents, codec)
916
- ];
917
- if (orphanedFiles.length > 0) {
1055
+ if (orphans.skills.length > 0 || orphans.agents.length > 0) {
918
1056
  return {
919
1057
  kind: "orphans",
920
1058
  status: "warn",
921
- message: plural(orphanedFiles.length, "orphaned agent file"),
922
- details: orphanedFiles.map((f) => `- ${f}.md (not in config)`)
1059
+ message: countedOrphans(orphans),
1060
+ details: orphanLines(orphans).map((line) => `- ${line}`)
923
1061
  };
924
1062
  }
925
- return {
926
- kind: "orphans",
927
- status: "pass",
928
- message: "No orphaned agent files"
929
- };
1063
+ if (state.kind === "absent") return skippedResult("orphans");
1064
+ return { kind: "orphans", status: "pass", message: "No orphaned skills or agent files" };
930
1065
  }
931
- function orphanedAgentNames(mdFiles, knownAgents, codec) {
932
- return mdFiles.map((fileName) => path3.basename(fileName, codec.extension)).filter((agentName) => !knownAgents.has(agentName));
1066
+ function orphanLines({ skills, agents }) {
1067
+ return [...skills, ...agents].map((name) => `${name} (not in config)`);
933
1068
  }
934
- async function checkUnownedInstallation(projectDir) {
935
- const { skills, agents } = await listInstalledArtifacts(projectDir);
936
- if (skills.length === 0 && agents.length === 0) return skippedResult("orphans");
937
- return {
938
- kind: "orphans-unowned",
939
- status: "fail",
940
- // The per-line "(not in config)" the other verdict repeats is said once here instead:
941
- // with no configuration it is true of every line, and there are as many lines as files.
942
- message: `${countedArtifacts(skills, agents)} installed here, and no configuration declares them`,
943
- details: [...skills, ...agents].map((artifact) => `- ${artifact}`)
944
- };
1069
+ function countedOrphans({ skills, agents }) {
1070
+ return nonZeroCounts([skills.length, "orphaned skill"], [agents.length, "orphaned agent file"]);
945
1071
  }
946
1072
  function ratio(found, total, noun) {
947
1073
  return `${found}/${plural(total, noun)}`;
948
1074
  }
949
1075
  function countedArtifacts(skills, agents) {
950
- return [
951
- ...skills.length > 0 ? [plural(skills.length, "skill")] : [],
952
- ...agents.length > 0 ? [plural(agents.length, "agent")] : []
953
- ].join(" and ");
1076
+ return nonZeroCounts([skills.length, "skill"], [agents.length, "agent"]);
954
1077
  }
955
- async function checkSkillsInstalled(config, projectDir) {
956
- const skills = config.skills;
957
- const ejectSkills = skills.filter((s) => s.origin === EJECT_SOURCE);
1078
+ function nonZeroCounts(...counts) {
1079
+ return counts.filter(([count]) => count > 0).map(([count, noun]) => plural(count, noun)).join(" and ");
1080
+ }
1081
+ async function checkSkillsInstalled(config, installation) {
1082
+ const ejectSkills = config.skills.filter((s) => s.origin === EJECT_SOURCE);
958
1083
  if (ejectSkills.length === 0) {
959
1084
  return {
960
1085
  kind: "installed",
@@ -963,7 +1088,7 @@ async function checkSkillsInstalled(config, projectDir) {
963
1088
  };
964
1089
  }
965
1090
  const skillChecks = await Promise.all(
966
- ejectSkills.map((skill) => ejectedSkillCheck(skill, projectDir))
1091
+ ejectSkills.map((skill) => ejectedSkillCheck(skill, installation))
967
1092
  );
968
1093
  const missingSkills = skillChecks.filter((c) => !c.installed);
969
1094
  if (missingSkills.length > 0) {
@@ -980,17 +1105,17 @@ async function checkSkillsInstalled(config, projectDir) {
980
1105
  message: `${ratio(ejectSkills.length, ejectSkills.length, "eject-mode skill")} installed`
981
1106
  };
982
1107
  }
983
- async function ejectedSkillCheck(skill, projectDir) {
984
- const provider = providerInUse(installBaseDir(projectDir, skill.scope));
1108
+ async function ejectedSkillCheck(skill, { projectDir, hosts }) {
1109
+ const provider = hosts[skill.scope];
985
1110
  const dir = skillsDir(provider, skill.scope, projectDir);
986
- const installed = await fileExists(path3.join(dir, skill.id, STANDARD_FILES.SKILL_MD));
1111
+ const installed = await fileExists(path4.join(dir, skill.id, STANDARD_FILES.SKILL_MD));
987
1112
  return {
988
1113
  id: skill.id,
989
1114
  installed,
990
1115
  lookedIn: skillsPathPrefix(provider, skill.scope, projectDir)
991
1116
  };
992
1117
  }
993
- async function checkPluginSkillsInstalled(config, projectDir) {
1118
+ async function checkPluginSkillsInstalled(config, installation) {
994
1119
  const pluginSkills = config.skills.filter((s) => s.origin !== EJECT_SOURCE);
995
1120
  if (pluginSkills.length === 0) {
996
1121
  return {
@@ -999,18 +1124,11 @@ async function checkPluginSkillsInstalled(config, projectDir) {
999
1124
  message: "No plugin-mode skills configured"
1000
1125
  };
1001
1126
  }
1002
- const baseDirs = unique(pluginSkills.map((s) => installBaseDir(projectDir, s.scope)));
1003
- const missingPerBaseDir = await Promise.all(
1004
- baseDirs.map(async (baseDir) => {
1005
- const installedIds = new Set(
1006
- (await getVerifiedPluginInstallPaths(baseDir)).map(
1007
- (plugin) => parseMarketplacePluginRef(plugin.pluginKey)
1008
- )
1009
- );
1010
- return pluginSkills.filter((skill) => installBaseDir(projectDir, skill.scope) === baseDir).filter((skill) => !installedIds.has(skill.id)).map((skill) => skill.id);
1011
- })
1127
+ const scopes = unique(pluginSkills.map((s) => s.scope));
1128
+ const missingPerScope = await Promise.all(
1129
+ scopes.map((scope) => pluginSkillsMissingAt(scope, pluginSkills, installation))
1012
1130
  );
1013
- const missingSkills = missingPerBaseDir.flat();
1131
+ const missingSkills = missingPerScope.flat();
1014
1132
  if (missingSkills.length > 0) {
1015
1133
  return {
1016
1134
  kind: "plugins",
@@ -1025,6 +1143,13 @@ async function checkPluginSkillsInstalled(config, projectDir) {
1025
1143
  message: `${ratio(pluginSkills.length, pluginSkills.length, "plugin-mode skill")} installed`
1026
1144
  };
1027
1145
  }
1146
+ async function pluginSkillsMissingAt(scope, pluginSkills, { projectDir, hosts }) {
1147
+ const installedIds = await installedPluginSkillIds(
1148
+ installBaseDir(projectDir, scope),
1149
+ hosts[scope]
1150
+ );
1151
+ return pluginSkills.filter((skill) => skill.scope === scope).filter((skill) => !installedIds.has(skill.id)).map((skill) => skill.id);
1152
+ }
1028
1153
  var MARKETPLACE_CHOSEN_BY = {
1029
1154
  flag: "named by this run",
1030
1155
  env: `named by ${SOURCE_ENV_VAR}`,
@@ -1037,29 +1162,37 @@ function howItWasReached(source, isLocal) {
1037
1162
  }
1038
1163
  function reachedMarketplace(result) {
1039
1164
  const { source, sourceOrigin } = result.sourceConfig;
1040
- const skillCount = result.marketplaceSkillIds.size;
1041
- const sourceLabel = result.isLocal ? "local" : "remote";
1165
+ const available = `${plural(result.marketplaceSkillIds.size, "skill")} available`;
1166
+ const chosenBy = MARKETPLACE_CHOSEN_BY[sourceOrigin];
1167
+ if (result.unreachable) {
1168
+ return {
1169
+ kind: "source",
1170
+ status: "warn",
1171
+ message: `Could not reach ${source} \u2014 read its cached copy instead`,
1172
+ details: [available, `The copy at ${result.sourcePath} may be out of date \u2014 ${chosenBy}`]
1173
+ };
1174
+ }
1042
1175
  return {
1043
1176
  kind: "source",
1044
1177
  status: "pass",
1045
- message: `Connected to ${sourceLabel}: ${result.sourcePath}`,
1046
- details: [
1047
- `${plural(skillCount, "skill")} available`,
1048
- `${howItWasReached(source, result.isLocal)} \u2014 ${MARKETPLACE_CHOSEN_BY[sourceOrigin]}`
1049
- ]
1178
+ message: `Connected to ${result.isLocal ? "local" : "remote"}: ${result.sourcePath}`,
1179
+ details: [available, `${howItWasReached(source, result.isLocal)} \u2014 ${chosenBy}`]
1050
1180
  };
1051
1181
  }
1052
- async function checkSourceReachable(projectDir) {
1182
+ async function checkSourceReachable({
1183
+ projectDir,
1184
+ hosts
1185
+ }) {
1053
1186
  try {
1054
- const { sourceResult } = await loadSource({ projectDir });
1055
- return reachedMarketplace(sourceResult);
1187
+ return reachedMarketplace(
1188
+ await loadSkillsMatrixFromSource({ projectDir, provider: hosts.project })
1189
+ );
1056
1190
  } catch (error) {
1057
- const message = getErrorMessage(error);
1058
1191
  return {
1059
1192
  kind: "source",
1060
1193
  status: "fail",
1061
- message: "Failed to load marketplace",
1062
- details: [message]
1194
+ message: refusedOr(error, "Failed to load marketplace"),
1195
+ details: [getErrorMessage(error)]
1063
1196
  };
1064
1197
  }
1065
1198
  }
@@ -1074,11 +1207,8 @@ var CONTENT_CHECKS = [
1074
1207
  kind: "content-sources",
1075
1208
  name: "Marketplaces",
1076
1209
  noun: "marketplace",
1077
- // The registered marketplaces are the ones config.ts names.
1078
1210
  readsConfig: true,
1079
- // A marketplace whose content is broken is missing skills from the matrix every configured
1080
- // id is resolved against, so the skills row would call ids "not found" that this row has
1081
- // already accounted for.
1211
+ // A broken marketplace leaves skills out of the matrix ids resolve against.
1082
1212
  blocks: ["skills"],
1083
1213
  run: validateRegisteredSources
1084
1214
  },
@@ -1087,9 +1217,7 @@ var CONTENT_CHECKS = [
1087
1217
  name: "Plugins",
1088
1218
  noun: "plugin",
1089
1219
  readsConfig: false,
1090
- // `resolvePluginInstallPaths` swallows a registry it cannot parse and returns none, so every
1091
- // plugin-mode skill would read "no enabled plugin found" — the registry's finding, once per
1092
- // configured skill, wearing the row's words.
1220
+ // An unparseable registry resolves as no installs at all.
1093
1221
  blocks: ["plugins"],
1094
1222
  run: validateInstalledPlugins
1095
1223
  },
@@ -1097,12 +1225,9 @@ var CONTENT_CHECKS = [
1097
1225
  kind: "content-skills",
1098
1226
  name: "Skills",
1099
1227
  noun: "skill",
1100
- // `~/.claude/skills/` is shared with everything else that installs a skill there, so which of
1101
- // its directories are this installation's is a question only the config can answer for the
1102
- // ones carrying no provenance marker. A config nobody can read leaves that unanswerable.
1228
+ // Only the config says which unmarked shared-skills directories are this installation's.
1103
1229
  readsConfig: true,
1104
- // `extractLocalSkill` DROPS a skill whose metadata.yaml is missing or unusable, so a "not
1105
- // found" from the skills row would be this finding re-worded.
1230
+ // `extractLocalSkill` drops a skill with unusable metadata.
1106
1231
  blocks: ["skills"],
1107
1232
  run: validateInstalledSkills
1108
1233
  },
@@ -1111,8 +1236,7 @@ var CONTENT_CHECKS = [
1111
1236
  name: "Agents",
1112
1237
  noun: "agent",
1113
1238
  readsConfig: false,
1114
- // Nothing downstream opens an agent .md. `Agents Compiled` asks whether the file is there and
1115
- // `No Orphans` reads the names of the files that are — broken frontmatter changes neither.
1239
+ // No operational row reads agent file contents.
1116
1240
  blocks: [],
1117
1241
  run: validateInstalledAgents
1118
1242
  }
@@ -1197,58 +1321,77 @@ var TIPS = [
1197
1321
  {
1198
1322
  kind: "agents",
1199
1323
  status: "warn",
1200
- tip: ` Tip: Run '${CLI_INVOKE_COMMAND} compile' to generate missing agent files`
1324
+ line: (provider) => ` Tip: Run '${commandActingOn(provider, "compile")}' to generate missing agent files`
1201
1325
  },
1202
1326
  {
1203
1327
  kind: "config",
1204
1328
  status: "fail",
1205
- tip: ` Tip: Run '${CLI_INVOKE_COMMAND} init' to create or fix configuration`
1329
+ line: () => ` Tip: Run '${CLI_INVOKE_COMMAND} init' to create or fix configuration`
1206
1330
  },
1207
1331
  {
1208
- // The remedy for a valid config with nothing in it. Not the tip above: there is nothing to
1209
- // create and nothing to fix. `init` on this config opens the editor rather than the dashboard
1210
- // — that is what the detection returning null is FOR — so it is the way to fill it in.
1211
1332
  kind: "config-empty",
1212
1333
  status: "warn",
1213
- tip: ` Tip: Nothing is configured yet \u2014 run '${CLI_INVOKE_COMMAND} init' to choose skills and sub-agents`
1334
+ line: () => ` Tip: Nothing is configured yet \u2014 run '${CLI_INVOKE_COMMAND} init' to choose skills and sub-agents`
1214
1335
  },
1215
1336
  {
1216
- // Printed beside the config tip above, which says how to get a configuration back and
1217
- // nothing about the files that outlived the old one. Both halves are named with what they
1218
- // actually do: `uninstall` matches skill directories by their own `forked-from` metadata and
1219
- // compiled agents by the marker each one carries, so neither needs the configuration that is
1220
- // gone — which is why the row can name both and this tip can promise both.
1221
1337
  kind: "orphans-unowned",
1222
1338
  status: "fail",
1223
- tip: ` Tip: Nothing declares the files above \u2014 '${CLI_INVOKE_COMMAND} init --from <id>' writes a configuration that can own them again, or '${CLI_INVOKE_COMMAND} uninstall' removes them, the compiled agents included: each file listed carries this CLI's own provenance, which is what that command reads when there is no configuration left`
1339
+ line: (provider) => ` Tip: Nothing declares the files above \u2014 '${commandActingOn(provider, "init --from <id>")}' writes a configuration that can own them again, or '${commandActingOn(provider, "uninstall")}' removes them, the compiled agents included: each file listed carries this CLI's own provenance, which is what that command reads when there is no configuration left`
1224
1340
  },
1225
1341
  {
1226
1342
  kind: "skills",
1227
1343
  status: "fail",
1228
- tip: " Tip: Check skill IDs in config match available skills"
1344
+ line: () => " Tip: Check skill IDs in config match available skills"
1229
1345
  },
1230
1346
  {
1231
1347
  kind: "installed",
1232
1348
  status: "warn",
1233
- // No command is named on purpose: 'eject skills --force' re-copies every skill in the
1234
- // marketplace and always targets project scope, so it cannot repair a global-scoped
1235
- // skill and it litters a plugin-mode project with local skill directories.
1236
- tip: " Tip: Re-eject the missing skills from the marketplace to restore their files"
1349
+ // No command named: `eject skills --force` copies every skill, and only at project scope.
1350
+ line: () => " Tip: Re-eject the missing skills from the marketplace to restore their files"
1237
1351
  }
1238
1352
  ];
1353
+ function commandActingOn(provider, command) {
1354
+ return `${CLI_INVOKE_COMMAND} ${command}${providerArgument(provider)}`;
1355
+ }
1239
1356
  function formatTips(results) {
1240
- return TIPS.filter((t) => results.some((r) => r.kind === t.kind && r.status === t.status)).map(
1241
- (t) => t.tip
1242
- );
1357
+ return unique(TIPS.flatMap((tip) => linesOf(tip, results)));
1358
+ }
1359
+ function linesOf(tip, results) {
1360
+ return results.filter((result) => answers(tip, result)).map((result) => tip.line(result.provider));
1361
+ }
1362
+ function answers(tip, result) {
1363
+ return result.kind === tip.kind && result.status === tip.status;
1243
1364
  }
1244
1365
  async function recreateConfigTips(contentResults, projectDir) {
1245
1366
  if (!failedContentKinds(contentResults).has(CONFIG_CHECK.kind)) return [];
1246
1367
  const failures = await findConfigLoadFailures(projectDir).catch(() => []);
1247
1368
  return failures.map(
1248
- (failure) => ` Tip: There is no automatic repair \u2014 recreate the configuration by running ${recreateConfigFrom(failure.scopeRoot)}`
1369
+ (failure) => ` Tip: There is no automatic repair \u2014 recreate the configuration by running ${recreateConfigFrom(failure)}`
1249
1370
  );
1250
1371
  }
1372
+ var INSTALLATIONS_ROW_NAME = "Installations";
1373
+ async function checkInstallationsInPlay(projectDir, providers) {
1374
+ const finding = await ambiguityFinding(projectDir);
1375
+ return {
1376
+ kind: "installations",
1377
+ status: "fail",
1378
+ message: `${providers.length} installations here, each checked below`,
1379
+ details: finding === null ? [] : [`- ${finding}`]
1380
+ };
1381
+ }
1382
+ function installationHeading(provider) {
1383
+ return `${SECTION_INDENT}The ${provider} installation`;
1384
+ }
1385
+ var SOURCE_REPO_ROW_NAME = "Marketplace Repository";
1386
+ async function uninstalledSourceRepo(projectDir) {
1387
+ if (!await isSourceRepo(projectDir)) return null;
1388
+ return { kind: "source-repo", status: "skip", message: SKIP_NO_INSTALLATION };
1389
+ }
1251
1390
  var LAYOUT_ROW_NAME = "Layout";
1391
+ async function checkLayout({ projectDir, hosts }) {
1392
+ const scopes = await sourceScopesInPlay(projectDir, ({ kind }) => hosts[kind]);
1393
+ return scopes.map(toLayoutResult);
1394
+ }
1252
1395
  function toLayoutResult(scope) {
1253
1396
  return {
1254
1397
  kind: "layout",
@@ -1257,8 +1400,8 @@ function toLayoutResult(scope) {
1257
1400
  };
1258
1401
  }
1259
1402
  var PLACEMENTS_ROW_NAME = "Placements Offered";
1260
- async function checkOfferedPlacements(projectDir) {
1261
- const findings = await everythingThatWouldStopACommandActing(projectDir);
1403
+ async function checkOfferedPlacements(installation) {
1404
+ const findings = await unofferablePlacements(installation);
1262
1405
  if (findings.length === 0) {
1263
1406
  return { kind: "placements", status: "pass", message: "Every configured skill is placeable" };
1264
1407
  }
@@ -1269,13 +1412,6 @@ async function checkOfferedPlacements(projectDir) {
1269
1412
  details: findings.map((finding) => `- ${finding}`)
1270
1413
  };
1271
1414
  }
1272
- async function everythingThatWouldStopACommandActing(projectDir) {
1273
- const [unofferable, ambiguous] = await Promise.all([
1274
- unofferablePlacementsFound(projectDir),
1275
- ambiguityFinding(projectDir)
1276
- ]);
1277
- return ambiguous === null ? unofferable : [...unofferable, ambiguous];
1278
- }
1279
1415
  function skippedResult(kind) {
1280
1416
  return { kind, status: "skip", message: "Skipped (config invalid)" };
1281
1417
  }
@@ -1296,13 +1432,31 @@ function contentBlockedResult(row, failedContent) {
1296
1432
  function runContentCheck(check, projectDir) {
1297
1433
  return safeCheck(check.kind, async () => toContentResult(check, await check.run(projectDir)));
1298
1434
  }
1435
+ async function runGatedContentCheck(check, projectDir, configUnreadable) {
1436
+ if (check.readsConfig && configUnreadable) return skippedContentResult(check.kind);
1437
+ return runContentCheck(check, projectDir);
1438
+ }
1439
+ async function flatMapInTurn(items, step) {
1440
+ return items.reduce(
1441
+ async (earlier, item) => [...await earlier, ...await step(item)],
1442
+ Promise.resolve([])
1443
+ );
1444
+ }
1299
1445
  async function safeCheck(kind, fn) {
1300
1446
  try {
1301
1447
  return await fn();
1302
1448
  } catch (error) {
1303
- return { kind, status: "fail", message: "Check threw", details: [getErrorMessage(error)] };
1449
+ return {
1450
+ kind,
1451
+ status: "fail",
1452
+ message: refusedOr(error, "Check threw"),
1453
+ details: [getErrorMessage(error)]
1454
+ };
1304
1455
  }
1305
1456
  }
1457
+ function refusedOr(error, otherwise) {
1458
+ return error instanceof MarketplaceRefusedError ? "Refused" : otherwise;
1459
+ }
1306
1460
  var Doctor = class _Doctor extends BaseCommand {
1307
1461
  static summary = "Diagnose common configuration issues";
1308
1462
  static description = `Run diagnostic checks on your ${DEFAULT_BRANDING.NAME} configuration to identify issues with config validity, skill resolution, agent compilation, and marketplace connectivity.`;
@@ -1327,12 +1481,6 @@ var Doctor = class _Doctor extends BaseCommand {
1327
1481
  this.log(`${SECTION_INDENT}Checking configuration health...`);
1328
1482
  this.log("");
1329
1483
  }
1330
- /**
1331
- * The config is checked first and alone. A check that reads it to know what to validate would
1332
- * otherwise ask a file already reported as unreadable — and asking is what emitted the loader's
1333
- * own failure line once per read, spliced between these rows. Everything that walks installed
1334
- * content on disk still runs: it says something true whatever the config is in.
1335
- */
1336
1484
  async runContentChecks(projectDir) {
1337
1485
  this.log(`${SECTION_INDENT}${SECTION_CONTENT}`);
1338
1486
  const configResult = await runContentCheck(CONFIG_CHECK, projectDir);
@@ -1340,7 +1488,7 @@ var Doctor = class _Doctor extends BaseCommand {
1340
1488
  const rows = await Promise.all(
1341
1489
  CONTENT_CHECKS.map(async (check) => ({
1342
1490
  name: check.name,
1343
- result: check.readsConfig && configUnreadable ? skippedContentResult(check.kind) : await runContentCheck(check, projectDir)
1491
+ result: await runGatedContentCheck(check, projectDir, configUnreadable)
1344
1492
  }))
1345
1493
  );
1346
1494
  const allRows = [{ name: CONFIG_CHECK.name, result: configResult }, ...rows];
@@ -1349,13 +1497,6 @@ var Doctor = class _Doctor extends BaseCommand {
1349
1497
  }
1350
1498
  return allRows.map((row) => row.result);
1351
1499
  }
1352
- /**
1353
- * The whole layer stands down for the two findings that leave every row with nothing to say: a
1354
- * config nobody can read — every row is read out of it, so all of them would be cascades of that
1355
- * one file — and a marketplace repository with nothing installed, where a marketplace author has
1356
- * no install for the layer to describe. Every other content error is scoped to the rows that
1357
- * read what it is about; {@link GatedContentCheck} says which those are, per pass.
1358
- */
1359
1500
  async runOperationalChecks(projectDir, contentResults) {
1360
1501
  this.log("");
1361
1502
  this.log(`${SECTION_INDENT}${SECTION_OPERATIONAL}`);
@@ -1364,48 +1505,78 @@ var Doctor = class _Doctor extends BaseCommand {
1364
1505
  this.log(`${ROW_INDENT}${SKIP_AFTER_CONFIG_ERROR}`);
1365
1506
  return [];
1366
1507
  }
1367
- const detected = await detectProject(projectDir);
1368
- if (!detected && await this.isUninstalledSourceRepo(projectDir)) {
1369
- this.log(`${ROW_INDENT}${SKIP_NO_INSTALLATION}`);
1508
+ const providers = await providersInPlay(projectDir);
1509
+ if (providers.length > 1) {
1510
+ return this.checkEachInstallation(projectDir, providers, failedContent);
1511
+ }
1512
+ return this.checkTheInstallation(projectDir, failedContent);
1513
+ }
1514
+ async checkTheInstallation(projectDir, failedContent) {
1515
+ const installation = { projectDir, hosts: hostsOnDisk(projectDir) };
1516
+ const configState = await resolveConfigState(installation);
1517
+ if (configState.kind === "absent") {
1518
+ const sourceRepo = await safeCheck("source-repo", () => uninstalledSourceRepo(projectDir));
1519
+ if (sourceRepo !== null) return this.reportSourceRepo(sourceRepo);
1520
+ }
1521
+ return this.runAllChecks(installation, configState, failedContent);
1522
+ }
1523
+ /** A repository nothing is installed in skips the layer; one whose own config breaks is a row. */
1524
+ reportSourceRepo(sourceRepo) {
1525
+ if (sourceRepo.status === "skip") {
1526
+ this.log(`${ROW_INDENT}${sourceRepo.message}`);
1370
1527
  return [];
1371
1528
  }
1372
- const configState = await resolveConfigState(detected, projectDir);
1373
- return this.runAllChecks(projectDir, configState, failedContent);
1374
- }
1375
- /**
1376
- * A config file that exists but failed to load also detects as "no project", and
1377
- * that is a finding rather than an absence — so the skip requires no config file
1378
- * at all, not merely no usable one.
1379
- */
1380
- async isUninstalledSourceRepo(projectDir) {
1381
- if (await fileExists(getInstalledConfigPath(projectDir))) return false;
1382
- return isSourceRepo(projectDir);
1383
- }
1384
- async runAllChecks(projectDir, configState, failedContent) {
1385
- const { result: configResult, config } = checkConfigValid(configState, projectDir);
1529
+ this.logCheck(SOURCE_REPO_ROW_NAME, sourceRepo);
1530
+ return [sourceRepo];
1531
+ }
1532
+ /** In turn rather than at once: each block prints as it runs, and each load seats the matrix. */
1533
+ async checkEachInstallation(projectDir, providers, failedContent) {
1534
+ const installations = await safeCheck(
1535
+ "installations",
1536
+ () => checkInstallationsInPlay(projectDir, providers)
1537
+ );
1538
+ this.logCheck(INSTALLATIONS_ROW_NAME, installations);
1539
+ const blocks = await flatMapInTurn(
1540
+ providers,
1541
+ (provider) => this.checkOneOf(projectDir, provider, failedContent)
1542
+ );
1543
+ return [installations, ...blocks];
1544
+ }
1545
+ async checkOneOf(projectDir, provider, failedContent) {
1546
+ this.log("");
1547
+ this.log(installationHeading(provider));
1548
+ const installation = { projectDir, hosts: hostsOf(provider) };
1549
+ const results = await this.runAllChecks(
1550
+ installation,
1551
+ await resolveConfigState(installation),
1552
+ failedContent
1553
+ );
1554
+ return results.map((result) => ({ ...result, provider }));
1555
+ }
1556
+ async runAllChecks(installation, configState, failedContent) {
1557
+ const { result: configResult, config } = checkConfigValid(configState, installation);
1386
1558
  this.logCheck("Config Valid", configResult);
1387
- const layoutResults = await this.reportLayout(projectDir);
1559
+ const layoutResults = await this.reportLayout(installation);
1388
1560
  const placementsResult = await safeCheck(
1389
1561
  "placements",
1390
- () => checkOfferedPlacements(projectDir)
1562
+ () => checkOfferedPlacements(installation)
1391
1563
  );
1392
1564
  this.logCheck(PLACEMENTS_ROW_NAME, placementsResult);
1393
- const sourceResult = await safeCheck("source", () => checkSourceReachable(projectDir));
1565
+ const sourceResult = await safeCheck("source", () => checkSourceReachable(installation));
1394
1566
  const filteredConfig = config ? filterExcludedEntries(config) : null;
1395
- const skillsResult = await this.resolveSkillsCheck(
1396
- config,
1397
- sourceResult,
1398
- projectDir,
1399
- failedContent
1400
- );
1567
+ const skillsResult = await this.resolveSkillsCheck(config, sourceResult, failedContent);
1401
1568
  this.logCheck("Skills Resolved", skillsResult);
1402
- const agentsResult = filteredConfig ? await safeCheck("agents", () => checkAgentsCompiled(filteredConfig, projectDir)) : skippedResult("agents");
1569
+ const agentsResult = await this.resolveAgentsCheck(filteredConfig, installation);
1403
1570
  this.logCheck("Agents Compiled", agentsResult);
1404
- const orphansResult = await this.resolveOrphansCheck(config, configState, projectDir);
1571
+ const orphansResult = await this.resolveOrphansCheck(config, configState, installation);
1405
1572
  this.logCheck("No Orphans", orphansResult);
1406
- const installedResult = filteredConfig ? await safeCheck("installed", () => checkSkillsInstalled(filteredConfig, projectDir)) : skippedResult("installed");
1573
+ const installedResult = await this.resolveInstalledCheck(filteredConfig, installation);
1407
1574
  this.logCheck("Skills Installed", installedResult);
1408
- const pluginsResult = await this.resolvePluginsCheck(filteredConfig, projectDir, failedContent);
1575
+ const pluginsResult = await this.resolvePluginsCheck(
1576
+ filteredConfig,
1577
+ installation,
1578
+ failedContent
1579
+ );
1409
1580
  this.logCheck("Plugins Installed", pluginsResult);
1410
1581
  this.logCheck("Marketplace Reachable", sourceResult);
1411
1582
  return [
@@ -1420,59 +1591,38 @@ var Doctor = class _Doctor extends BaseCommand {
1420
1591
  sourceResult
1421
1592
  ];
1422
1593
  }
1423
- /**
1424
- * One `Layout` row per scope in play.
1425
- *
1426
- * Per scope rather than per machine: a project and the global installation are two
1427
- * installations, and one row for the two can only name one of their folders.
1428
- *
1429
- * Printed right under `Config Valid`, because every row below it is read out of a config file
1430
- * in the folder this row names.
1431
- */
1432
- async reportLayout(projectDir) {
1433
- const rows = (await sourceScopesInPlay(projectDir)).map(toLayoutResult);
1594
+ async reportLayout(installation) {
1595
+ const rows = await checkLayout(installation);
1434
1596
  for (const row of rows) {
1435
1597
  this.logCheck(LAYOUT_ROW_NAME, row);
1436
1598
  }
1437
1599
  return rows;
1438
1600
  }
1439
- /**
1440
- * The orphan row, and which question it can answer. With a config it names the compiled
1441
- * agents that config does not; with no config at all every installed file is unowned and it
1442
- * names all of them. It skips for the one state left — a config that loads and fails
1443
- * validation — because there the file that says who owns what has already been rejected, and
1444
- * an installation must not be called stranded on the strength of one nobody can trust.
1445
- */
1446
- async resolveOrphansCheck(config, configState, projectDir) {
1447
- if (config) return safeCheck("orphans", () => checkNoOrphans(config, projectDir));
1448
- if (configState.kind === "absent") {
1449
- return safeCheck("orphans", () => checkUnownedInstallation(projectDir));
1450
- }
1451
- return skippedResult("orphans");
1452
- }
1453
- /**
1454
- * The skills row, and the three states in which it cannot be computed: no config to read the
1455
- * skills out of, content it resolves against that the layer above has already reported broken,
1456
- * and a marketplace that never loaded — so there is no populated matrix to resolve them
1457
- * against, and every configured skill would be reported "not found".
1458
- */
1459
- async resolveSkillsCheck(config, sourceResult, projectDir, failedContent) {
1601
+ /** Skips on a config the checks rejected: ownership can't be judged from a rejected file. */
1602
+ async resolveOrphansCheck(config, configState, installation) {
1603
+ if (!config && configState.kind !== "absent") return skippedResult("orphans");
1604
+ return safeCheck("orphans", () => checkNoOrphans(configState, config, installation));
1605
+ }
1606
+ async resolveSkillsCheck(config, sourceResult, failedContent) {
1460
1607
  if (!config) return skippedResult("skills");
1461
1608
  const blocked = contentBlockedResult("skills", failedContent);
1462
1609
  if (blocked) return blocked;
1463
1610
  if (sourceResult.status === "fail") return sourceUnreachableSkillsResult();
1464
- return safeCheck("skills", () => checkSkillsResolved(config, matrix, projectDir));
1465
- }
1466
- /**
1467
- * The plugin row, and the two states in which it cannot be computed: no config to read the
1468
- * plugin-mode skills out of, and a plugin registry the layer above could not parse — which
1469
- * resolves as no installs at all, so every configured skill would be reported missing.
1470
- */
1471
- async resolvePluginsCheck(config, projectDir, failedContent) {
1611
+ return safeCheck("skills", () => checkSkillsResolved(config, matrix));
1612
+ }
1613
+ async resolveAgentsCheck(config, installation) {
1614
+ if (!config) return skippedResult("agents");
1615
+ return safeCheck("agents", () => checkAgentsCompiled(config, installation));
1616
+ }
1617
+ async resolveInstalledCheck(config, installation) {
1618
+ if (!config) return skippedResult("installed");
1619
+ return safeCheck("installed", () => checkSkillsInstalled(config, installation));
1620
+ }
1621
+ async resolvePluginsCheck(config, installation, failedContent) {
1472
1622
  if (!config) return skippedResult("plugins");
1473
1623
  const blocked = contentBlockedResult("plugins", failedContent);
1474
1624
  if (blocked) return blocked;
1475
- return safeCheck("plugins", () => checkPluginSkillsInstalled(config, projectDir));
1625
+ return safeCheck("plugins", () => checkPluginSkillsInstalled(config, installation));
1476
1626
  }
1477
1627
  logCheck(name, result) {
1478
1628
  for (const line of formatCheckLine(name, result)) {