@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,106 @@
|
|
|
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
|
+
|
|
7
|
+
export const copilotAdapter: HarnessAdapter = (
|
|
8
|
+
artifact,
|
|
9
|
+
templateEnv,
|
|
10
|
+
context?,
|
|
11
|
+
) => {
|
|
12
|
+
const files: OutputFile[] = [];
|
|
13
|
+
const warnings: AdapterWarning[] = [];
|
|
14
|
+
|
|
15
|
+
// Capability degradation checks
|
|
16
|
+
if (context) {
|
|
17
|
+
const checks: Array<{
|
|
18
|
+
capability: HarnessCapabilityName;
|
|
19
|
+
hasFeature: boolean;
|
|
20
|
+
}> = [
|
|
21
|
+
{ capability: "hooks", hasFeature: artifact.hooks.length > 0 },
|
|
22
|
+
{ capability: "mcp", hasFeature: artifact.mcpServers.length > 0 },
|
|
23
|
+
{ capability: "workflows", hasFeature: artifact.workflows.length > 0 },
|
|
24
|
+
];
|
|
25
|
+
for (const { capability, hasFeature } of checks) {
|
|
26
|
+
if (!hasFeature) continue;
|
|
27
|
+
const entry = context.capabilities[capability];
|
|
28
|
+
if (entry.support === "full") continue;
|
|
29
|
+
if (context.strict) {
|
|
30
|
+
warnings.push({
|
|
31
|
+
artifactName: artifact.name,
|
|
32
|
+
harnessName: "copilot",
|
|
33
|
+
message: `Strict mode: capability ${capability} not supported by harness copilot`,
|
|
34
|
+
});
|
|
35
|
+
return { files, warnings };
|
|
36
|
+
}
|
|
37
|
+
const degradation = applyDegradation(
|
|
38
|
+
entry.degradation ?? "inline",
|
|
39
|
+
capability,
|
|
40
|
+
artifact,
|
|
41
|
+
"copilot",
|
|
42
|
+
);
|
|
43
|
+
warnings.push(...degradation.warnings);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const harnessConfig = (artifact.frontmatter as Record<string, unknown>)[
|
|
48
|
+
"harness-config"
|
|
49
|
+
] as Record<string, unknown> | undefined;
|
|
50
|
+
const copilotConfig = (harnessConfig?.copilot ?? {}) as Record<
|
|
51
|
+
string,
|
|
52
|
+
unknown
|
|
53
|
+
>;
|
|
54
|
+
const { format } = resolveFormat("copilot", copilotConfig);
|
|
55
|
+
|
|
56
|
+
// Generate .github/copilot-instructions.md
|
|
57
|
+
const instructionsContent = renderTemplate(
|
|
58
|
+
templateEnv,
|
|
59
|
+
"copilot/instructions.md.njk",
|
|
60
|
+
{ artifact },
|
|
61
|
+
);
|
|
62
|
+
files.push({
|
|
63
|
+
relativePath: ".github/copilot-instructions.md",
|
|
64
|
+
content: instructionsContent,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Generate path-scoped instructions if file_patterns or harness-config.copilot.path-scoped
|
|
68
|
+
const filePatterns = artifact.frontmatter.file_patterns;
|
|
69
|
+
const pathScoped = copilotConfig["path-scoped"] as string[] | undefined;
|
|
70
|
+
const applyTo = pathScoped || filePatterns;
|
|
71
|
+
if (applyTo && applyTo.length > 0) {
|
|
72
|
+
const applyToStr = applyTo.join(", ");
|
|
73
|
+
const scopedContent = renderTemplate(templateEnv, "copilot/scoped.md.njk", {
|
|
74
|
+
artifact,
|
|
75
|
+
applyTo: applyToStr,
|
|
76
|
+
});
|
|
77
|
+
files.push({
|
|
78
|
+
relativePath: `.github/instructions/${artifact.name}.instructions.md`,
|
|
79
|
+
content: scopedContent,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Generate AGENTS.md when format is "agent", or when workflows exist / agents-md is true
|
|
84
|
+
if (
|
|
85
|
+
format === "agent" ||
|
|
86
|
+
artifact.workflows.length > 0 ||
|
|
87
|
+
copilotConfig["agents-md"] === true
|
|
88
|
+
) {
|
|
89
|
+
const agentsContent = renderTemplate(templateEnv, "copilot/agents.md.njk", {
|
|
90
|
+
artifact,
|
|
91
|
+
});
|
|
92
|
+
files.push({ relativePath: "AGENTS.md", content: agentsContent });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Skip hooks with warning
|
|
96
|
+
if (artifact.hooks.length > 0) {
|
|
97
|
+
warnings.push({
|
|
98
|
+
artifactName: artifact.name,
|
|
99
|
+
harnessName: "copilot",
|
|
100
|
+
message:
|
|
101
|
+
"GitHub Copilot does not support hooks; skipping all hook definitions",
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return { files, warnings };
|
|
106
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { resolveFormat } from "../format-registry";
|
|
2
|
+
import type { InclusionMode } from "../schemas";
|
|
3
|
+
import { renderTemplate } from "../template-engine";
|
|
4
|
+
import type { HarnessCapabilityName } from "./capabilities";
|
|
5
|
+
import { applyDegradation } from "./degradation";
|
|
6
|
+
import type { AdapterWarning, HarnessAdapter, OutputFile } from "./types";
|
|
7
|
+
import { buildMcpConfig } from "./types";
|
|
8
|
+
|
|
9
|
+
const CURSOR_INCLUSION_MAP: Record<InclusionMode, string> = {
|
|
10
|
+
always: "always",
|
|
11
|
+
auto: "auto",
|
|
12
|
+
fileMatch: "auto",
|
|
13
|
+
manual: "agent-requested",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const cursorAdapter: HarnessAdapter = (
|
|
17
|
+
artifact,
|
|
18
|
+
templateEnv,
|
|
19
|
+
context?,
|
|
20
|
+
) => {
|
|
21
|
+
const files: OutputFile[] = [];
|
|
22
|
+
const warnings: AdapterWarning[] = [];
|
|
23
|
+
|
|
24
|
+
// Capability degradation checks
|
|
25
|
+
if (context) {
|
|
26
|
+
const checks: Array<{
|
|
27
|
+
capability: HarnessCapabilityName;
|
|
28
|
+
hasFeature: boolean;
|
|
29
|
+
}> = [
|
|
30
|
+
{ capability: "hooks", hasFeature: artifact.hooks.length > 0 },
|
|
31
|
+
{ capability: "mcp", hasFeature: artifact.mcpServers.length > 0 },
|
|
32
|
+
{ capability: "workflows", hasFeature: artifact.workflows.length > 0 },
|
|
33
|
+
];
|
|
34
|
+
for (const { capability, hasFeature } of checks) {
|
|
35
|
+
if (!hasFeature) continue;
|
|
36
|
+
const entry = context.capabilities[capability];
|
|
37
|
+
if (entry.support === "full") continue;
|
|
38
|
+
if (context.strict) {
|
|
39
|
+
warnings.push({
|
|
40
|
+
artifactName: artifact.name,
|
|
41
|
+
harnessName: "cursor",
|
|
42
|
+
message: `Strict mode: capability ${capability} not supported by harness cursor`,
|
|
43
|
+
});
|
|
44
|
+
return { files, warnings };
|
|
45
|
+
}
|
|
46
|
+
const degradation = applyDegradation(
|
|
47
|
+
entry.degradation ?? "inline",
|
|
48
|
+
capability,
|
|
49
|
+
artifact,
|
|
50
|
+
"cursor",
|
|
51
|
+
);
|
|
52
|
+
warnings.push(...degradation.warnings);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const harnessConfig = (artifact.frontmatter as Record<string, unknown>)[
|
|
57
|
+
"harness-config"
|
|
58
|
+
] as Record<string, unknown> | undefined;
|
|
59
|
+
const cursorConfig = (harnessConfig?.cursor ?? {}) as Record<string, unknown>;
|
|
60
|
+
resolveFormat("cursor", cursorConfig);
|
|
61
|
+
|
|
62
|
+
// Map inclusion mode
|
|
63
|
+
const inclusionOverride = cursorConfig.inclusion as InclusionMode | undefined;
|
|
64
|
+
const canonicalInclusion =
|
|
65
|
+
inclusionOverride || artifact.frontmatter.inclusion;
|
|
66
|
+
const cursorInclusion = CURSOR_INCLUSION_MAP[canonicalInclusion] || "always";
|
|
67
|
+
|
|
68
|
+
// Generate .cursor/rules/<artifact>.md
|
|
69
|
+
const ruleContent = renderTemplate(templateEnv, "cursor/rule.md.njk", {
|
|
70
|
+
artifact,
|
|
71
|
+
cursorInclusion,
|
|
72
|
+
});
|
|
73
|
+
files.push({
|
|
74
|
+
relativePath: `.cursor/rules/${artifact.name}.md`,
|
|
75
|
+
content: ruleContent,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Generate .cursor/mcp.json
|
|
79
|
+
if (artifact.mcpServers.length > 0) {
|
|
80
|
+
const mcpConfig = buildMcpConfig(artifact.mcpServers);
|
|
81
|
+
const mcpContent = renderTemplate(templateEnv, "cursor/mcp.json.njk", {
|
|
82
|
+
mcpConfig,
|
|
83
|
+
});
|
|
84
|
+
files.push({ relativePath: ".cursor/mcp.json", content: mcpContent });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Skip hooks with warning
|
|
88
|
+
if (artifact.hooks.length > 0) {
|
|
89
|
+
warnings.push({
|
|
90
|
+
artifactName: artifact.name,
|
|
91
|
+
harnessName: "cursor",
|
|
92
|
+
message: "Cursor does not support hooks; skipping all hook definitions",
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return { files, warnings };
|
|
97
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CanonicalHook,
|
|
3
|
+
DegradationStrategy,
|
|
4
|
+
KnowledgeArtifact,
|
|
5
|
+
} from "../schemas";
|
|
6
|
+
import type { HarnessCapabilityName } from "./capabilities";
|
|
7
|
+
import type { AdapterWarning } from "./types";
|
|
8
|
+
|
|
9
|
+
export interface DegradationResult {
|
|
10
|
+
inlineText: string;
|
|
11
|
+
commentText: string;
|
|
12
|
+
warnings: AdapterWarning[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Render hooks as prose for inline degradation */
|
|
16
|
+
export function degradeHooksInline(
|
|
17
|
+
hooks: CanonicalHook[],
|
|
18
|
+
artifactName: string,
|
|
19
|
+
harnessName: string,
|
|
20
|
+
): DegradationResult {
|
|
21
|
+
const warnings: AdapterWarning[] = [];
|
|
22
|
+
if (hooks.length === 0) return { inlineText: "", commentText: "", warnings };
|
|
23
|
+
|
|
24
|
+
const lines = [
|
|
25
|
+
"",
|
|
26
|
+
"---",
|
|
27
|
+
"<!-- forge:degraded hooks (inline) -->",
|
|
28
|
+
"## Automated Behaviors",
|
|
29
|
+
"",
|
|
30
|
+
"The following behaviors should be applied manually since this harness does not support hooks natively:",
|
|
31
|
+
"",
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
for (const hook of hooks) {
|
|
35
|
+
const trigger = hook.event.replace(/_/g, " ");
|
|
36
|
+
const action =
|
|
37
|
+
hook.action.type === "ask_agent"
|
|
38
|
+
? hook.action.prompt
|
|
39
|
+
: `Run: \`${hook.action.command}\``;
|
|
40
|
+
lines.push(`- **When** ${trigger}: ${action}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
warnings.push({
|
|
44
|
+
artifactName,
|
|
45
|
+
harnessName,
|
|
46
|
+
message: `hooks: degraded via inline strategy (${hooks.length} hook(s) rendered as prose)`,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return { inlineText: lines.join("\n"), commentText: "", warnings };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Apply a degradation strategy for a given capability */
|
|
53
|
+
export function applyDegradation(
|
|
54
|
+
strategy: DegradationStrategy,
|
|
55
|
+
capability: HarnessCapabilityName,
|
|
56
|
+
artifact: KnowledgeArtifact,
|
|
57
|
+
harnessName: string,
|
|
58
|
+
): DegradationResult {
|
|
59
|
+
const warnings: AdapterWarning[] = [];
|
|
60
|
+
|
|
61
|
+
switch (strategy) {
|
|
62
|
+
case "inline":
|
|
63
|
+
if (capability === "hooks") {
|
|
64
|
+
return degradeHooksInline(artifact.hooks, artifact.name, harnessName);
|
|
65
|
+
}
|
|
66
|
+
warnings.push({
|
|
67
|
+
artifactName: artifact.name,
|
|
68
|
+
harnessName,
|
|
69
|
+
message: `${capability}: degraded via inline strategy`,
|
|
70
|
+
});
|
|
71
|
+
return {
|
|
72
|
+
inlineText: `\n<!-- forge:degraded ${capability} (inline) -->\n`,
|
|
73
|
+
commentText: "",
|
|
74
|
+
warnings,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
case "comment": {
|
|
78
|
+
const comment = `<!-- forge:unsupported ${capability} — this harness does not support ${capability} -->`;
|
|
79
|
+
warnings.push({
|
|
80
|
+
artifactName: artifact.name,
|
|
81
|
+
harnessName,
|
|
82
|
+
message: `${capability}: degraded via comment strategy`,
|
|
83
|
+
});
|
|
84
|
+
return { inlineText: "", commentText: comment, warnings };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
case "omit":
|
|
88
|
+
warnings.push({
|
|
89
|
+
artifactName: artifact.name,
|
|
90
|
+
harnessName,
|
|
91
|
+
message: `${capability}: omitted (not supported by ${harnessName})`,
|
|
92
|
+
});
|
|
93
|
+
return { inlineText: "", commentText: "", warnings };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter Registry — Routes harness adapters through Rosetta Stone target translators
|
|
3
|
+
*
|
|
4
|
+
* Each adapter entry wraps the corresponding target translator with template
|
|
5
|
+
* bundle loading and result mapping, preserving the existing adapter function
|
|
6
|
+
* signatures: (artifact, templateEnv, context?) => AdapterResult.
|
|
7
|
+
*
|
|
8
|
+
* ADR-RS-002: adapterRegistry remains keyed by harness. Each adapter entry
|
|
9
|
+
* calls the corresponding target translator with an in-memory template bundle
|
|
10
|
+
* and maps plans/diagnostics back to AdapterResult.
|
|
11
|
+
*
|
|
12
|
+
* ADR-RS-004: Templates are preloaded into an immutable bundle before entering
|
|
13
|
+
* the pure translation boundary.
|
|
14
|
+
*
|
|
15
|
+
* Requirements: 1.1, 6.1, 12.2, 14.5, 14.10
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { resolve } from "node:path";
|
|
19
|
+
import type nunjucks from "nunjucks";
|
|
20
|
+
import { resolveFormat } from "../format-registry";
|
|
21
|
+
import type {
|
|
22
|
+
TargetTranslator,
|
|
23
|
+
TargetTranslatorContext,
|
|
24
|
+
} from "../rosetta/registry";
|
|
25
|
+
import type { ImmutableTemplateBundle } from "../rosetta/templates";
|
|
26
|
+
import type {
|
|
27
|
+
FormatContract,
|
|
28
|
+
HarnessName,
|
|
29
|
+
KnowledgeArtifact,
|
|
30
|
+
} from "../schemas";
|
|
31
|
+
import { SUPPORTED_HARNESSES } from "../schemas";
|
|
32
|
+
import { loadTemplateBundle } from "../template-bundle-loader";
|
|
33
|
+
import type { AdapterContext, AdapterResult, HarnessAdapter } from "./types";
|
|
34
|
+
|
|
35
|
+
export type { HarnessName };
|
|
36
|
+
export { SUPPORTED_HARNESSES };
|
|
37
|
+
|
|
38
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
39
|
+
// Template Bundle Cache
|
|
40
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Cached immutable template bundle, loaded once on first use.
|
|
44
|
+
* The templates dir is inferred from the Nunjucks Environment's loader path
|
|
45
|
+
* or falls back to the standard `templates/harness-adapters` location.
|
|
46
|
+
*/
|
|
47
|
+
let cachedTemplateBundle: ImmutableTemplateBundle | null = null;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get or create the immutable template bundle.
|
|
51
|
+
* Lazily loads templates from the filesystem on first call, then returns the
|
|
52
|
+
* cached frozen bundle for all subsequent calls.
|
|
53
|
+
*/
|
|
54
|
+
function getTemplateBundle(
|
|
55
|
+
templateEnv: nunjucks.Environment,
|
|
56
|
+
): ImmutableTemplateBundle {
|
|
57
|
+
if (cachedTemplateBundle !== null) {
|
|
58
|
+
return cachedTemplateBundle;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Extract the templates directory from the Nunjucks environment's loader.
|
|
62
|
+
// The FileSystemLoader stores its search paths; use the first one.
|
|
63
|
+
let templatesDir: string;
|
|
64
|
+
const loaders = (
|
|
65
|
+
templateEnv as unknown as { loaders: Array<{ searchPaths?: string[] }> }
|
|
66
|
+
).loaders;
|
|
67
|
+
if (loaders?.[0]?.searchPaths?.[0]) {
|
|
68
|
+
templatesDir = loaders[0].searchPaths[0];
|
|
69
|
+
} else {
|
|
70
|
+
// Fallback: resolve from cwd (standard kanon layout)
|
|
71
|
+
templatesDir = resolve("templates/harness-adapters");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
cachedTemplateBundle = loadTemplateBundle(templatesDir);
|
|
75
|
+
return cachedTemplateBundle;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
79
|
+
// Format Contract Lookup (lazy to avoid circular initialization)
|
|
80
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
81
|
+
|
|
82
|
+
/** Lazily-built map from harness name to primary bidirectional/target format contract. */
|
|
83
|
+
let harnessContractMap: Map<HarnessName, FormatContract> | null = null;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Get the harness-to-contract map, building it on first call.
|
|
87
|
+
* Uses lazy initialization to avoid circular dependency issues at module load.
|
|
88
|
+
*/
|
|
89
|
+
function getHarnessContractMap(): ReadonlyMap<HarnessName, FormatContract> {
|
|
90
|
+
if (harnessContractMap !== null) {
|
|
91
|
+
return harnessContractMap;
|
|
92
|
+
}
|
|
93
|
+
// Import lazily to break the circular init chain:
|
|
94
|
+
// adapters/index → rosetta/builtins/contracts → rosetta/builtins/compatibility-profiles → adapters/capabilities
|
|
95
|
+
const { BUILTIN_FORMAT_CONTRACTS } = require("../rosetta/builtins/contracts");
|
|
96
|
+
const map = new Map<HarnessName, FormatContract>();
|
|
97
|
+
for (const contract of BUILTIN_FORMAT_CONTRACTS) {
|
|
98
|
+
if (contract.harness === null) continue;
|
|
99
|
+
if (contract.direction === "source") continue;
|
|
100
|
+
const harness = contract.harness as HarnessName;
|
|
101
|
+
// First matching bidirectional/target contract wins
|
|
102
|
+
if (!map.has(harness)) {
|
|
103
|
+
map.set(harness, contract);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
harnessContractMap = map;
|
|
107
|
+
return map;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Lazily-built target translator map from format identifiers. */
|
|
111
|
+
let targetTranslatorMap: ReadonlyMap<string, TargetTranslator> | null = null;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Get the target translator map, loading it on first call.
|
|
115
|
+
*/
|
|
116
|
+
function getTargetTranslatorMap(): ReadonlyMap<string, TargetTranslator> {
|
|
117
|
+
if (targetTranslatorMap !== null) {
|
|
118
|
+
return targetTranslatorMap;
|
|
119
|
+
}
|
|
120
|
+
const { TARGET_TRANSLATORS } = require("../rosetta/builtins/targets");
|
|
121
|
+
targetTranslatorMap = TARGET_TRANSLATORS as ReadonlyMap<
|
|
122
|
+
string,
|
|
123
|
+
TargetTranslator
|
|
124
|
+
>;
|
|
125
|
+
return targetTranslatorMap;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
129
|
+
// Result Mapping
|
|
130
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Map a TargetTranslationOutput (plan + diagnostics + degradations) back to
|
|
134
|
+
* the AdapterResult shape that existing consumers expect.
|
|
135
|
+
*
|
|
136
|
+
* - plan.outputFiles → AdapterResult.files (preserving relativePath, content, executable)
|
|
137
|
+
* - diagnostics with severity "warning" → AdapterResult.warnings
|
|
138
|
+
* - diagnostics with severity "error" → AdapterResult.errors
|
|
139
|
+
* - degradation records generate additional warnings
|
|
140
|
+
*/
|
|
141
|
+
function mapToAdapterResult(
|
|
142
|
+
output: {
|
|
143
|
+
plan: Record<string, unknown>;
|
|
144
|
+
diagnostics: readonly Record<string, unknown>[];
|
|
145
|
+
degradations: readonly Record<string, unknown>[];
|
|
146
|
+
},
|
|
147
|
+
artifactName: string,
|
|
148
|
+
harnessName: HarnessName,
|
|
149
|
+
): AdapterResult {
|
|
150
|
+
const plan = output.plan as {
|
|
151
|
+
outputFiles?: Array<{
|
|
152
|
+
relativePath: string;
|
|
153
|
+
content: string | Uint8Array;
|
|
154
|
+
executable?: boolean;
|
|
155
|
+
}>;
|
|
156
|
+
};
|
|
157
|
+
const files = (plan.outputFiles ?? []).map((f) => ({
|
|
158
|
+
relativePath: f.relativePath,
|
|
159
|
+
content:
|
|
160
|
+
typeof f.content === "string"
|
|
161
|
+
? f.content
|
|
162
|
+
: new TextDecoder().decode(f.content),
|
|
163
|
+
...(f.executable ? { executable: true } : {}),
|
|
164
|
+
}));
|
|
165
|
+
|
|
166
|
+
const warnings: AdapterResult["warnings"] = [];
|
|
167
|
+
const errors: AdapterResult["errors"] = [];
|
|
168
|
+
|
|
169
|
+
// Map diagnostics to warnings/errors
|
|
170
|
+
for (const diag of output.diagnostics) {
|
|
171
|
+
const d = diag as { severity?: string; message?: string; code?: string };
|
|
172
|
+
const message = d.message ?? d.code ?? "Unknown diagnostic";
|
|
173
|
+
|
|
174
|
+
if (d.severity === "error") {
|
|
175
|
+
errors.push({
|
|
176
|
+
artifactName,
|
|
177
|
+
harnessName,
|
|
178
|
+
message,
|
|
179
|
+
});
|
|
180
|
+
} else {
|
|
181
|
+
// "warning" and "info" both map to adapter warnings for backward compat
|
|
182
|
+
warnings.push({
|
|
183
|
+
artifactName,
|
|
184
|
+
harnessName,
|
|
185
|
+
message,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Map degradation records to warnings
|
|
191
|
+
for (const deg of output.degradations) {
|
|
192
|
+
const d = deg as {
|
|
193
|
+
capability?: string;
|
|
194
|
+
action?: string;
|
|
195
|
+
affectedValueCount?: number;
|
|
196
|
+
};
|
|
197
|
+
warnings.push({
|
|
198
|
+
artifactName,
|
|
199
|
+
harnessName,
|
|
200
|
+
message: `Degradation: ${d.capability ?? "unknown"} (${d.action ?? "omit"}, ${d.affectedValueCount ?? 0} value(s) affected)`,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
files,
|
|
206
|
+
warnings,
|
|
207
|
+
...(errors.length > 0 ? { errors } : {}),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
212
|
+
// Adapter Wrapper
|
|
213
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Create an adapter that routes through a Rosetta Stone target translator.
|
|
217
|
+
*
|
|
218
|
+
* Preserves the existing adapter signature:
|
|
219
|
+
* (artifact: KnowledgeArtifact, templateEnv: nunjucks.Environment, context?: AdapterContext) => AdapterResult
|
|
220
|
+
*
|
|
221
|
+
* During migration, each adapter entry in the registry wraps the target
|
|
222
|
+
* translator with the necessary template loading and result mapping.
|
|
223
|
+
*/
|
|
224
|
+
function createRosettaAdapter(harness: HarnessName): HarnessAdapter {
|
|
225
|
+
return (
|
|
226
|
+
artifact: KnowledgeArtifact,
|
|
227
|
+
templateEnv: nunjucks.Environment,
|
|
228
|
+
_context?: AdapterContext,
|
|
229
|
+
): AdapterResult => {
|
|
230
|
+
// Get the format contract for this harness (lazy init)
|
|
231
|
+
const contractMap = getHarnessContractMap();
|
|
232
|
+
const contract = contractMap.get(harness);
|
|
233
|
+
if (!contract) {
|
|
234
|
+
return {
|
|
235
|
+
files: [],
|
|
236
|
+
warnings: [
|
|
237
|
+
{
|
|
238
|
+
artifactName: artifact.name,
|
|
239
|
+
harnessName: harness,
|
|
240
|
+
message: `No format contract found for harness "${harness}"`,
|
|
241
|
+
},
|
|
242
|
+
],
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Resolve variant from harness-config using the same logic as before
|
|
247
|
+
const harnessConfig = (artifact.frontmatter as Record<string, unknown>)[
|
|
248
|
+
"harness-config"
|
|
249
|
+
] as Record<string, unknown> | undefined;
|
|
250
|
+
const perHarnessConfig = (harnessConfig?.[harness] ?? {}) as Record<
|
|
251
|
+
string,
|
|
252
|
+
unknown
|
|
253
|
+
>;
|
|
254
|
+
const { format: variant } = resolveFormat(harness, perHarnessConfig);
|
|
255
|
+
|
|
256
|
+
// Preload immutable template bundle (cached after first load)
|
|
257
|
+
const templates = getTemplateBundle(templateEnv);
|
|
258
|
+
|
|
259
|
+
// Build the target translator context
|
|
260
|
+
const translatorContext: TargetTranslatorContext = {
|
|
261
|
+
format: contract,
|
|
262
|
+
variant,
|
|
263
|
+
canonicalSchemaVersion: "1.0.0",
|
|
264
|
+
options: {},
|
|
265
|
+
callerContext: {},
|
|
266
|
+
templates,
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
// Look up and call the target translator (lazy init)
|
|
270
|
+
const translatorMap = getTargetTranslatorMap();
|
|
271
|
+
const translator = translatorMap.get(contract.id as unknown as string);
|
|
272
|
+
if (!translator) {
|
|
273
|
+
return {
|
|
274
|
+
files: [],
|
|
275
|
+
warnings: [
|
|
276
|
+
{
|
|
277
|
+
artifactName: artifact.name,
|
|
278
|
+
harnessName: harness,
|
|
279
|
+
message: `No target translator found for format "${contract.id}"`,
|
|
280
|
+
},
|
|
281
|
+
],
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Call the target translator with the artifact as a record
|
|
286
|
+
const output = translator(
|
|
287
|
+
artifact as unknown as Record<string, unknown>,
|
|
288
|
+
translatorContext,
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
// Map the output back to AdapterResult shape
|
|
292
|
+
return mapToAdapterResult(output, artifact.name, harness);
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
297
|
+
// Public Adapter Registry
|
|
298
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Registry mapping harness names to adapter functions.
|
|
302
|
+
*
|
|
303
|
+
* Each entry routes through the corresponding Rosetta Stone target translator,
|
|
304
|
+
* preloading immutable template bundles and mapping plans/diagnostics back to
|
|
305
|
+
* the AdapterResult shape that existing consumers expect.
|
|
306
|
+
*/
|
|
307
|
+
export const adapterRegistry: Record<HarnessName, HarnessAdapter> = {
|
|
308
|
+
kiro: createRosettaAdapter("kiro"),
|
|
309
|
+
"claude-code": createRosettaAdapter("claude-code"),
|
|
310
|
+
codex: createRosettaAdapter("codex"),
|
|
311
|
+
copilot: createRosettaAdapter("copilot"),
|
|
312
|
+
cursor: createRosettaAdapter("cursor"),
|
|
313
|
+
windsurf: createRosettaAdapter("windsurf"),
|
|
314
|
+
cline: createRosettaAdapter("cline"),
|
|
315
|
+
qdeveloper: createRosettaAdapter("qdeveloper"),
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Reset the cached template bundle. Used in tests to ensure fresh state.
|
|
320
|
+
* @internal
|
|
321
|
+
*/
|
|
322
|
+
export function _resetTemplateBundleCache(): void {
|
|
323
|
+
cachedTemplateBundle = null;
|
|
324
|
+
}
|