@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,343 @@
1
+ /**
2
+ * Rosetta Stone — Codex Harness-Native Source Translator
3
+ *
4
+ * Translates Codex's native format (AGENTS.md, skills, TOML MCP config)
5
+ * into a canonical KnowledgeArtifact candidate.
6
+ *
7
+ * CONSTRAINTS:
8
+ * - NO filesystem, process, clock, random, Git, or network imports
9
+ * - Pure function only
10
+ *
11
+ * Requirements: 2.9, 4.1, 4.2, 4.3, 4.5, 4.6
12
+ */
13
+
14
+ import matter from "gray-matter";
15
+ import type {
16
+ McpServerDefinition,
17
+ SourceDocument,
18
+ TranslationDiagnostic,
19
+ } from "../../../schemas";
20
+ import { createDiagnostic } from "../../diagnostics";
21
+ import type {
22
+ SourceTranslationOutput,
23
+ SourceTranslatorContext,
24
+ } from "../../registry";
25
+ import {
26
+ normalizeDocumentOrder,
27
+ SourceAccountant,
28
+ } from "../../source-accounting";
29
+
30
+ // ═══════════════════════════════════════════════════════════════════════════════
31
+ // Internal helpers
32
+ // ═══════════════════════════════════════════════════════════════════════════════
33
+
34
+ function isAgentsMd(path: string): boolean {
35
+ return path === "AGENTS.md" || path.endsWith("/AGENTS.md");
36
+ }
37
+
38
+ function isSkillMd(path: string): boolean {
39
+ return path.endsWith("SKILL.md");
40
+ }
41
+
42
+ function isConfigToml(path: string): boolean {
43
+ return path.endsWith("config.toml");
44
+ }
45
+
46
+ function isMarkdownDoc(path: string): boolean {
47
+ return path.endsWith(".md");
48
+ }
49
+
50
+ /**
51
+ * Derives a kebab-case artifact name from a document path.
52
+ * For SKILL.md files, prefers the parent directory name.
53
+ * For AGENTS.md, uses "codex-agents".
54
+ */
55
+ function deriveArtifactName(path: string): string {
56
+ const segments = path.split("/");
57
+ const base = segments[segments.length - 1] ?? "";
58
+ let name = base.replace(/\.[^.]+$/, "");
59
+
60
+ // .codex/skills/<name>/SKILL.md → use the skill directory name
61
+ if (name.toLowerCase() === "skill" && segments.length >= 2) {
62
+ name = segments[segments.length - 2];
63
+ }
64
+
65
+ // AGENTS.md → stable descriptive name
66
+ if (name.toLowerCase() === "agents") {
67
+ name = "codex-agents";
68
+ }
69
+
70
+ return name
71
+ .toLowerCase()
72
+ .replace(/[^a-z0-9]+/g, "-")
73
+ .replace(/^-|-$/g, "");
74
+ }
75
+
76
+ /**
77
+ * Parse a markdown file with optional frontmatter.
78
+ */
79
+ function parseMarkdownContent(
80
+ doc: SourceDocument,
81
+ diagnostics: TranslationDiagnostic[],
82
+ ): { body: string; frontmatter: Record<string, unknown> } {
83
+ const content =
84
+ typeof doc.content === "string"
85
+ ? doc.content
86
+ : new TextDecoder().decode(doc.content);
87
+
88
+ try {
89
+ const parsed = matter(content);
90
+ return { body: parsed.content.trim(), frontmatter: { ...parsed.data } };
91
+ } catch {
92
+ diagnostics.push(
93
+ createDiagnostic("RS_CANONICAL_INVALID_FRONTMATTER", {
94
+ message: `Failed to parse frontmatter in "${doc.path}".`,
95
+ source: { path: doc.path },
96
+ }),
97
+ );
98
+ return { body: content.trim(), frontmatter: {} };
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Parse `.codex/config.toml` — extract [mcp_servers.<name>] tables.
104
+ *
105
+ * This is a deliberately small TOML reader scoped to the mcp_servers section.
106
+ * It understands `command = "..."`, `args = [...]`, and `env = { K = "v" }`.
107
+ */
108
+ function parseConfigTomlDocument(
109
+ doc: SourceDocument,
110
+ _diagnostics: TranslationDiagnostic[],
111
+ ): McpServerDefinition[] {
112
+ const content =
113
+ typeof doc.content === "string"
114
+ ? doc.content
115
+ : new TextDecoder().decode(doc.content);
116
+
117
+ const lines = content.split(/\r?\n/);
118
+ const mcpServers: McpServerDefinition[] = [];
119
+
120
+ let current: {
121
+ name: string;
122
+ command?: string;
123
+ url?: string;
124
+ args: string[];
125
+ env: Record<string, string>;
126
+ timeout?: number;
127
+ } | null = null;
128
+
129
+ const flush = () => {
130
+ if (!current) return;
131
+ if (current.command) {
132
+ mcpServers.push({
133
+ name: current.name,
134
+ transport: "stdio",
135
+ command: current.command,
136
+ args: current.args,
137
+ env: current.env,
138
+ ...(current.timeout !== undefined ? { timeout: current.timeout } : {}),
139
+ });
140
+ } else if (current.url) {
141
+ mcpServers.push({
142
+ name: current.name,
143
+ transport: "sse",
144
+ url: current.url,
145
+ env: current.env,
146
+ ...(current.timeout !== undefined ? { timeout: current.timeout } : {}),
147
+ });
148
+ }
149
+ current = null;
150
+ };
151
+
152
+ const parseStringArray = (value: string): string[] => {
153
+ const inner = value.trim().replace(/^\[/, "").replace(/\]$/, "");
154
+ if (!inner.trim()) return [];
155
+ return inner
156
+ .split(",")
157
+ .map((s) => s.trim().replace(/^["']|["']$/g, ""))
158
+ .filter((s) => s.length > 0);
159
+ };
160
+
161
+ const parseInlineTable = (value: string): Record<string, string> => {
162
+ const env: Record<string, string> = {};
163
+ const inner = value.trim().replace(/^\{/, "").replace(/\}$/, "");
164
+ for (const pair of inner.split(",")) {
165
+ const eq = pair.indexOf("=");
166
+ if (eq === -1) continue;
167
+ const k = pair
168
+ .slice(0, eq)
169
+ .trim()
170
+ .replace(/^["']|["']$/g, "");
171
+ const v = pair
172
+ .slice(eq + 1)
173
+ .trim()
174
+ .replace(/^["']|["']$/g, "");
175
+ if (k) env[k] = v;
176
+ }
177
+ return env;
178
+ };
179
+
180
+ for (const line of lines) {
181
+ const trimmed = line.trim();
182
+ if (!trimmed || trimmed.startsWith("#")) continue;
183
+
184
+ const tableMatch = trimmed.match(/^\[mcp_servers\.([^\]]+)\]$/);
185
+ if (tableMatch) {
186
+ flush();
187
+ current = {
188
+ name: tableMatch[1].replace(/^["']|["']$/g, ""),
189
+ args: [],
190
+ env: {},
191
+ };
192
+ continue;
193
+ }
194
+
195
+ // Leaving the mcp_servers section
196
+ if (trimmed.startsWith("[") && !trimmed.startsWith("[mcp_servers")) {
197
+ flush();
198
+ continue;
199
+ }
200
+
201
+ if (!current) continue;
202
+
203
+ const eq = trimmed.indexOf("=");
204
+ if (eq === -1) continue;
205
+ const key = trimmed.slice(0, eq).trim();
206
+ const value = trimmed.slice(eq + 1).trim();
207
+
208
+ if (key === "command") {
209
+ current.command = value.replace(/^["']|["']$/g, "");
210
+ } else if (key === "url") {
211
+ current.url = value.replace(/^["']|["']$/g, "");
212
+ } else if (key === "args") {
213
+ current.args = parseStringArray(value);
214
+ } else if (key === "env") {
215
+ current.env = parseInlineTable(value);
216
+ } else if (key === "startup_timeout_ms") {
217
+ const parsed = Number(value);
218
+ if (Number.isFinite(parsed)) current.timeout = parsed;
219
+ }
220
+ }
221
+ flush();
222
+
223
+ return mcpServers;
224
+ }
225
+
226
+ // ═══════════════════════════════════════════════════════════════════════════════
227
+ // Exported Translator
228
+ // ═══════════════════════════════════════════════════════════════════════════════
229
+
230
+ /**
231
+ * Codex harness-native source translator.
232
+ *
233
+ * Consumes:
234
+ * - AGENTS.md → body content becomes the artifact body
235
+ * - SKILL.md files under .codex/skills/ or .agents/skills/
236
+ * - .codex/config.toml → TOML MCP server configuration
237
+ *
238
+ * Sets type: "rule" and harnesses: ["codex"]
239
+ */
240
+ export function translateCodexNative(
241
+ documents: readonly SourceDocument[],
242
+ context: SourceTranslatorContext,
243
+ ): SourceTranslationOutput {
244
+ const accountant = new SourceAccountant();
245
+ const diagnostics: TranslationDiagnostic[] = [];
246
+ const sorted = normalizeDocumentOrder(documents);
247
+
248
+ // Determine artifact name from caller context
249
+ const artifactNameHint = context.callerContext.artifactNameHint as
250
+ | string
251
+ | undefined;
252
+
253
+ // Classify documents
254
+ const agentsMdDocs = sorted.filter((d) => isAgentsMd(d.path));
255
+ const skillDocs = sorted.filter(
256
+ (d) => isSkillMd(d.path) && !isAgentsMd(d.path),
257
+ );
258
+ const tomlDocs = sorted.filter((d) => isConfigToml(d.path));
259
+ // Other markdown docs that aren't AGENTS.md or SKILL.md
260
+ const otherMdDocs = sorted.filter(
261
+ (d) =>
262
+ isMarkdownDoc(d.path) &&
263
+ !isAgentsMd(d.path) &&
264
+ !isSkillMd(d.path) &&
265
+ !isConfigToml(d.path),
266
+ );
267
+
268
+ // Pick the primary document: prefer AGENTS.md, fall back to SKILL.md
269
+ const primaryDoc = agentsMdDocs[0] ?? skillDocs[0];
270
+
271
+ // Handle missing primary file
272
+ if (!primaryDoc) {
273
+ diagnostics.push(
274
+ createDiagnostic("RS_CANONICAL_MISSING_KNOWLEDGE_MD", {
275
+ formatId: context.format.id,
276
+ message: "No AGENTS.md or SKILL.md file found in the document set.",
277
+ }),
278
+ );
279
+ return {
280
+ diagnostics,
281
+ consumedPaths: accountant.getConsumedPaths(),
282
+ preservedPaths: accountant.getPreservedPaths(),
283
+ };
284
+ }
285
+
286
+ // Parse primary markdown
287
+ const { body, frontmatter } = parseMarkdownContent(primaryDoc, diagnostics);
288
+
289
+ accountant.consume(primaryDoc.path);
290
+ accountant.mapField(primaryDoc.path, "content", "body");
291
+
292
+ // Mark remaining AGENTS.md and SKILL.md as preserved
293
+ for (let i = 1; i < agentsMdDocs.length; i++) {
294
+ accountant.preserve(agentsMdDocs[i].path);
295
+ }
296
+ for (const skillDoc of skillDocs) {
297
+ if (skillDoc.path !== primaryDoc.path) {
298
+ accountant.preserve(skillDoc.path);
299
+ }
300
+ }
301
+
302
+ // Parse TOML MCP config
303
+ const allMcpServers: McpServerDefinition[] = [];
304
+ for (const tomlDoc of tomlDocs) {
305
+ const servers = parseConfigTomlDocument(tomlDoc, diagnostics);
306
+ allMcpServers.push(...servers);
307
+ accountant.consume(tomlDoc.path);
308
+ accountant.mapField(tomlDoc.path, "mcp_servers", "mcpServers");
309
+ }
310
+
311
+ // Preserve other markdown docs
312
+ for (const otherDoc of otherMdDocs) {
313
+ accountant.preserve(otherDoc.path);
314
+ }
315
+
316
+ // Derive artifact name
317
+ const name = artifactNameHint ?? deriveArtifactName(primaryDoc.path);
318
+
319
+ // Build the canonical candidate
320
+ const candidate: Record<string, unknown> = {
321
+ name,
322
+ frontmatter: {
323
+ name,
324
+ ...frontmatter,
325
+ type: "rule",
326
+ harnesses: ["codex"],
327
+ },
328
+ body,
329
+ hooks: [],
330
+ mcpServers: allMcpServers,
331
+ workflows: [],
332
+ sourcePath: primaryDoc.path,
333
+ extraFields: {},
334
+ bodyOverrides: {},
335
+ };
336
+
337
+ return {
338
+ candidate,
339
+ diagnostics,
340
+ consumedPaths: accountant.getConsumedPaths(),
341
+ preservedPaths: accountant.getPreservedPaths(),
342
+ };
343
+ }
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Rosetta Stone — Copilot Harness-Native Source Translator
3
+ *
4
+ * Translates GitHub Copilot's native format (.github/copilot-instructions.md,
5
+ * agent files) into a canonical KnowledgeArtifact candidate.
6
+ *
7
+ * CONSTRAINTS:
8
+ * - NO filesystem, process, clock, random, Git, or network imports
9
+ * - Pure function only
10
+ *
11
+ * Requirements: 2.9, 4.1, 4.2, 4.3, 4.5, 4.6
12
+ */
13
+
14
+ import matter from "gray-matter";
15
+ import type { SourceDocument, TranslationDiagnostic } from "../../../schemas";
16
+ import { createDiagnostic } from "../../diagnostics";
17
+ import type {
18
+ SourceTranslationOutput,
19
+ SourceTranslatorContext,
20
+ } from "../../registry";
21
+ import {
22
+ normalizeDocumentOrder,
23
+ SourceAccountant,
24
+ } from "../../source-accounting";
25
+
26
+ // ═══════════════════════════════════════════════════════════════════════════════
27
+ // Internal helpers
28
+ // ═══════════════════════════════════════════════════════════════════════════════
29
+
30
+ /**
31
+ * Checks if a document is a Copilot instructions file.
32
+ */
33
+ function isCopilotInstructions(path: string): boolean {
34
+ return (
35
+ path === ".github/copilot-instructions.md" ||
36
+ path.endsWith("/copilot-instructions.md")
37
+ );
38
+ }
39
+
40
+ /**
41
+ * Checks if a document is a Copilot agent file.
42
+ */
43
+ function isCopilotAgent(path: string): boolean {
44
+ return path.includes(".github/copilot/") && path.endsWith(".md");
45
+ }
46
+
47
+ /**
48
+ * Checks if a document is a primary Copilot file (instructions or agent).
49
+ */
50
+ function isPrimaryCopilotFile(path: string): boolean {
51
+ return isCopilotInstructions(path) || isCopilotAgent(path);
52
+ }
53
+
54
+ /**
55
+ * Derives a kebab-case artifact name from a document path.
56
+ */
57
+ function deriveArtifactName(path: string): string {
58
+ const segments = path.split("/");
59
+ const base = segments[segments.length - 1] ?? "";
60
+ const name = base.replace(/\.[^.]+$/, "");
61
+ return name
62
+ .toLowerCase()
63
+ .replace(/[^a-z0-9]+/g, "-")
64
+ .replace(/^-|-$/g, "");
65
+ }
66
+
67
+ // ═══════════════════════════════════════════════════════════════════════════════
68
+ // Exported Translator
69
+ // ═══════════════════════════════════════════════════════════════════════════════
70
+
71
+ /**
72
+ * Copilot harness-native source translator.
73
+ *
74
+ * Consumes:
75
+ * - .github/copilot-instructions.md → body content becomes the artifact body
76
+ * - .github/copilot/*.md → agent instruction files
77
+ *
78
+ * Sets type: "rule" and harnesses: ["copilot"]
79
+ */
80
+ export function translateCopilotNative(
81
+ documents: readonly SourceDocument[],
82
+ context: SourceTranslatorContext,
83
+ ): SourceTranslationOutput {
84
+ const accountant = new SourceAccountant();
85
+ const diagnostics: TranslationDiagnostic[] = [];
86
+ const sorted = normalizeDocumentOrder(documents);
87
+
88
+ // Determine artifact name from caller context
89
+ const artifactNameHint = context.callerContext.artifactNameHint as
90
+ | string
91
+ | undefined;
92
+
93
+ // Classify documents — prefer instructions file, fall back to agent files
94
+ const instructionDocs = sorted.filter((d) => isCopilotInstructions(d.path));
95
+ const agentDocs = sorted.filter(
96
+ (d) => isCopilotAgent(d.path) && !isCopilotInstructions(d.path),
97
+ );
98
+
99
+ const primaryDoc = instructionDocs[0] ?? agentDocs[0];
100
+
101
+ // Handle missing primary file
102
+ if (!primaryDoc) {
103
+ diagnostics.push(
104
+ createDiagnostic("RS_CANONICAL_MISSING_KNOWLEDGE_MD", {
105
+ formatId: context.format.id,
106
+ message:
107
+ "No Copilot instructions or agent file found in the document set.",
108
+ }),
109
+ );
110
+ return {
111
+ diagnostics,
112
+ consumedPaths: accountant.getConsumedPaths(),
113
+ preservedPaths: accountant.getPreservedPaths(),
114
+ };
115
+ }
116
+
117
+ // Parse primary markdown
118
+ const content =
119
+ typeof primaryDoc.content === "string"
120
+ ? primaryDoc.content
121
+ : new TextDecoder().decode(primaryDoc.content);
122
+
123
+ let frontmatterData: Record<string, unknown> = {};
124
+ let body = "";
125
+
126
+ try {
127
+ const parsed = matter(content);
128
+ frontmatterData = { ...parsed.data };
129
+ body = parsed.content.trim();
130
+ } catch {
131
+ diagnostics.push(
132
+ createDiagnostic("RS_CANONICAL_INVALID_FRONTMATTER", {
133
+ formatId: context.format.id,
134
+ message: `Failed to parse frontmatter in "${primaryDoc.path}".`,
135
+ source: { path: primaryDoc.path },
136
+ }),
137
+ );
138
+ body = content.trim();
139
+ }
140
+
141
+ accountant.consume(primaryDoc.path);
142
+ accountant.mapField(primaryDoc.path, "content", "body");
143
+
144
+ // Mark remaining files as preserved
145
+ for (const doc of sorted) {
146
+ if (doc.path !== primaryDoc.path && isPrimaryCopilotFile(doc.path)) {
147
+ accountant.preserve(doc.path);
148
+ }
149
+ }
150
+
151
+ // Derive artifact name
152
+ const name = artifactNameHint ?? deriveArtifactName(primaryDoc.path);
153
+
154
+ // Build the canonical candidate
155
+ const candidate: Record<string, unknown> = {
156
+ name,
157
+ frontmatter: {
158
+ name,
159
+ ...frontmatterData,
160
+ type: "rule",
161
+ harnesses: ["copilot"],
162
+ },
163
+ body,
164
+ hooks: [],
165
+ mcpServers: [],
166
+ workflows: [],
167
+ sourcePath: primaryDoc.path,
168
+ extraFields: {},
169
+ bodyOverrides: {},
170
+ };
171
+
172
+ return {
173
+ candidate,
174
+ diagnostics,
175
+ consumedPaths: accountant.getConsumedPaths(),
176
+ preservedPaths: accountant.getPreservedPaths(),
177
+ };
178
+ }
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Rosetta Stone — Cursor Harness-Native Source Translator
3
+ *
4
+ * Translates Cursor's native format (.cursor/rules/*.mdc or .cursorrules)
5
+ * into a canonical KnowledgeArtifact candidate.
6
+ *
7
+ * CONSTRAINTS:
8
+ * - NO filesystem, process, clock, random, Git, or network imports
9
+ * - Pure function only
10
+ *
11
+ * Requirements: 2.9, 4.1, 4.2, 4.3, 4.5, 4.6
12
+ */
13
+
14
+ import matter from "gray-matter";
15
+ import type { SourceDocument, TranslationDiagnostic } from "../../../schemas";
16
+ import { createDiagnostic } from "../../diagnostics";
17
+ import type {
18
+ SourceTranslationOutput,
19
+ SourceTranslatorContext,
20
+ } from "../../registry";
21
+ import {
22
+ normalizeDocumentOrder,
23
+ SourceAccountant,
24
+ } from "../../source-accounting";
25
+
26
+ // ═══════════════════════════════════════════════════════════════════════════════
27
+ // Internal helpers
28
+ // ═══════════════════════════════════════════════════════════════════════════════
29
+
30
+ /**
31
+ * Checks if a document is a .cursorrules file.
32
+ */
33
+ function isCursorRulesRoot(path: string): boolean {
34
+ return path === ".cursorrules" || path.endsWith("/.cursorrules");
35
+ }
36
+
37
+ /**
38
+ * Checks if a document is a Cursor rule file (.cursor/rules/*.mdc or .md).
39
+ */
40
+ function isCursorRuleFile(path: string): boolean {
41
+ return (
42
+ (path.includes(".cursor/rules/") || path.includes(".cursor/rules\\")) &&
43
+ (path.endsWith(".mdc") || path.endsWith(".md"))
44
+ );
45
+ }
46
+
47
+ /**
48
+ * Checks if a document is any recognized Cursor file.
49
+ */
50
+ function isCursorFile(path: string): boolean {
51
+ return isCursorRulesRoot(path) || isCursorRuleFile(path);
52
+ }
53
+
54
+ /**
55
+ * Derives a kebab-case artifact name from a document path.
56
+ */
57
+ function deriveArtifactName(path: string): string {
58
+ const segments = path.split("/");
59
+ const base = segments[segments.length - 1] ?? "";
60
+ const name = base.replace(/\.(mdc|md)$/, "");
61
+ return name
62
+ .toLowerCase()
63
+ .replace(/[^a-z0-9]+/g, "-")
64
+ .replace(/^-|-$/g, "");
65
+ }
66
+
67
+ // ═══════════════════════════════════════════════════════════════════════════════
68
+ // Exported Translator
69
+ // ═══════════════════════════════════════════════════════════════════════════════
70
+
71
+ /**
72
+ * Cursor harness-native source translator.
73
+ *
74
+ * Consumes:
75
+ * - .cursorrules → body content becomes the artifact body
76
+ * - .cursor/rules/*.mdc → rule file body content
77
+ *
78
+ * Sets type: "rule" and harnesses: ["cursor"]
79
+ */
80
+ export function translateCursorNative(
81
+ documents: readonly SourceDocument[],
82
+ context: SourceTranslatorContext,
83
+ ): SourceTranslationOutput {
84
+ const accountant = new SourceAccountant();
85
+ const diagnostics: TranslationDiagnostic[] = [];
86
+ const sorted = normalizeDocumentOrder(documents);
87
+
88
+ // Determine artifact name from caller context
89
+ const artifactNameHint = context.callerContext.artifactNameHint as
90
+ | string
91
+ | undefined;
92
+
93
+ // Classify documents — prefer .cursorrules, fall back to rule files
94
+ const rootRuleDocs = sorted.filter((d) => isCursorRulesRoot(d.path));
95
+ const ruleDirDocs = sorted.filter((d) => isCursorRuleFile(d.path));
96
+
97
+ const primaryDoc = rootRuleDocs[0] ?? ruleDirDocs[0];
98
+
99
+ // Handle missing primary file
100
+ if (!primaryDoc) {
101
+ diagnostics.push(
102
+ createDiagnostic("RS_CANONICAL_MISSING_KNOWLEDGE_MD", {
103
+ formatId: context.format.id,
104
+ message:
105
+ "No .cursorrules or .cursor/rules/ file found in the document set.",
106
+ }),
107
+ );
108
+ return {
109
+ diagnostics,
110
+ consumedPaths: accountant.getConsumedPaths(),
111
+ preservedPaths: accountant.getPreservedPaths(),
112
+ };
113
+ }
114
+
115
+ // Parse primary file
116
+ const content =
117
+ typeof primaryDoc.content === "string"
118
+ ? primaryDoc.content
119
+ : new TextDecoder().decode(primaryDoc.content);
120
+
121
+ let frontmatterData: Record<string, unknown> = {};
122
+ let body = "";
123
+
124
+ try {
125
+ const parsed = matter(content);
126
+ frontmatterData = { ...parsed.data };
127
+ body = parsed.content.trim();
128
+ } catch {
129
+ diagnostics.push(
130
+ createDiagnostic("RS_CANONICAL_INVALID_FRONTMATTER", {
131
+ formatId: context.format.id,
132
+ message: `Failed to parse frontmatter in "${primaryDoc.path}".`,
133
+ source: { path: primaryDoc.path },
134
+ }),
135
+ );
136
+ body = content.trim();
137
+ }
138
+
139
+ accountant.consume(primaryDoc.path);
140
+ accountant.mapField(primaryDoc.path, "content", "body");
141
+
142
+ // Mark remaining files as preserved
143
+ for (const doc of sorted) {
144
+ if (doc.path !== primaryDoc.path && isCursorFile(doc.path)) {
145
+ accountant.preserve(doc.path);
146
+ }
147
+ }
148
+
149
+ // Derive artifact name
150
+ const name = artifactNameHint ?? deriveArtifactName(primaryDoc.path);
151
+
152
+ // Build the canonical candidate
153
+ const candidate: Record<string, unknown> = {
154
+ name,
155
+ frontmatter: {
156
+ name,
157
+ ...frontmatterData,
158
+ type: "rule",
159
+ harnesses: ["cursor"],
160
+ },
161
+ body,
162
+ hooks: [],
163
+ mcpServers: [],
164
+ workflows: [],
165
+ sourcePath: primaryDoc.path,
166
+ extraFields: {},
167
+ bodyOverrides: {},
168
+ };
169
+
170
+ return {
171
+ candidate,
172
+ diagnostics,
173
+ consumedPaths: accountant.getConsumedPaths(),
174
+ preservedPaths: accountant.getPreservedPaths(),
175
+ };
176
+ }