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