@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,115 @@
1
+ import { resolveFormat } from "../format-registry";
2
+ import { renderTemplate } from "../template-engine";
3
+ import type { HarnessCapabilityName } from "./capabilities";
4
+ import { applyDegradation } from "./degradation";
5
+ import type { AdapterWarning, HarnessAdapter, OutputFile } from "./types";
6
+ import { buildMcpConfig } from "./types";
7
+
8
+ export const qdeveloperAdapter: HarnessAdapter = (
9
+ artifact,
10
+ templateEnv,
11
+ context?,
12
+ ) => {
13
+ const files: OutputFile[] = [];
14
+ const warnings: AdapterWarning[] = [];
15
+
16
+ // Capability degradation checks
17
+ if (context) {
18
+ const checks: Array<{
19
+ capability: HarnessCapabilityName;
20
+ hasFeature: boolean;
21
+ }> = [
22
+ { capability: "hooks", hasFeature: artifact.hooks.length > 0 },
23
+ { capability: "mcp", hasFeature: artifact.mcpServers.length > 0 },
24
+ { capability: "workflows", hasFeature: artifact.workflows.length > 0 },
25
+ ];
26
+ for (const { capability, hasFeature } of checks) {
27
+ if (!hasFeature) continue;
28
+ const entry = context.capabilities[capability];
29
+ if (entry.support === "full") continue;
30
+ if (context.strict) {
31
+ warnings.push({
32
+ artifactName: artifact.name,
33
+ harnessName: "qdeveloper",
34
+ message: `Strict mode: capability ${capability} not supported by harness qdeveloper`,
35
+ });
36
+ return { files, warnings };
37
+ }
38
+ const degradation = applyDegradation(
39
+ entry.degradation ?? "inline",
40
+ capability,
41
+ artifact,
42
+ "qdeveloper",
43
+ );
44
+ warnings.push(...degradation.warnings);
45
+ }
46
+ }
47
+
48
+ const harnessConfig = (artifact.frontmatter as Record<string, unknown>)[
49
+ "harness-config"
50
+ ] as Record<string, unknown> | undefined;
51
+ const qdeveloperConfig = (harnessConfig?.qdeveloper ?? {}) as Record<
52
+ string,
53
+ unknown
54
+ >;
55
+ const { format } = resolveFormat("qdeveloper", qdeveloperConfig);
56
+
57
+ // Generate .q/rules/<artifact>.md when format is "rule"
58
+ if (format === "rule") {
59
+ const ruleContent = renderTemplate(templateEnv, "qdeveloper/rule.md.njk", {
60
+ artifact,
61
+ });
62
+ files.push({
63
+ relativePath: `.q/rules/${artifact.name}.md`,
64
+ content: ruleContent,
65
+ });
66
+ }
67
+
68
+ // Generate .q/agents/ files when format is "agent" or when workflows exist
69
+ if (format === "agent") {
70
+ // In agent format, produce agent file from the artifact body
71
+ const agentContent = renderTemplate(
72
+ templateEnv,
73
+ "qdeveloper/agent.md.njk",
74
+ { artifact },
75
+ );
76
+ files.push({
77
+ relativePath: `.q/agents/${artifact.name}.md`,
78
+ content: agentContent,
79
+ });
80
+ }
81
+
82
+ // Generate .q/agents/ files from workflows (regardless of format)
83
+ for (const wf of artifact.workflows) {
84
+ const agentContent = renderTemplate(
85
+ templateEnv,
86
+ "qdeveloper/agent.md.njk",
87
+ { artifact: { ...artifact, body: wf.content, workflows: [] } },
88
+ );
89
+ files.push({
90
+ relativePath: `.q/agents/${wf.filename}`,
91
+ content: agentContent,
92
+ });
93
+ }
94
+
95
+ // Generate MCP configuration
96
+ if (artifact.mcpServers.length > 0) {
97
+ const mcpConfig = buildMcpConfig(artifact.mcpServers);
98
+ const mcpContent = renderTemplate(templateEnv, "qdeveloper/mcp.json.njk", {
99
+ mcpConfig,
100
+ });
101
+ files.push({ relativePath: ".q/mcp.json", content: mcpContent });
102
+ }
103
+
104
+ // Skip hooks with warning
105
+ if (artifact.hooks.length > 0) {
106
+ warnings.push({
107
+ artifactName: artifact.name,
108
+ harnessName: "qdeveloper",
109
+ message:
110
+ "Amazon Q Developer does not support hooks; skipping all hook definitions",
111
+ });
112
+ }
113
+
114
+ return { files, warnings };
115
+ };
@@ -0,0 +1,81 @@
1
+ import type nunjucks from "nunjucks";
2
+ import type {
3
+ CapabilityEntry,
4
+ HarnessName,
5
+ KnowledgeArtifact,
6
+ McpServerDefinition,
7
+ } from "../schemas";
8
+ import { isStdioServer } from "../schemas";
9
+ import type { HarnessCapabilityName } from "./capabilities";
10
+
11
+ export interface OutputFile {
12
+ relativePath: string;
13
+ content: string;
14
+ executable?: boolean;
15
+ }
16
+
17
+ export interface AdapterWarning {
18
+ artifactName: string;
19
+ harnessName: string;
20
+ message: string;
21
+ }
22
+
23
+ export interface AdapterError {
24
+ artifactName: string;
25
+ harnessName: HarnessName;
26
+ message: string;
27
+ /** Optional: the field/path or filename that caused the error */
28
+ field?: string;
29
+ }
30
+
31
+ export interface AdapterResult {
32
+ files: OutputFile[];
33
+ warnings: AdapterWarning[];
34
+ /** Optional error channel for strict violations; unset by default so existing adapters compile unchanged */
35
+ errors?: AdapterError[];
36
+ }
37
+
38
+ export interface AdapterContext {
39
+ capabilities: Record<HarnessCapabilityName, CapabilityEntry>;
40
+ strict: boolean;
41
+ }
42
+
43
+ export type HarnessAdapter = (
44
+ artifact: KnowledgeArtifact,
45
+ templateEnv: nunjucks.Environment,
46
+ context?: AdapterContext,
47
+ ) => AdapterResult;
48
+
49
+ /**
50
+ * Build a harness-native mcpServers config object from canonical server definitions.
51
+ * Handles both stdio (command-based) and URL-based (SSE/HTTP) servers.
52
+ */
53
+ export function buildMcpConfig(
54
+ servers: McpServerDefinition[],
55
+ ): Record<string, unknown> {
56
+ const mcpConfig: Record<string, unknown> = { mcpServers: {} };
57
+ for (const server of servers) {
58
+ if (isStdioServer(server)) {
59
+ const entry: Record<string, unknown> = {
60
+ command: server.command,
61
+ args: server.args,
62
+ env: server.env,
63
+ };
64
+ if (server.timeout) entry.timeout = server.timeout;
65
+ if (server.autoApprove?.length) entry.autoApprove = server.autoApprove;
66
+ if (server.disabled !== undefined) entry.disabled = server.disabled;
67
+ (mcpConfig.mcpServers as Record<string, unknown>)[server.name] = entry;
68
+ } else {
69
+ const entry: Record<string, unknown> = {
70
+ url: server.url,
71
+ };
72
+ if (server.env && Object.keys(server.env).length > 0)
73
+ entry.env = server.env;
74
+ if (server.timeout) entry.timeout = server.timeout;
75
+ if (server.autoApprove?.length) entry.autoApprove = server.autoApprove;
76
+ if (server.disabled !== undefined) entry.disabled = server.disabled;
77
+ (mcpConfig.mcpServers as Record<string, unknown>)[server.name] = entry;
78
+ }
79
+ }
80
+ return mcpConfig;
81
+ }
@@ -0,0 +1,96 @@
1
+ import { resolveFormat } from "../format-registry";
2
+ import { renderTemplate } from "../template-engine";
3
+ import type { HarnessCapabilityName } from "./capabilities";
4
+ import { applyDegradation } from "./degradation";
5
+ import type { AdapterWarning, HarnessAdapter, OutputFile } from "./types";
6
+ import { buildMcpConfig } from "./types";
7
+
8
+ export const windsurfAdapter: HarnessAdapter = (
9
+ artifact,
10
+ templateEnv,
11
+ context?,
12
+ ) => {
13
+ const files: OutputFile[] = [];
14
+ const warnings: AdapterWarning[] = [];
15
+
16
+ // Capability degradation checks
17
+ if (context) {
18
+ const checks: Array<{
19
+ capability: HarnessCapabilityName;
20
+ hasFeature: boolean;
21
+ }> = [
22
+ { capability: "hooks", hasFeature: artifact.hooks.length > 0 },
23
+ { capability: "mcp", hasFeature: artifact.mcpServers.length > 0 },
24
+ { capability: "workflows", hasFeature: artifact.workflows.length > 0 },
25
+ ];
26
+ for (const { capability, hasFeature } of checks) {
27
+ if (!hasFeature) continue;
28
+ const entry = context.capabilities[capability];
29
+ if (entry.support === "full") continue;
30
+ if (context.strict) {
31
+ warnings.push({
32
+ artifactName: artifact.name,
33
+ harnessName: "windsurf",
34
+ message: `Strict mode: capability ${capability} not supported by harness windsurf`,
35
+ });
36
+ return { files, warnings };
37
+ }
38
+ const degradation = applyDegradation(
39
+ entry.degradation ?? "inline",
40
+ capability,
41
+ artifact,
42
+ "windsurf",
43
+ );
44
+ warnings.push(...degradation.warnings);
45
+ }
46
+ }
47
+
48
+ const harnessConfig = (artifact.frontmatter as Record<string, unknown>)[
49
+ "harness-config"
50
+ ] as Record<string, unknown> | undefined;
51
+ const windsurfConfig = (harnessConfig?.windsurf ?? {}) as Record<
52
+ string,
53
+ unknown
54
+ >;
55
+ resolveFormat("windsurf", windsurfConfig);
56
+
57
+ // Generate .windsurf/rules/<artifact>.md
58
+ const ruleContent = renderTemplate(templateEnv, "windsurf/rule.md.njk", {
59
+ artifact,
60
+ });
61
+ files.push({
62
+ relativePath: `.windsurf/rules/${artifact.name}.md`,
63
+ content: ruleContent,
64
+ });
65
+
66
+ // Copy workflows to .windsurf/workflows/
67
+ for (const wf of artifact.workflows) {
68
+ const wfContent = renderTemplate(templateEnv, "windsurf/workflow.md.njk", {
69
+ workflow: wf,
70
+ });
71
+ files.push({
72
+ relativePath: `.windsurf/workflows/${wf.filename}`,
73
+ content: wfContent,
74
+ });
75
+ }
76
+
77
+ // Generate .windsurf/mcp.json
78
+ if (artifact.mcpServers.length > 0) {
79
+ const mcpConfig = buildMcpConfig(artifact.mcpServers);
80
+ const mcpContent = renderTemplate(templateEnv, "windsurf/mcp.json.njk", {
81
+ mcpConfig,
82
+ });
83
+ files.push({ relativePath: ".windsurf/mcp.json", content: mcpContent });
84
+ }
85
+
86
+ // Skip hooks with warning
87
+ if (artifact.hooks.length > 0) {
88
+ warnings.push({
89
+ artifactName: artifact.name,
90
+ harnessName: "windsurf",
91
+ message: "Windsurf does not support hooks; skipping all hook definitions",
92
+ });
93
+ }
94
+
95
+ return { files, warnings };
96
+ };
package/src/admin.ts ADDED
@@ -0,0 +1,283 @@
1
+ import { exists, mkdir, rm, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import matter from "gray-matter";
4
+ import * as yaml from "js-yaml";
5
+ import { generateCatalog } from "./catalog";
6
+ import {
7
+ type CatalogEntry,
8
+ type Frontmatter,
9
+ FrontmatterSchema,
10
+ } from "./schemas";
11
+
12
+ const KEBAB_CASE_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/;
13
+
14
+ /** Input shape for create/update requests */
15
+ export interface ArtifactInput {
16
+ name: string;
17
+ displayName?: string;
18
+ description: string;
19
+ keywords: string[];
20
+ author: string;
21
+ version: string;
22
+ harnesses: string[];
23
+ type: string;
24
+ inclusion?: string;
25
+ categories: string[];
26
+ ecosystem: string[];
27
+ depends: string[];
28
+ enhances: string[];
29
+ body: string;
30
+ }
31
+
32
+ /**
33
+ * Converts a display name to a kebab-case artifact name.
34
+ * Lowercases, strips non-alphanumeric characters, and joins segments with hyphens.
35
+ */
36
+ export function toKebabCase(input: string): string {
37
+ return input
38
+ .toLowerCase()
39
+ .replace(/[^a-z0-9]+/g, " ")
40
+ .trim()
41
+ .split(/\s+/)
42
+ .filter((s) => s.length > 0)
43
+ .join("-");
44
+ }
45
+
46
+ /**
47
+ * Validates ArtifactInput against FrontmatterSchema and the kebab-case name pattern.
48
+ * Returns either the parsed Frontmatter data or an array of field-level errors.
49
+ */
50
+ export function validateArtifactInput(
51
+ input: ArtifactInput,
52
+ ):
53
+ | { success: true; data: Frontmatter }
54
+ | { success: false; errors: Array<{ field: string; message: string }> } {
55
+ const errors: Array<{ field: string; message: string }> = [];
56
+
57
+ // Validate name against kebab-case pattern
58
+ if (!KEBAB_CASE_PATTERN.test(input.name)) {
59
+ errors.push({
60
+ field: "name",
61
+ message:
62
+ "Name must be kebab-case (lowercase alphanumeric segments separated by hyphens)",
63
+ });
64
+ }
65
+
66
+ // Build the frontmatter object for schema validation
67
+ const frontmatterData: Record<string, unknown> = {
68
+ name: input.name,
69
+ description: input.description,
70
+ keywords: input.keywords,
71
+ author: input.author,
72
+ version: input.version,
73
+ harnesses: input.harnesses,
74
+ type: input.type,
75
+ categories: input.categories,
76
+ ecosystem: input.ecosystem,
77
+ depends: input.depends,
78
+ enhances: input.enhances,
79
+ };
80
+
81
+ if (input.displayName !== undefined) {
82
+ frontmatterData.displayName = input.displayName;
83
+ }
84
+ if (input.inclusion !== undefined) {
85
+ frontmatterData.inclusion = input.inclusion;
86
+ }
87
+
88
+ const result = FrontmatterSchema.safeParse(frontmatterData);
89
+
90
+ if (!result.success) {
91
+ for (const issue of result.error.issues) {
92
+ errors.push({
93
+ field: issue.path.join(".") || "unknown",
94
+ message: issue.message,
95
+ });
96
+ }
97
+ }
98
+
99
+ if (errors.length > 0) {
100
+ return { success: false, errors };
101
+ }
102
+
103
+ return { success: true, data: result.data as Frontmatter };
104
+ }
105
+
106
+ /**
107
+ * Return whether a scalar can remain unquoted without gray-matter changing
108
+ * its type or value when the generated frontmatter is parsed again.
109
+ */
110
+ function preservesYamlStringValue(value: string): boolean {
111
+ try {
112
+ const parsed: unknown = matter(`---\nvalue: ${value}\n---\n`).data.value;
113
+ return typeof parsed === "string" && parsed === value;
114
+ } catch {
115
+ return false;
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Remove quotes only from strings that survive a gray-matter round-trip as
121
+ * strings. Non-string values remain in the representation emitted by js-yaml.
122
+ */
123
+ function unquoteSafeYamlScalars(yamlContent: string): string {
124
+ return yamlContent
125
+ .split("\n")
126
+ .map((line: string): string => {
127
+ const match = line.match(/^(\s*(?:-\s+|[^:]+:\s+))'((?:[^']|'')*)'\s*$/);
128
+ if (!match) return line;
129
+
130
+ const [, prefix, encodedScalar] = match;
131
+ const scalar = encodedScalar.replace(/''/g, "'");
132
+ if (preservesYamlStringValue(scalar)) {
133
+ return `${prefix}${scalar}`;
134
+ }
135
+ return line;
136
+ })
137
+ .join("\n");
138
+ }
139
+
140
+ /**
141
+ * Converts Frontmatter + body into a knowledge.md string.
142
+ * Produces YAML frontmatter wrapped in `---` delimiters, followed by body content.
143
+ */
144
+ export function serializeFrontmatter(
145
+ frontmatter: Frontmatter,
146
+ body: string,
147
+ ): string {
148
+ const yamlStr = yaml.dump(frontmatter, {
149
+ lineWidth: -1,
150
+ noRefs: true,
151
+ quoteStyle: "single",
152
+ forceQuotes: true,
153
+ });
154
+ return `---\n${unquoteSafeYamlScalars(yamlStr)}---\n${body}`;
155
+ }
156
+
157
+ /**
158
+ * Creates a new artifact directory with knowledge.md, hooks.yaml, mcp-servers.yaml, and workflows/.
159
+ * Validates input, checks for conflicts, writes files, and returns the new CatalogEntry.
160
+ */
161
+ export async function createArtifact(
162
+ knowledgeDir: string,
163
+ input: ArtifactInput,
164
+ ): Promise<CatalogEntry> {
165
+ // Validate input
166
+ const validation = validateArtifactInput(input);
167
+ if (!validation.success) {
168
+ const error: Error & { type?: string; details?: unknown } = new Error(
169
+ "Validation failed",
170
+ );
171
+ error.type = "validation";
172
+ error.details = validation.errors;
173
+ throw error;
174
+ }
175
+
176
+ const artifactDir = join(knowledgeDir, input.name);
177
+
178
+ // Check for existing directory
179
+ if (await exists(artifactDir)) {
180
+ const error: Error & { type?: string } = new Error(
181
+ `Artifact '${input.name}' already exists`,
182
+ );
183
+ error.type = "conflict";
184
+ throw error;
185
+ }
186
+
187
+ // Create the artifact directory
188
+ await mkdir(artifactDir, { recursive: true });
189
+
190
+ // Write knowledge.md
191
+ const knowledgeMd = serializeFrontmatter(validation.data, input.body);
192
+ await writeFile(join(artifactDir, "knowledge.md"), knowledgeMd, "utf-8");
193
+
194
+ // Write empty hooks.yaml and mcp-servers.yaml
195
+ await writeFile(join(artifactDir, "hooks.yaml"), "[]\n", "utf-8");
196
+ await writeFile(join(artifactDir, "mcp-servers.yaml"), "[]\n", "utf-8");
197
+
198
+ // Create empty workflows/ subdirectory
199
+ await mkdir(join(artifactDir, "workflows"), { recursive: true });
200
+
201
+ // Re-scan catalog and find the new entry
202
+ const entries = await generateCatalog(knowledgeDir);
203
+ const newEntry = entries.find((e) => e.name === input.name);
204
+
205
+ if (!newEntry) {
206
+ throw new Error("Failed to find newly created artifact in catalog");
207
+ }
208
+
209
+ return newEntry;
210
+ }
211
+
212
+ /**
213
+ * Updates an existing artifact's knowledge.md (preserves all other files).
214
+ * Validates input, checks the artifact directory exists, overwrites knowledge.md,
215
+ * re-scans catalog, and returns the updated CatalogEntry.
216
+ */
217
+ export async function updateArtifact(
218
+ knowledgeDir: string,
219
+ name: string,
220
+ input: ArtifactInput,
221
+ ): Promise<CatalogEntry> {
222
+ // Validate input
223
+ const validation = validateArtifactInput(input);
224
+ if (!validation.success) {
225
+ const error: Error & { type?: string; details?: unknown } = new Error(
226
+ "Validation failed",
227
+ );
228
+ error.type = "validation";
229
+ error.details = validation.errors;
230
+ throw error;
231
+ }
232
+
233
+ const artifactDir = join(knowledgeDir, name);
234
+
235
+ // Check that the artifact directory exists
236
+ if (!(await exists(artifactDir))) {
237
+ const error: Error & { type?: string } = new Error(
238
+ `Artifact '${name}' not found`,
239
+ );
240
+ error.type = "not-found";
241
+ throw error;
242
+ }
243
+
244
+ // Overwrite only knowledge.md — preserve hooks.yaml, mcp-servers.yaml, workflows/, etc.
245
+ const knowledgeMd = serializeFrontmatter(validation.data, input.body);
246
+ await writeFile(join(artifactDir, "knowledge.md"), knowledgeMd, "utf-8");
247
+
248
+ // Re-scan catalog and find the updated entry
249
+ const entries = await generateCatalog(knowledgeDir);
250
+ const updatedEntry = entries.find((e) => e.name === input.name);
251
+
252
+ if (!updatedEntry) {
253
+ throw new Error("Failed to find updated artifact in catalog");
254
+ }
255
+
256
+ return updatedEntry;
257
+ }
258
+
259
+ /**
260
+ * Deletes an artifact directory recursively.
261
+ * Validates the directory exists, removes it entirely, and re-scans the catalog.
262
+ */
263
+ export async function deleteArtifact(
264
+ knowledgeDir: string,
265
+ name: string,
266
+ ): Promise<void> {
267
+ const artifactDir = join(knowledgeDir, name);
268
+
269
+ // Check that the artifact directory exists
270
+ if (!(await exists(artifactDir))) {
271
+ const error: Error & { type?: string } = new Error(
272
+ `Artifact '${name}' not found`,
273
+ );
274
+ error.type = "not-found";
275
+ throw error;
276
+ }
277
+
278
+ // Recursively remove the entire artifact directory
279
+ await rm(artifactDir, { recursive: true, force: true });
280
+
281
+ // Re-scan catalog to reflect the deletion
282
+ await generateCatalog(knowledgeDir);
283
+ }
@@ -0,0 +1,118 @@
1
+ import type { AssetType } from "./schemas";
2
+
3
+ /**
4
+ * Per-asset-type file conventions and validation rules.
5
+ *
6
+ * Required files must exist for an artifact to be valid.
7
+ * Optional files are recognized but not required.
8
+ * Validation rules are human-readable descriptions used to generate warnings
9
+ * in validate.ts — the actual check logic lives there, this registry just
10
+ * defines which rules apply to which types.
11
+ */
12
+ export interface AssetFileConvention {
13
+ requiredFiles: string[];
14
+ optionalFiles: string[];
15
+ /** Rule keys that validate.ts will check for this type. */
16
+ validationRuleKeys: AssetValidationRuleKey[];
17
+ }
18
+
19
+ export type AssetValidationRuleKey =
20
+ | "reference-pack-must-be-manual"
21
+ | "workflow-should-have-workflows-dir"
22
+ | "prompt-body-too-short"
23
+ | "agent-should-document-loop"
24
+ | "type-power-deprecated"
25
+ | "kiro-power-should-be-progressive"
26
+ | "kiro-power-workflow-should-be-progressive"
27
+ | "kiro-default-inclusion-informational";
28
+
29
+ /**
30
+ * Markdown heading markers that signal an artifact's body documents
31
+ * agent-loop behavior (goal, inputs/outputs, an autonomous loop), independent
32
+ * of the artifact's declared `type`. Shared by validate.ts (agent
33
+ * conventions) and temper.ts (degradation detection) so both answer "does
34
+ * this content document agent-loop behavior?" the same way.
35
+ */
36
+ const AGENT_LOOP_MARKERS: Record<string, RegExp> = {
37
+ goal: /^#{1,6}\s*(goal|objective)s?\b/im,
38
+ inputs: /^#{1,6}\s*inputs?\b/im,
39
+ outputs: /^#{1,6}\s*outputs?\b/im,
40
+ loop: /^#{1,6}\s*(autonomous\s+)?loop\b/im,
41
+ };
42
+
43
+ /**
44
+ * An artifact "documents agent-loop behavior" when its body has at least two
45
+ * of the four canonical headings (goal, inputs, outputs, loop). Two is a
46
+ * deliberately low bar — it flags likely agent content for degradation
47
+ * reporting without requiring a rigid template.
48
+ */
49
+ export function documentsAgentLoop(body: string): boolean {
50
+ const hits = Object.values(AGENT_LOOP_MARKERS).filter((marker) =>
51
+ marker.test(body),
52
+ ).length;
53
+ return hits >= 2;
54
+ }
55
+
56
+ export const ASSET_CONVENTION_RULES: Record<AssetValidationRuleKey, string> = {
57
+ "reference-pack-must-be-manual":
58
+ 'reference-pack artifacts should use inclusion: "manual" to avoid being auto-injected into every session',
59
+ "workflow-should-have-workflows-dir":
60
+ "workflow artifacts should contain at least one file in the workflows/ directory",
61
+ "prompt-body-too-short":
62
+ "prompt artifacts should have a non-trivial body (at least 50 characters)",
63
+ "agent-should-document-loop":
64
+ "agent artifacts should document at least two of: a goal/objective, inputs, outputs, and an autonomous loop — otherwise harnesses have no basis for rendering agent-specific behavior",
65
+ "type-power-deprecated":
66
+ 'type: "power" is a deprecated alias for "skill" — "power" is Kiro\'s own output-format concept, not an asset-taxonomy value. Use type: "skill" and set harness-config.kiro.format: "power" explicitly instead.',
67
+ "kiro-power-should-be-progressive":
68
+ 'Artifacts using harness-config.kiro.format: "power" should not set inclusion to "always"; POWER.md is the always-on surface, steering/ files are meant to be progressively disclosed.',
69
+ "kiro-power-workflow-should-be-progressive":
70
+ 'Artifacts using harness-config.kiro.format: "power" that ship workflow files should not set inclusion to "always"; workflow files are intended to be referenced on-demand.',
71
+ "kiro-default-inclusion-informational":
72
+ 'No Kiro inclusion mode was set explicitly. Set harness-config.kiro.inclusion to "always", "fileMatch", or "manual" to make the Progressive Steering choice explicit.',
73
+ };
74
+
75
+ export const ASSET_CONVENTIONS: Record<AssetType, AssetFileConvention> = {
76
+ skill: {
77
+ requiredFiles: ["knowledge.md"],
78
+ optionalFiles: ["hooks.yaml", "mcp-servers.yaml", "workflows/"],
79
+ validationRuleKeys: [],
80
+ },
81
+ // "power" is a deprecated alias for "skill" (see ADR-0051) — same file
82
+ // conventions, plus a deprecation warning skill doesn't need.
83
+ power: {
84
+ requiredFiles: ["knowledge.md"],
85
+ optionalFiles: ["hooks.yaml", "mcp-servers.yaml", "workflows/"],
86
+ validationRuleKeys: ["type-power-deprecated"],
87
+ },
88
+ rule: {
89
+ requiredFiles: ["knowledge.md"],
90
+ optionalFiles: [],
91
+ validationRuleKeys: [],
92
+ },
93
+ workflow: {
94
+ requiredFiles: ["knowledge.md"],
95
+ optionalFiles: ["workflows/"],
96
+ validationRuleKeys: ["workflow-should-have-workflows-dir"],
97
+ },
98
+ agent: {
99
+ requiredFiles: ["knowledge.md"],
100
+ optionalFiles: ["hooks.yaml", "mcp-servers.yaml", "workflows/"],
101
+ validationRuleKeys: ["agent-should-document-loop"],
102
+ },
103
+ prompt: {
104
+ requiredFiles: ["knowledge.md"],
105
+ optionalFiles: [],
106
+ validationRuleKeys: ["prompt-body-too-short"],
107
+ },
108
+ template: {
109
+ requiredFiles: ["knowledge.md"],
110
+ optionalFiles: [],
111
+ validationRuleKeys: [],
112
+ },
113
+ "reference-pack": {
114
+ requiredFiles: ["knowledge.md"],
115
+ optionalFiles: [],
116
+ validationRuleKeys: ["reference-pack-must-be-manual"],
117
+ },
118
+ };