@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.
- package/CHANGELOG.md +410 -0
- package/LICENSE +21 -0
- package/README.md +168 -0
- package/bridge/mcp-server.cjs +14171 -0
- package/package.json +98 -0
- package/src/adapters/capabilities.ts +178 -0
- package/src/adapters/claude-code.ts +110 -0
- package/src/adapters/cline.ts +98 -0
- package/src/adapters/codex.ts +173 -0
- package/src/adapters/copilot.ts +106 -0
- package/src/adapters/cursor.ts +97 -0
- package/src/adapters/degradation.ts +95 -0
- package/src/adapters/index.ts +324 -0
- package/src/adapters/kiro-frontmatter.ts +139 -0
- package/src/adapters/kiro-inclusion.ts +86 -0
- package/src/adapters/kiro.ts +412 -0
- package/src/adapters/qdeveloper.ts +115 -0
- package/src/adapters/types.ts +81 -0
- package/src/adapters/windsurf.ts +96 -0
- package/src/admin.ts +283 -0
- package/src/asset-conventions.ts +118 -0
- package/src/attribution-backfill.ts +319 -0
- package/src/attribution-report.ts +95 -0
- package/src/attribution.ts +239 -0
- package/src/backends/github.ts +194 -0
- package/src/backends/http.ts +122 -0
- package/src/backends/index.ts +39 -0
- package/src/backends/local.ts +47 -0
- package/src/backends/s3.ts +157 -0
- package/src/backends/types.ts +59 -0
- package/src/base-cache.ts +270 -0
- package/src/browse-ui.ts +3754 -0
- package/src/browse.ts +1038 -0
- package/src/build.ts +1108 -0
- package/src/catalog.ts +204 -0
- package/src/cli-deprecated.ts +29 -0
- package/src/cli.ts +773 -0
- package/src/collection-admin.ts +287 -0
- package/src/collection-builder.ts +464 -0
- package/src/collections.ts +116 -0
- package/src/compatibility.ts +105 -0
- package/src/config.ts +743 -0
- package/src/eval/rubrics/kiro-progressive-steering.ts +841 -0
- package/src/eval.ts +1169 -0
- package/src/file-writer.ts +61 -0
- package/src/format-registry.ts +141 -0
- package/src/guild/auto-updater.ts +163 -0
- package/src/guild/backend-resolver.ts +49 -0
- package/src/guild/cli.ts +592 -0
- package/src/guild/collection-expander.ts +47 -0
- package/src/guild/global-cache.ts +247 -0
- package/src/guild/hook-generator.ts +100 -0
- package/src/guild/manifest.ts +154 -0
- package/src/guild/path-utils.ts +12 -0
- package/src/guild/sync.ts +622 -0
- package/src/guild/version-resolver.ts +42 -0
- package/src/help/metadata.ts +445 -0
- package/src/help/renderer.ts +265 -0
- package/src/help/typo-suggester.ts +25 -0
- package/src/hooks/expression.ts +493 -0
- package/src/hooks/pipeline.ts +141 -0
- package/src/import.ts +773 -0
- package/src/importers/claude-code.ts +134 -0
- package/src/importers/cline.ts +103 -0
- package/src/importers/codex.ts +140 -0
- package/src/importers/copilot.ts +103 -0
- package/src/importers/cursor.ts +105 -0
- package/src/importers/index.ts +390 -0
- package/src/importers/kiro.ts +110 -0
- package/src/importers/qdeveloper.ts +103 -0
- package/src/importers/types.ts +54 -0
- package/src/importers/windsurf.ts +104 -0
- package/src/install.ts +1005 -0
- package/src/manifest-admin.ts +306 -0
- package/src/mcp-bridge.ts +240 -0
- package/src/mutation/delta.ts +50 -0
- package/src/mutation/history.ts +66 -0
- package/src/mutation/operators.ts +524 -0
- package/src/mutation/runner.ts +332 -0
- package/src/new.ts +106 -0
- package/src/outcomes/collision.ts +127 -0
- package/src/outcomes/normalize.ts +208 -0
- package/src/outcomes/registry.ts +173 -0
- package/src/parser.ts +446 -0
- package/src/provenance-backfill-cli.ts +319 -0
- package/src/provenance-backfill.ts +520 -0
- package/src/publish.ts +354 -0
- package/src/reconcile-orchestrator.ts +502 -0
- package/src/reconcile-report-renderer.ts +176 -0
- package/src/resolve-body.ts +15 -0
- package/src/rosetta/builtins/compatibility-profiles.ts +297 -0
- package/src/rosetta/builtins/contracts.ts +1033 -0
- package/src/rosetta/builtins/pretty-printers/claude-code-native.ts +122 -0
- package/src/rosetta/builtins/pretty-printers/cline-native.ts +50 -0
- package/src/rosetta/builtins/pretty-printers/codex-native.ts +127 -0
- package/src/rosetta/builtins/pretty-printers/copilot-native.ts +50 -0
- package/src/rosetta/builtins/pretty-printers/cursor-native.ts +50 -0
- package/src/rosetta/builtins/pretty-printers/index.ts +81 -0
- package/src/rosetta/builtins/pretty-printers/kiro-native.ts +166 -0
- package/src/rosetta/builtins/pretty-printers/kiro-power.ts +108 -0
- package/src/rosetta/builtins/pretty-printers/kiro-skill.ts +88 -0
- package/src/rosetta/builtins/pretty-printers/qdeveloper-native.ts +51 -0
- package/src/rosetta/builtins/pretty-printers/superpowers.ts +97 -0
- package/src/rosetta/builtins/pretty-printers/windsurf-native.ts +50 -0
- package/src/rosetta/builtins/sources/claude-code-native.ts +348 -0
- package/src/rosetta/builtins/sources/cline-native.ts +176 -0
- package/src/rosetta/builtins/sources/codex-native.ts +343 -0
- package/src/rosetta/builtins/sources/copilot-native.ts +178 -0
- package/src/rosetta/builtins/sources/cursor-native.ts +176 -0
- package/src/rosetta/builtins/sources/index.ts +95 -0
- package/src/rosetta/builtins/sources/kiro-native.ts +462 -0
- package/src/rosetta/builtins/sources/kiro-power.ts +285 -0
- package/src/rosetta/builtins/sources/kiro-skill.ts +230 -0
- package/src/rosetta/builtins/sources/qdeveloper-native.ts +181 -0
- package/src/rosetta/builtins/sources/superpowers.ts +240 -0
- package/src/rosetta/builtins/sources/windsurf-native.ts +176 -0
- package/src/rosetta/builtins/targets/claude-code.ts +181 -0
- package/src/rosetta/builtins/targets/cline.ts +87 -0
- package/src/rosetta/builtins/targets/codex.ts +226 -0
- package/src/rosetta/builtins/targets/copilot.ts +103 -0
- package/src/rosetta/builtins/targets/cursor.ts +87 -0
- package/src/rosetta/builtins/targets/index.ts +60 -0
- package/src/rosetta/builtins/targets/kiro.ts +278 -0
- package/src/rosetta/builtins/targets/qdeveloper.ts +103 -0
- package/src/rosetta/builtins/targets/windsurf.ts +87 -0
- package/src/rosetta/canonical.ts +729 -0
- package/src/rosetta/compatibility.ts +432 -0
- package/src/rosetta/contracts.ts +329 -0
- package/src/rosetta/detector.ts +724 -0
- package/src/rosetta/diagnostics.ts +630 -0
- package/src/rosetta/engine-bootstrap.ts +103 -0
- package/src/rosetta/engine.ts +744 -0
- package/src/rosetta/index.ts +381 -0
- package/src/rosetta/inspection.ts +530 -0
- package/src/rosetta/plan.ts +448 -0
- package/src/rosetta/provenance-digest.ts +369 -0
- package/src/rosetta/reconcile.ts +812 -0
- package/src/rosetta/redaction.ts +467 -0
- package/src/rosetta/registry.ts +712 -0
- package/src/rosetta/renderers.ts +571 -0
- package/src/rosetta/request-guard.ts +335 -0
- package/src/rosetta/resolution.ts +419 -0
- package/src/rosetta/source-accounting.ts +233 -0
- package/src/rosetta/templates.ts +129 -0
- package/src/rosetta-cli.ts +717 -0
- package/src/rosetta-docs-generator.ts +793 -0
- package/src/rosetta-profiles-cli.ts +367 -0
- package/src/schemas.ts +1712 -0
- package/src/spec-coordination.ts +1141 -0
- package/src/temper.ts +747 -0
- package/src/template-bundle-loader.ts +312 -0
- package/src/template-engine.ts +53 -0
- package/src/translation-application-policy.ts +496 -0
- package/src/translation-orchestrator.ts +1013 -0
- package/src/translation-plan-applier.ts +473 -0
- package/src/tutorial.ts +305 -0
- package/src/validate.ts +1093 -0
- package/src/versioning.ts +553 -0
- package/src/wizard.ts +660 -0
- package/src/workspace.ts +237 -0
- package/templates/eval-contexts/claude-code.md.njk +6 -0
- package/templates/eval-contexts/cline.md.njk +6 -0
- package/templates/eval-contexts/copilot.md.njk +6 -0
- package/templates/eval-contexts/cursor.md.njk +6 -0
- package/templates/eval-contexts/kiro.md.njk +10 -0
- package/templates/eval-contexts/qdeveloper.md.njk +6 -0
- package/templates/eval-contexts/windsurf.md.njk +6 -0
- package/templates/harness-adapters/_base/attribution-footer.md.njk +17 -0
- package/templates/harness-adapters/_base/base.md.njk +16 -0
- package/templates/harness-adapters/claude-code/claude.md.njk +1 -0
- package/templates/harness-adapters/claude-code/mcp.json.njk +1 -0
- package/templates/harness-adapters/claude-code/settings.json.njk +1 -0
- package/templates/harness-adapters/claude-code/skill-library-index.md.njk +13 -0
- package/templates/harness-adapters/claude-code/skill.md.njk +19 -0
- package/templates/harness-adapters/cline/hook.sh.njk +4 -0
- package/templates/harness-adapters/cline/mcp.json.njk +1 -0
- package/templates/harness-adapters/cline/rule.md.njk +1 -0
- package/templates/harness-adapters/codex/agents-md.md.njk +6 -0
- package/templates/harness-adapters/codex/agents-pointer.md.njk +16 -0
- package/templates/harness-adapters/codex/skill.md.njk +27 -0
- package/templates/harness-adapters/copilot/agents.md.njk +1 -0
- package/templates/harness-adapters/copilot/instructions.md.njk +1 -0
- package/templates/harness-adapters/copilot/scoped.md.njk +6 -0
- package/templates/harness-adapters/cursor/mcp.json.njk +1 -0
- package/templates/harness-adapters/cursor/rule.md.njk +6 -0
- package/templates/harness-adapters/kiro/hook.json.njk +1 -0
- package/templates/harness-adapters/kiro/mcp.json.njk +1 -0
- package/templates/harness-adapters/kiro/power-steering.md.njk +3 -0
- package/templates/harness-adapters/kiro/power.md.njk +12 -0
- package/templates/harness-adapters/kiro/steering.md.njk +16 -0
- package/templates/harness-adapters/qdeveloper/agent.md.njk +1 -0
- package/templates/harness-adapters/qdeveloper/mcp.json.njk +1 -0
- package/templates/harness-adapters/qdeveloper/rule.md.njk +1 -0
- package/templates/harness-adapters/windsurf/mcp.json.njk +1 -0
- package/templates/harness-adapters/windsurf/rule.md.njk +1 -0
- package/templates/harness-adapters/windsurf/workflow.md.njk +1 -0
- package/templates/knowledge/hooks.yaml.njk +4 -0
- package/templates/knowledge/knowledge.md.njk +53 -0
- 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
|
+
}
|