@thinkingsage/kanon 0.8.0

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 (199) hide show
  1. package/CHANGELOG.md +410 -0
  2. package/LICENSE +21 -0
  3. package/README.md +168 -0
  4. package/bridge/mcp-server.cjs +14171 -0
  5. package/package.json +98 -0
  6. package/src/adapters/capabilities.ts +178 -0
  7. package/src/adapters/claude-code.ts +110 -0
  8. package/src/adapters/cline.ts +98 -0
  9. package/src/adapters/codex.ts +173 -0
  10. package/src/adapters/copilot.ts +106 -0
  11. package/src/adapters/cursor.ts +97 -0
  12. package/src/adapters/degradation.ts +95 -0
  13. package/src/adapters/index.ts +324 -0
  14. package/src/adapters/kiro-frontmatter.ts +139 -0
  15. package/src/adapters/kiro-inclusion.ts +86 -0
  16. package/src/adapters/kiro.ts +412 -0
  17. package/src/adapters/qdeveloper.ts +115 -0
  18. package/src/adapters/types.ts +81 -0
  19. package/src/adapters/windsurf.ts +96 -0
  20. package/src/admin.ts +283 -0
  21. package/src/asset-conventions.ts +118 -0
  22. package/src/attribution-backfill.ts +319 -0
  23. package/src/attribution-report.ts +95 -0
  24. package/src/attribution.ts +239 -0
  25. package/src/backends/github.ts +194 -0
  26. package/src/backends/http.ts +122 -0
  27. package/src/backends/index.ts +39 -0
  28. package/src/backends/local.ts +47 -0
  29. package/src/backends/s3.ts +157 -0
  30. package/src/backends/types.ts +59 -0
  31. package/src/base-cache.ts +270 -0
  32. package/src/browse-ui.ts +3754 -0
  33. package/src/browse.ts +1038 -0
  34. package/src/build.ts +1108 -0
  35. package/src/catalog.ts +204 -0
  36. package/src/cli-deprecated.ts +29 -0
  37. package/src/cli.ts +773 -0
  38. package/src/collection-admin.ts +287 -0
  39. package/src/collection-builder.ts +464 -0
  40. package/src/collections.ts +116 -0
  41. package/src/compatibility.ts +105 -0
  42. package/src/config.ts +743 -0
  43. package/src/eval/rubrics/kiro-progressive-steering.ts +841 -0
  44. package/src/eval.ts +1169 -0
  45. package/src/file-writer.ts +61 -0
  46. package/src/format-registry.ts +141 -0
  47. package/src/guild/auto-updater.ts +163 -0
  48. package/src/guild/backend-resolver.ts +49 -0
  49. package/src/guild/cli.ts +592 -0
  50. package/src/guild/collection-expander.ts +47 -0
  51. package/src/guild/global-cache.ts +247 -0
  52. package/src/guild/hook-generator.ts +100 -0
  53. package/src/guild/manifest.ts +154 -0
  54. package/src/guild/path-utils.ts +12 -0
  55. package/src/guild/sync.ts +622 -0
  56. package/src/guild/version-resolver.ts +42 -0
  57. package/src/help/metadata.ts +445 -0
  58. package/src/help/renderer.ts +265 -0
  59. package/src/help/typo-suggester.ts +25 -0
  60. package/src/hooks/expression.ts +493 -0
  61. package/src/hooks/pipeline.ts +141 -0
  62. package/src/import.ts +773 -0
  63. package/src/importers/claude-code.ts +134 -0
  64. package/src/importers/cline.ts +103 -0
  65. package/src/importers/codex.ts +140 -0
  66. package/src/importers/copilot.ts +103 -0
  67. package/src/importers/cursor.ts +105 -0
  68. package/src/importers/index.ts +390 -0
  69. package/src/importers/kiro.ts +110 -0
  70. package/src/importers/qdeveloper.ts +103 -0
  71. package/src/importers/types.ts +54 -0
  72. package/src/importers/windsurf.ts +104 -0
  73. package/src/install.ts +1005 -0
  74. package/src/manifest-admin.ts +306 -0
  75. package/src/mcp-bridge.ts +240 -0
  76. package/src/mutation/delta.ts +50 -0
  77. package/src/mutation/history.ts +66 -0
  78. package/src/mutation/operators.ts +524 -0
  79. package/src/mutation/runner.ts +332 -0
  80. package/src/new.ts +106 -0
  81. package/src/outcomes/collision.ts +127 -0
  82. package/src/outcomes/normalize.ts +208 -0
  83. package/src/outcomes/registry.ts +173 -0
  84. package/src/parser.ts +446 -0
  85. package/src/provenance-backfill-cli.ts +319 -0
  86. package/src/provenance-backfill.ts +520 -0
  87. package/src/publish.ts +354 -0
  88. package/src/reconcile-orchestrator.ts +502 -0
  89. package/src/reconcile-report-renderer.ts +176 -0
  90. package/src/resolve-body.ts +15 -0
  91. package/src/rosetta/builtins/compatibility-profiles.ts +297 -0
  92. package/src/rosetta/builtins/contracts.ts +1033 -0
  93. package/src/rosetta/builtins/pretty-printers/claude-code-native.ts +122 -0
  94. package/src/rosetta/builtins/pretty-printers/cline-native.ts +50 -0
  95. package/src/rosetta/builtins/pretty-printers/codex-native.ts +127 -0
  96. package/src/rosetta/builtins/pretty-printers/copilot-native.ts +50 -0
  97. package/src/rosetta/builtins/pretty-printers/cursor-native.ts +50 -0
  98. package/src/rosetta/builtins/pretty-printers/index.ts +81 -0
  99. package/src/rosetta/builtins/pretty-printers/kiro-native.ts +166 -0
  100. package/src/rosetta/builtins/pretty-printers/kiro-power.ts +108 -0
  101. package/src/rosetta/builtins/pretty-printers/kiro-skill.ts +88 -0
  102. package/src/rosetta/builtins/pretty-printers/qdeveloper-native.ts +51 -0
  103. package/src/rosetta/builtins/pretty-printers/superpowers.ts +97 -0
  104. package/src/rosetta/builtins/pretty-printers/windsurf-native.ts +50 -0
  105. package/src/rosetta/builtins/sources/claude-code-native.ts +348 -0
  106. package/src/rosetta/builtins/sources/cline-native.ts +176 -0
  107. package/src/rosetta/builtins/sources/codex-native.ts +343 -0
  108. package/src/rosetta/builtins/sources/copilot-native.ts +178 -0
  109. package/src/rosetta/builtins/sources/cursor-native.ts +176 -0
  110. package/src/rosetta/builtins/sources/index.ts +95 -0
  111. package/src/rosetta/builtins/sources/kiro-native.ts +462 -0
  112. package/src/rosetta/builtins/sources/kiro-power.ts +285 -0
  113. package/src/rosetta/builtins/sources/kiro-skill.ts +230 -0
  114. package/src/rosetta/builtins/sources/qdeveloper-native.ts +181 -0
  115. package/src/rosetta/builtins/sources/superpowers.ts +240 -0
  116. package/src/rosetta/builtins/sources/windsurf-native.ts +176 -0
  117. package/src/rosetta/builtins/targets/claude-code.ts +181 -0
  118. package/src/rosetta/builtins/targets/cline.ts +87 -0
  119. package/src/rosetta/builtins/targets/codex.ts +226 -0
  120. package/src/rosetta/builtins/targets/copilot.ts +103 -0
  121. package/src/rosetta/builtins/targets/cursor.ts +87 -0
  122. package/src/rosetta/builtins/targets/index.ts +60 -0
  123. package/src/rosetta/builtins/targets/kiro.ts +278 -0
  124. package/src/rosetta/builtins/targets/qdeveloper.ts +103 -0
  125. package/src/rosetta/builtins/targets/windsurf.ts +87 -0
  126. package/src/rosetta/canonical.ts +729 -0
  127. package/src/rosetta/compatibility.ts +432 -0
  128. package/src/rosetta/contracts.ts +329 -0
  129. package/src/rosetta/detector.ts +724 -0
  130. package/src/rosetta/diagnostics.ts +630 -0
  131. package/src/rosetta/engine-bootstrap.ts +103 -0
  132. package/src/rosetta/engine.ts +744 -0
  133. package/src/rosetta/index.ts +381 -0
  134. package/src/rosetta/inspection.ts +530 -0
  135. package/src/rosetta/plan.ts +448 -0
  136. package/src/rosetta/provenance-digest.ts +369 -0
  137. package/src/rosetta/reconcile.ts +812 -0
  138. package/src/rosetta/redaction.ts +467 -0
  139. package/src/rosetta/registry.ts +712 -0
  140. package/src/rosetta/renderers.ts +571 -0
  141. package/src/rosetta/request-guard.ts +335 -0
  142. package/src/rosetta/resolution.ts +419 -0
  143. package/src/rosetta/source-accounting.ts +233 -0
  144. package/src/rosetta/templates.ts +129 -0
  145. package/src/rosetta-cli.ts +717 -0
  146. package/src/rosetta-docs-generator.ts +793 -0
  147. package/src/rosetta-profiles-cli.ts +367 -0
  148. package/src/schemas.ts +1712 -0
  149. package/src/spec-coordination.ts +1141 -0
  150. package/src/temper.ts +747 -0
  151. package/src/template-bundle-loader.ts +312 -0
  152. package/src/template-engine.ts +53 -0
  153. package/src/translation-application-policy.ts +496 -0
  154. package/src/translation-orchestrator.ts +1013 -0
  155. package/src/translation-plan-applier.ts +473 -0
  156. package/src/tutorial.ts +305 -0
  157. package/src/validate.ts +1093 -0
  158. package/src/versioning.ts +553 -0
  159. package/src/wizard.ts +660 -0
  160. package/src/workspace.ts +237 -0
  161. package/templates/eval-contexts/claude-code.md.njk +6 -0
  162. package/templates/eval-contexts/cline.md.njk +6 -0
  163. package/templates/eval-contexts/copilot.md.njk +6 -0
  164. package/templates/eval-contexts/cursor.md.njk +6 -0
  165. package/templates/eval-contexts/kiro.md.njk +10 -0
  166. package/templates/eval-contexts/qdeveloper.md.njk +6 -0
  167. package/templates/eval-contexts/windsurf.md.njk +6 -0
  168. package/templates/harness-adapters/_base/attribution-footer.md.njk +17 -0
  169. package/templates/harness-adapters/_base/base.md.njk +16 -0
  170. package/templates/harness-adapters/claude-code/claude.md.njk +1 -0
  171. package/templates/harness-adapters/claude-code/mcp.json.njk +1 -0
  172. package/templates/harness-adapters/claude-code/settings.json.njk +1 -0
  173. package/templates/harness-adapters/claude-code/skill-library-index.md.njk +13 -0
  174. package/templates/harness-adapters/claude-code/skill.md.njk +19 -0
  175. package/templates/harness-adapters/cline/hook.sh.njk +4 -0
  176. package/templates/harness-adapters/cline/mcp.json.njk +1 -0
  177. package/templates/harness-adapters/cline/rule.md.njk +1 -0
  178. package/templates/harness-adapters/codex/agents-md.md.njk +6 -0
  179. package/templates/harness-adapters/codex/agents-pointer.md.njk +16 -0
  180. package/templates/harness-adapters/codex/skill.md.njk +27 -0
  181. package/templates/harness-adapters/copilot/agents.md.njk +1 -0
  182. package/templates/harness-adapters/copilot/instructions.md.njk +1 -0
  183. package/templates/harness-adapters/copilot/scoped.md.njk +6 -0
  184. package/templates/harness-adapters/cursor/mcp.json.njk +1 -0
  185. package/templates/harness-adapters/cursor/rule.md.njk +6 -0
  186. package/templates/harness-adapters/kiro/hook.json.njk +1 -0
  187. package/templates/harness-adapters/kiro/mcp.json.njk +1 -0
  188. package/templates/harness-adapters/kiro/power-steering.md.njk +3 -0
  189. package/templates/harness-adapters/kiro/power.md.njk +12 -0
  190. package/templates/harness-adapters/kiro/steering.md.njk +16 -0
  191. package/templates/harness-adapters/qdeveloper/agent.md.njk +1 -0
  192. package/templates/harness-adapters/qdeveloper/mcp.json.njk +1 -0
  193. package/templates/harness-adapters/qdeveloper/rule.md.njk +1 -0
  194. package/templates/harness-adapters/windsurf/mcp.json.njk +1 -0
  195. package/templates/harness-adapters/windsurf/rule.md.njk +1 -0
  196. package/templates/harness-adapters/windsurf/workflow.md.njk +1 -0
  197. package/templates/knowledge/hooks.yaml.njk +4 -0
  198. package/templates/knowledge/knowledge.md.njk +53 -0
  199. package/templates/knowledge/mcp-servers.yaml.njk +2 -0
@@ -0,0 +1,464 @@
1
+ import { exists, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import * as p from "@clack/prompts";
4
+ import chalk from "chalk";
5
+ import { generateCatalog, SOURCE_DIRS } from "./catalog";
6
+ import {
7
+ buildCollectionMembership,
8
+ loadCollections,
9
+ validateArtifactCollectionRefs,
10
+ } from "./collections";
11
+ import type { Collection, HarnessName } from "./schemas";
12
+ import { SUPPORTED_HARNESSES, TrustLaneSchema } from "./schemas";
13
+
14
+ export interface CollectionBuildResult {
15
+ collection: string;
16
+ harness: string;
17
+ artifactsIncluded: number;
18
+ filesWritten: number;
19
+ }
20
+
21
+ /**
22
+ * Build a single collection bundle for a single harness.
23
+ *
24
+ * Copies all pre-built dist artifacts whose `collections` field includes
25
+ * this collection into `dist/<harness>/collections/<collection-name>/`.
26
+ * Each artifact's files land in a subdirectory named after the artifact,
27
+ * preserving the same structure as a normal artifact dist dir.
28
+ */
29
+ async function buildCollectionForHarness(
30
+ collection: Collection,
31
+ members: string[], // artifact names belonging to this collection
32
+ harness: HarnessName,
33
+ distDir: string,
34
+ ): Promise<{ artifactsIncluded: number; filesWritten: number }> {
35
+ const collectionOutDir = join(
36
+ distDir,
37
+ harness,
38
+ "collections",
39
+ collection.name,
40
+ );
41
+ await mkdir(collectionOutDir, { recursive: true });
42
+
43
+ let artifactsIncluded = 0;
44
+ let filesWritten = 0;
45
+
46
+ for (const artifactName of members) {
47
+ const artifactDistDir = join(distDir, harness, artifactName);
48
+
49
+ if (!(await exists(artifactDistDir))) continue;
50
+
51
+ const destDir = join(collectionOutDir, artifactName);
52
+ await mkdir(destDir, { recursive: true });
53
+
54
+ // Recursively copy all files from the artifact's dist dir
55
+ const copied = await copyDir(artifactDistDir, destDir);
56
+ if (copied > 0) {
57
+ artifactsIncluded++;
58
+ filesWritten += copied;
59
+ }
60
+ }
61
+
62
+ return { artifactsIncluded, filesWritten };
63
+ }
64
+
65
+ async function copyDir(src: string, dest: string): Promise<number> {
66
+ let count = 0;
67
+ const entries = await readdir(src, { withFileTypes: true });
68
+
69
+ for (const entry of entries) {
70
+ const srcPath = join(src, entry.name);
71
+ const destPath = join(dest, entry.name);
72
+
73
+ if (entry.isDirectory()) {
74
+ await mkdir(destPath, { recursive: true });
75
+ count += await copyDir(srcPath, destPath);
76
+ } else if (entry.isFile()) {
77
+ const content = await readFile(srcPath);
78
+ await writeFile(destPath, content);
79
+ count++;
80
+ }
81
+ }
82
+
83
+ return count;
84
+ }
85
+
86
+ /**
87
+ * Build all collection bundles for all (or a specific) harness.
88
+ *
89
+ * Requires `kanon build` to have already run — this command reads from
90
+ * the existing dist/ output rather than re-compiling artifacts.
91
+ */
92
+ export async function buildCollections(options: {
93
+ collectionsDir: string;
94
+ distDir: string;
95
+ harness?: HarnessName;
96
+ sourceDirs?: string[];
97
+ }): Promise<CollectionBuildResult[]> {
98
+ const { collectionsDir, distDir, harness } = options;
99
+ const sourceDirs = options.sourceDirs ?? [...SOURCE_DIRS];
100
+ const results: CollectionBuildResult[] = [];
101
+
102
+ // Load collection manifests
103
+ const collections = await loadCollections(collectionsDir);
104
+ if (collections.length === 0) {
105
+ console.error(
106
+ chalk.yellow("No collection manifests found in collections/"),
107
+ );
108
+ return results;
109
+ }
110
+
111
+ // Load catalog to get collection membership from artifact frontmatter
112
+ const catalogEntries = await generateCatalog(sourceDirs);
113
+
114
+ // Warn on unregistered collection references
115
+ const refWarnings = validateArtifactCollectionRefs(
116
+ catalogEntries,
117
+ collections,
118
+ );
119
+ for (const w of refWarnings) {
120
+ console.error(chalk.yellow(` ⚠ ${w.field}: ${w.message}`));
121
+ }
122
+
123
+ // Build membership map: collection name → artifact names
124
+ const membership = buildCollectionMembership(catalogEntries);
125
+
126
+ const targetHarnesses: HarnessName[] = harness
127
+ ? [harness]
128
+ : [...SUPPORTED_HARNESSES];
129
+
130
+ for (const collection of collections) {
131
+ const members = membership.get(collection.name) ?? [];
132
+
133
+ if (members.length === 0) {
134
+ console.error(
135
+ chalk.yellow(
136
+ ` ⚠ Collection "${collection.name}" has no member artifacts`,
137
+ ),
138
+ );
139
+ }
140
+
141
+ for (const h of targetHarnesses) {
142
+ const { artifactsIncluded, filesWritten } =
143
+ await buildCollectionForHarness(collection, members, h, distDir);
144
+ results.push({
145
+ collection: collection.name,
146
+ harness: h,
147
+ artifactsIncluded,
148
+ filesWritten,
149
+ });
150
+ }
151
+ }
152
+
153
+ return results;
154
+ }
155
+
156
+ /**
157
+ * Default `kanon collection` action — shows a status overview of all collection
158
+ * manifests, their member counts, trust lanes, and any validation warnings.
159
+ */
160
+ export async function collectionStatusCommand(): Promise<void> {
161
+ // When called as `kanon collection help`, show help instead of status
162
+ const collectionArgIdx = process.argv.indexOf("collection");
163
+ const nextArg =
164
+ collectionArgIdx >= 0 ? process.argv[collectionArgIdx + 1] : undefined;
165
+ if (nextArg === "help") {
166
+ process.argv.splice(collectionArgIdx + 1, 1, "--help");
167
+ // Re-parse will be handled by Commander on the next tick; just return to let it flow
168
+ console.log(
169
+ [
170
+ "",
171
+ " Manage knowledge collections",
172
+ "",
173
+ " Usage: forge collection [options] [command]",
174
+ "",
175
+ " Commands:",
176
+ " new Scaffold a new collection manifest",
177
+ " build Build collection bundles from dist artifacts",
178
+ "",
179
+ " Options:",
180
+ " -h, --help display help for command",
181
+ "",
182
+ ].join("\n"),
183
+ );
184
+ return;
185
+ }
186
+
187
+ const collectionsDir = "collections";
188
+ const collections = await loadCollections(collectionsDir);
189
+ const catalogEntries = await generateCatalog([...SOURCE_DIRS]);
190
+ const membership = buildCollectionMembership(catalogEntries);
191
+ const warnings = validateArtifactCollectionRefs(catalogEntries, collections);
192
+
193
+ if (collections.length === 0 && warnings.length === 0) {
194
+ console.log("");
195
+ console.log(chalk.bold(" No collections yet."));
196
+ console.log("");
197
+ console.log(
198
+ chalk.dim(
199
+ ' Collections are curated bundles of artifacts (e.g. "aws", "security", "onboarding").',
200
+ ),
201
+ );
202
+ console.log(
203
+ chalk.dim(" Artifacts declare membership in their frontmatter: ") +
204
+ chalk.cyan("collections: [aws]"),
205
+ );
206
+ console.log("");
207
+
208
+ const create = await p.confirm({
209
+ message: "Create your first collection now?",
210
+ initialValue: true,
211
+ });
212
+
213
+ if (p.isCancel(create) || !create) {
214
+ console.log("");
215
+ console.log(
216
+ chalk.dim(" Run ") +
217
+ chalk.cyan("forge collection new <name>") +
218
+ chalk.dim(" when you're ready."),
219
+ );
220
+ console.log("");
221
+ return;
222
+ }
223
+
224
+ const name = await p.text({
225
+ message: "Collection name (kebab-case)",
226
+ placeholder: "my-collection",
227
+ validate: (v) => {
228
+ if (!v || v.trim().length === 0) return "Name is required";
229
+ if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(v.trim()))
230
+ return "Must be kebab-case (e.g. my-collection)";
231
+ },
232
+ });
233
+ if (p.isCancel(name)) return;
234
+
235
+ await collectionNewCommand(name as string);
236
+ return;
237
+ }
238
+
239
+ const total = collections.length;
240
+ const totalMembers = [...membership.values()].reduce(
241
+ (s, m) => s + m.length,
242
+ 0,
243
+ );
244
+
245
+ console.log(
246
+ chalk.bold(`\n Collections`) +
247
+ chalk.dim(
248
+ ` (${total} manifest${total !== 1 ? "s" : ""}, ${totalMembers} total memberships)\n`,
249
+ ),
250
+ );
251
+
252
+ if (collections.length > 0) {
253
+ // Column widths
254
+ const nameWidth =
255
+ Math.max(...collections.map((c) => c.displayName.length), 4) + 2;
256
+ const trustWidth = 12;
257
+
258
+ for (const c of collections) {
259
+ const members = membership.get(c.name) ?? [];
260
+ const memberCount = members.length;
261
+ const trustBadge = c.trust
262
+ ? chalk.dim(`[${c.trust}]`).padEnd(trustWidth + 7) // pad accounts for dim codes
263
+ : "".padEnd(trustWidth);
264
+ const nameStr = chalk.cyan(c.displayName.padEnd(nameWidth));
265
+ const countStr =
266
+ memberCount === 0
267
+ ? chalk.yellow(`${memberCount} members`)
268
+ : chalk.green(`${memberCount} member${memberCount !== 1 ? "s" : ""}`);
269
+ const desc = c.description ? chalk.dim(` ${c.description}`) : "";
270
+
271
+ console.log(` ${nameStr}${trustBadge}${countStr}${desc}`);
272
+
273
+ if (members.length > 0 && members.length <= 5) {
274
+ console.log(chalk.dim(` ${members.join(", ")}`));
275
+ } else if (members.length > 5) {
276
+ console.log(
277
+ chalk.dim(
278
+ ` ${members.slice(0, 4).join(", ")}, +${members.length - 4} more`,
279
+ ),
280
+ );
281
+ }
282
+ }
283
+ }
284
+
285
+ // Unknown collection references (artifacts pointing to non-existent manifests)
286
+ if (warnings.length > 0) {
287
+ const uniqueUnknown = [
288
+ ...new Set(warnings.map((w) => w.message.match(/"([^"]+)"/)?.[1] ?? "?")),
289
+ ];
290
+ console.log("");
291
+ for (const name of uniqueUnknown) {
292
+ console.log(
293
+ chalk.yellow(
294
+ ` ⚠ Artifacts declare collection "${name}" — no manifest found in collections/`,
295
+ ),
296
+ );
297
+ }
298
+ }
299
+
300
+ // Artifacts with no collection membership
301
+ const unaffiliated = catalogEntries.filter(
302
+ (e) => e.collections.length === 0,
303
+ ).length;
304
+ if (unaffiliated > 0) {
305
+ console.log(
306
+ chalk.dim(
307
+ `\n ${unaffiliated} artifact${unaffiliated !== 1 ? "s" : ""} not in any collection`,
308
+ ),
309
+ );
310
+ }
311
+
312
+ console.log("");
313
+ console.log(
314
+ chalk.dim(" forge collection build — generate distributable bundles"),
315
+ );
316
+ console.log("");
317
+ }
318
+
319
+ /**
320
+ * Scaffold a new collection manifest interactively.
321
+ * If `name` is provided, pre-populates the name prompt.
322
+ */
323
+ export async function collectionNewCommand(name?: string): Promise<void> {
324
+ p.intro(chalk.bold("New collection"));
325
+
326
+ const collectionsDir = "collections";
327
+ await mkdir(collectionsDir, { recursive: true });
328
+
329
+ const collectionName =
330
+ name ??
331
+ ((await p.text({
332
+ message: "Collection name (kebab-case)",
333
+ placeholder: "my-collection",
334
+ validate: (v) => {
335
+ if (!v || v.trim().length === 0) return "Name is required";
336
+ if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(v.trim()))
337
+ return "Must be kebab-case (e.g. my-collection)";
338
+ },
339
+ })) as string);
340
+ if (p.isCancel(collectionName)) {
341
+ p.cancel("Cancelled.");
342
+ return;
343
+ }
344
+
345
+ const filePath = join(collectionsDir, `${collectionName}.yaml`);
346
+ if (await exists(filePath)) {
347
+ console.error(
348
+ chalk.red(
349
+ ` Collection "${collectionName}" already exists at ${filePath}`,
350
+ ),
351
+ );
352
+ process.exit(1);
353
+ }
354
+
355
+ const displayName = (await p.text({
356
+ message: "Display name",
357
+ placeholder: collectionName
358
+ .split("-")
359
+ .map((w) => w[0].toUpperCase() + w.slice(1))
360
+ .join(" "),
361
+ })) as string;
362
+ if (p.isCancel(displayName)) {
363
+ p.cancel("Cancelled.");
364
+ return;
365
+ }
366
+
367
+ const description = (await p.text({
368
+ message: "Description (one sentence)",
369
+ placeholder: `Artifacts for ${displayName || collectionName}`,
370
+ })) as string;
371
+ if (p.isCancel(description)) {
372
+ p.cancel("Cancelled.");
373
+ return;
374
+ }
375
+
376
+ const trust = await p.select({
377
+ message: "Trust lane",
378
+ options: TrustLaneSchema.options.map((t) => ({
379
+ value: t,
380
+ label: t,
381
+ hint: {
382
+ official: "maintained by core team",
383
+ partner: "maintained by trusted partner",
384
+ community: "community contributed",
385
+ experimental: "experimental / untested",
386
+ }[t],
387
+ })),
388
+ initialValue: "community" as const,
389
+ });
390
+ if (p.isCancel(trust)) {
391
+ p.cancel("Cancelled.");
392
+ return;
393
+ }
394
+
395
+ const resolvedDisplayName =
396
+ (displayName as string).trim() ||
397
+ collectionName
398
+ .split("-")
399
+ .map((w) => w[0].toUpperCase() + w.slice(1))
400
+ .join(" ");
401
+
402
+ const yaml = `${[
403
+ `name: ${collectionName}`,
404
+ `displayName: "${resolvedDisplayName}"`,
405
+ `description: "${(description as string).trim()}"`,
406
+ `trust: ${trust}`,
407
+ `tags: []`,
408
+ ].join("\n")}\n`;
409
+
410
+ await writeFile(filePath, yaml, "utf-8");
411
+
412
+ p.outro(
413
+ chalk.green(`✓ Created ${filePath}\n\n`) +
414
+ chalk.dim(` Add artifacts to this collection by setting:\n`) +
415
+ chalk.cyan(` collections: [${collectionName}]\n`) +
416
+ chalk.dim(` in their knowledge.md frontmatter.`),
417
+ );
418
+ }
419
+
420
+ export async function collectionBuildCommand(options: {
421
+ harness?: string;
422
+ }): Promise<void> {
423
+ const distDir = "dist";
424
+ const collectionsDir = "collections";
425
+
426
+ if (!(await exists(distDir))) {
427
+ console.error(
428
+ chalk.red("No dist/ directory found. Run `kanon build` first."),
429
+ );
430
+ process.exit(1);
431
+ }
432
+
433
+ if (
434
+ options.harness &&
435
+ !(SUPPORTED_HARNESSES as readonly string[]).includes(options.harness)
436
+ ) {
437
+ console.error(
438
+ chalk.red(
439
+ `Error: Unknown harness "${options.harness}". Valid harnesses: ${SUPPORTED_HARNESSES.join(", ")}`,
440
+ ),
441
+ );
442
+ process.exit(1);
443
+ }
444
+
445
+ const results = await buildCollections({
446
+ collectionsDir,
447
+ distDir,
448
+ harness: options.harness as HarnessName | undefined,
449
+ });
450
+
451
+ const totalArtifacts = results.reduce((s, r) => s + r.artifactsIncluded, 0);
452
+ const totalFiles = results.reduce((s, r) => s + r.filesWritten, 0);
453
+ const uniqueCollections = new Set(results.map((r) => r.collection)).size;
454
+
455
+ console.error(
456
+ chalk.green(
457
+ `\n✓ Collection build complete: ${uniqueCollections} collection(s), ${totalArtifacts} artifact inclusions, ${totalFiles} files written`,
458
+ ),
459
+ );
460
+
461
+ if (options.harness) {
462
+ console.error(` Harness: ${options.harness}`);
463
+ }
464
+ }
@@ -0,0 +1,116 @@
1
+ import { exists, readdir, readFile } from "node:fs/promises";
2
+ import { basename, join } from "node:path";
3
+ import * as yaml from "js-yaml";
4
+ import {
5
+ type CatalogEntry,
6
+ type Collection,
7
+ CollectionSchema,
8
+ type ValidationWarning,
9
+ } from "./schemas";
10
+
11
+ /**
12
+ * Load all collection manifests from a directory.
13
+ * Each `<name>.yaml` file is parsed against CollectionSchema.
14
+ * Files that fail validation are skipped with a console warning.
15
+ */
16
+ export async function loadCollections(
17
+ collectionsDir: string,
18
+ ): Promise<Collection[]> {
19
+ if (!(await exists(collectionsDir))) return [];
20
+
21
+ const entries = await readdir(collectionsDir);
22
+ const yamlFiles = entries
23
+ .filter((f) => f.endsWith(".yaml") || f.endsWith(".yml"))
24
+ .filter((f) => f !== ".gitkeep")
25
+ .sort();
26
+
27
+ const collections: Collection[] = [];
28
+
29
+ for (const file of yamlFiles) {
30
+ const filePath = join(collectionsDir, file);
31
+ let raw: string;
32
+ try {
33
+ raw = await readFile(filePath, "utf-8");
34
+ } catch {
35
+ continue;
36
+ }
37
+
38
+ let parsed: unknown;
39
+ try {
40
+ parsed = yaml.load(raw);
41
+ } catch {
42
+ console.error(`Warning: Skipping ${file} — invalid YAML`);
43
+ continue;
44
+ }
45
+
46
+ // Infer name from filename if not set in the manifest
47
+ if (parsed && typeof parsed === "object" && !("name" in parsed)) {
48
+ (parsed as Record<string, unknown>).name = basename(
49
+ file,
50
+ ".yaml",
51
+ ).replace(/\.yml$/, "");
52
+ }
53
+
54
+ const result = CollectionSchema.safeParse(parsed);
55
+ if (!result.success) {
56
+ console.error(
57
+ `Warning: Skipping ${file} — schema validation failed: ${result.error.issues.map((i) => i.message).join(", ")}`,
58
+ );
59
+ continue;
60
+ }
61
+
62
+ collections.push(result.data);
63
+ }
64
+
65
+ return collections;
66
+ }
67
+
68
+ /**
69
+ * Returns warnings for artifact catalog entries that declare a collection name
70
+ * not backed by any manifest in collectionsDir.
71
+ *
72
+ * This is a soft check — artifacts may be authored before a collection manifest
73
+ * exists, so this is a warning rather than an error.
74
+ */
75
+ export function validateArtifactCollectionRefs(
76
+ entries: CatalogEntry[],
77
+ collections: Collection[],
78
+ ): ValidationWarning[] {
79
+ const knownCollectionNames = new Set(collections.map((c) => c.name));
80
+ const warnings: ValidationWarning[] = [];
81
+
82
+ for (const entry of entries) {
83
+ for (const collectionRef of entry.collections) {
84
+ if (!knownCollectionNames.has(collectionRef)) {
85
+ warnings.push({
86
+ field: "collections",
87
+ message: `Artifact declares collection "${collectionRef}" but no matching manifest found in collections/`,
88
+ filePath: `${entry.path}/knowledge.md`,
89
+ });
90
+ }
91
+ }
92
+ }
93
+
94
+ return warnings;
95
+ }
96
+
97
+ /**
98
+ * Summarize collection membership across all catalog entries.
99
+ * Returns a map from collection name → array of artifact names that belong to it.
100
+ */
101
+ export function buildCollectionMembership(
102
+ entries: CatalogEntry[],
103
+ ): Map<string, string[]> {
104
+ const membership = new Map<string, string[]>();
105
+
106
+ for (const entry of entries) {
107
+ for (const collectionName of entry.collections) {
108
+ if (!membership.has(collectionName)) {
109
+ membership.set(collectionName, []);
110
+ }
111
+ membership.get(collectionName)?.push(entry.name);
112
+ }
113
+ }
114
+
115
+ return membership;
116
+ }
@@ -0,0 +1,105 @@
1
+ import type { AssetType, HarnessName } from "./schemas";
2
+
3
+ export type CompatibilityLevel = "full" | "partial" | "none";
4
+
5
+ /**
6
+ * Declares how well each asset type is supported by each harness.
7
+ *
8
+ * - "full" — the harness has a native concept for this asset type
9
+ * - "partial" — the harness can represent this type but with degraded fidelity
10
+ * - "none" — the harness has no meaningful output for this type
11
+ *
12
+ * Omitting a harness from a type's entry implies "full" (all standard harnesses
13
+ * support skills/powers/rules fully by default).
14
+ */
15
+ export const ASSET_HARNESS_COMPATIBILITY: Record<
16
+ AssetType,
17
+ Partial<Record<HarnessName, CompatibilityLevel>>
18
+ > = {
19
+ // Core types — all harnesses support these fully
20
+ skill: {},
21
+ // "power" is a deprecated alias for "skill" (see ADR-0051) — its asset-type
22
+ // compatibility should match skill's exactly, since taxonomy no longer
23
+ // carries different meaning. (Kiro's actual "power" *format* — as opposed
24
+ // to this deprecated *type* value — has no separate row here; format-level
25
+ // concerns live in format-registry.ts / adapters, not this asset-type table.)
26
+ power: {},
27
+ rule: {},
28
+
29
+ // Extended types
30
+ workflow: {
31
+ // Kiro, Copilot, and Q Developer have native workflow/agent file support
32
+ kiro: "full",
33
+ copilot: "full",
34
+ qdeveloper: "full",
35
+ "claude-code": "partial", // emitted as CLAUDE.md sections
36
+ cursor: "partial", // emitted as rule file
37
+ windsurf: "partial",
38
+ cline: "partial",
39
+ },
40
+ // This table answers a build-level question ("does the harness produce
41
+ // meaningful output for this asset type, or should getCompatibility's
42
+ // "none" tell build.ts to skip it entirely?") which is coarser than
43
+ // CAPABILITY_MATRIX[<harness>].agents in adapters/capabilities.ts (a
44
+ // feature-level question consumed by degradation/temper: "can this
45
+ // harness represent a declarative sub-agent file?"). The two should never
46
+ // *contradict* — a "full" here must not pair with "none"/"partial" there —
47
+ // but they may legitimately differ where a harness has no native agent
48
+ // format yet still emits generic, meaningful output (kiro, claude-code,
49
+ // codex all fall into this bucket: no dedicated agent surface, but the
50
+ // adapter still writes real output, hence "partial" rather than "none").
51
+ // See ADR-0050.
52
+ agent: {
53
+ kiro: "partial", // no dedicated agent format; rendered as steering/power prose
54
+ "claude-code": "partial", // no agent surface; rendered as generic CLAUDE.md prose
55
+ codex: "partial", // sub-agents exist via profiles, not declarative files
56
+ copilot: "full",
57
+ cursor: "none",
58
+ windsurf: "none",
59
+ cline: "none",
60
+ qdeveloper: "full",
61
+ },
62
+ prompt: {
63
+ // Prompts are universally representable as steering/rule content
64
+ kiro: "full",
65
+ "claude-code": "full",
66
+ codex: "full",
67
+ copilot: "full",
68
+ cursor: "full",
69
+ windsurf: "full",
70
+ cline: "full",
71
+ qdeveloper: "full",
72
+ },
73
+ template: {
74
+ // Templates are reference material — includable in any harness context
75
+ kiro: "full",
76
+ "claude-code": "full",
77
+ copilot: "partial",
78
+ cursor: "partial",
79
+ windsurf: "partial",
80
+ cline: "partial",
81
+ qdeveloper: "partial",
82
+ },
83
+ "reference-pack": {
84
+ // Reference packs are manual-inclusion only; all harnesses can host them
85
+ kiro: "full",
86
+ "claude-code": "full",
87
+ codex: "full",
88
+ copilot: "full",
89
+ cursor: "full",
90
+ windsurf: "full",
91
+ cline: "full",
92
+ qdeveloper: "full",
93
+ },
94
+ };
95
+
96
+ /**
97
+ * Returns the compatibility level for a given asset type and harness.
98
+ * Defaults to "full" if the harness is not explicitly listed for that type.
99
+ */
100
+ export function getCompatibility(
101
+ type: AssetType,
102
+ harness: HarnessName,
103
+ ): CompatibilityLevel {
104
+ return ASSET_HARNESS_COMPATIBILITY[type]?.[harness] ?? "full";
105
+ }