@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,139 @@
|
|
|
1
|
+
import matter from "gray-matter";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { KiroProgressiveInclusionSchema } from "../schemas";
|
|
4
|
+
import type { KiroInclusionMode } from "./kiro-inclusion";
|
|
5
|
+
|
|
6
|
+
// --- Public types ---
|
|
7
|
+
|
|
8
|
+
export interface KiroSteeringFrontmatter {
|
|
9
|
+
inclusion: KiroInclusionMode;
|
|
10
|
+
fileMatchPattern?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ParseOk {
|
|
14
|
+
ok: true;
|
|
15
|
+
frontmatter: KiroSteeringFrontmatter | null; // null = no frontmatter block present
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ParseErr {
|
|
19
|
+
ok: false;
|
|
20
|
+
filePath: string;
|
|
21
|
+
approxLine: number;
|
|
22
|
+
message: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type ParseResult = ParseOk | ParseErr;
|
|
26
|
+
|
|
27
|
+
// --- Internal validation schema ---
|
|
28
|
+
|
|
29
|
+
const SteeringFrontmatterSchema = z.object({
|
|
30
|
+
inclusion: KiroProgressiveInclusionSchema,
|
|
31
|
+
fileMatchPattern: z.string().min(1).optional(),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// --- Parser ---
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Parse a Kiro steering file's leading YAML frontmatter.
|
|
38
|
+
*
|
|
39
|
+
* Returns `ParseOk` with `frontmatter: null` when:
|
|
40
|
+
* - No frontmatter block is present
|
|
41
|
+
* - Frontmatter is present but `inclusion` is missing or invalid
|
|
42
|
+
*
|
|
43
|
+
* Returns `ParseErr` when gray-matter/js-yaml cannot parse the YAML at all.
|
|
44
|
+
*/
|
|
45
|
+
export function parseKiroSteeringFile(
|
|
46
|
+
content: string,
|
|
47
|
+
filePath: string,
|
|
48
|
+
): ParseResult {
|
|
49
|
+
try {
|
|
50
|
+
const parsed = matter(content);
|
|
51
|
+
|
|
52
|
+
// No frontmatter block present.
|
|
53
|
+
// Note: gray-matter caches results and may return `matter: undefined` on
|
|
54
|
+
// repeated calls with the same content, even when frontmatter IS present.
|
|
55
|
+
// We detect "no frontmatter" by checking whether the parsed data object is
|
|
56
|
+
// empty AND the raw content does not start with a `---` delimiter.
|
|
57
|
+
const hasFrontmatterDelimiter = content.trimStart().startsWith("---");
|
|
58
|
+
if (!hasFrontmatterDelimiter || Object.keys(parsed.data).length === 0) {
|
|
59
|
+
return { ok: true, frontmatter: null };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Validate the subset of fields we care about
|
|
63
|
+
const result = SteeringFrontmatterSchema.safeParse(parsed.data);
|
|
64
|
+
if (!result.success) {
|
|
65
|
+
// Valid YAML but invalid/missing inclusion → treat as null frontmatter
|
|
66
|
+
return { ok: true, frontmatter: null };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const fm: KiroSteeringFrontmatter = {
|
|
70
|
+
inclusion: result.data.inclusion,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// Only include fileMatchPattern when inclusion is "fileMatch"
|
|
74
|
+
if (result.data.inclusion === "fileMatch" && result.data.fileMatchPattern) {
|
|
75
|
+
fm.fileMatchPattern = result.data.fileMatchPattern;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return { ok: true, frontmatter: fm };
|
|
79
|
+
} catch (err: unknown) {
|
|
80
|
+
// gray-matter wraps js-yaml errors; try to extract line info
|
|
81
|
+
let approxLine = 1;
|
|
82
|
+
let message = "Failed to parse YAML frontmatter";
|
|
83
|
+
|
|
84
|
+
if (isYAMLException(err)) {
|
|
85
|
+
if (err.mark && typeof err.mark.line === "number") {
|
|
86
|
+
// js-yaml mark.line is 0-indexed
|
|
87
|
+
approxLine = err.mark.line + 1;
|
|
88
|
+
}
|
|
89
|
+
message = err.reason || err.message || message;
|
|
90
|
+
} else if (err instanceof Error) {
|
|
91
|
+
message = err.message;
|
|
92
|
+
// Attempt to extract line from nested cause
|
|
93
|
+
const cause = (err as { cause?: unknown }).cause;
|
|
94
|
+
if (
|
|
95
|
+
isYAMLException(cause) &&
|
|
96
|
+
cause.mark &&
|
|
97
|
+
typeof cause.mark.line === "number"
|
|
98
|
+
) {
|
|
99
|
+
approxLine = cause.mark.line + 1;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return { ok: false, filePath, approxLine, message };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// --- Pretty-printer ---
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Pretty-print just the inclusion and fileMatchPattern fields as a
|
|
111
|
+
* Kiro-compatible frontmatter block.
|
|
112
|
+
*
|
|
113
|
+
* Suppresses `fileMatchPattern` when `inclusion !== "fileMatch"` regardless
|
|
114
|
+
* of caller input.
|
|
115
|
+
*/
|
|
116
|
+
export function printKiroFrontmatter(fm: KiroSteeringFrontmatter): string {
|
|
117
|
+
let block = "---\n";
|
|
118
|
+
block += `inclusion: ${fm.inclusion}\n`;
|
|
119
|
+
|
|
120
|
+
if (fm.inclusion === "fileMatch" && fm.fileMatchPattern) {
|
|
121
|
+
block += `fileMatchPattern: "${fm.fileMatchPattern}"\n`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
block += "---\n";
|
|
125
|
+
return block;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// --- Helpers ---
|
|
129
|
+
|
|
130
|
+
interface YAMLExceptionLike {
|
|
131
|
+
mark?: { line?: number };
|
|
132
|
+
reason?: string;
|
|
133
|
+
message?: string;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function isYAMLException(err: unknown): err is YAMLExceptionLike {
|
|
137
|
+
if (!err || typeof err !== "object") return false;
|
|
138
|
+
return "mark" in err || "reason" in err;
|
|
139
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { InclusionMode, KnowledgeArtifact } from "../schemas";
|
|
2
|
+
|
|
3
|
+
export type KiroInclusionMode = "always" | "fileMatch" | "manual";
|
|
4
|
+
|
|
5
|
+
export type KiroInclusionSource =
|
|
6
|
+
| "harness-config" // resolved from harness-config.kiro.inclusion
|
|
7
|
+
| "top-level" // resolved from frontmatter.inclusion
|
|
8
|
+
| "default"; // no value set; defaulted to "always"
|
|
9
|
+
|
|
10
|
+
export interface ResolvedKiroInclusion {
|
|
11
|
+
mode: KiroInclusionMode;
|
|
12
|
+
fileMatchPattern: string | undefined; // always undefined unless mode === "fileMatch"
|
|
13
|
+
source: KiroInclusionSource;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const VALID_KIRO_MODES: ReadonlySet<string> = new Set([
|
|
17
|
+
"always",
|
|
18
|
+
"fileMatch",
|
|
19
|
+
"manual",
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Resolves the effective Kiro inclusion mode for an artifact.
|
|
24
|
+
*
|
|
25
|
+
* Precedence:
|
|
26
|
+
* 1. harness-config.kiro.inclusion (if valid KiroInclusionMode)
|
|
27
|
+
* 2. top-level frontmatter.inclusion (if one of always|fileMatch|manual; "auto" treated as unset)
|
|
28
|
+
* 3. default: "always"
|
|
29
|
+
*
|
|
30
|
+
* fileMatchPattern is resolved only when mode === "fileMatch",
|
|
31
|
+
* from harness-config.kiro.fileMatchPattern; otherwise undefined.
|
|
32
|
+
*/
|
|
33
|
+
export function resolveKiroInclusion(
|
|
34
|
+
artifact: KnowledgeArtifact,
|
|
35
|
+
): ResolvedKiroInclusion {
|
|
36
|
+
const harnessConfig = artifact.frontmatter["harness-config"] as
|
|
37
|
+
| Record<string, Record<string, unknown>>
|
|
38
|
+
| undefined;
|
|
39
|
+
|
|
40
|
+
const kiroConfig = harnessConfig?.kiro;
|
|
41
|
+
|
|
42
|
+
// Precedence 1: harness-config.kiro.inclusion
|
|
43
|
+
if (kiroConfig && typeof kiroConfig === "object") {
|
|
44
|
+
const hcInclusion = kiroConfig.inclusion;
|
|
45
|
+
if (typeof hcInclusion === "string" && VALID_KIRO_MODES.has(hcInclusion)) {
|
|
46
|
+
const mode = hcInclusion as KiroInclusionMode;
|
|
47
|
+
return {
|
|
48
|
+
mode,
|
|
49
|
+
fileMatchPattern:
|
|
50
|
+
mode === "fileMatch"
|
|
51
|
+
? resolveFileMatchPattern(kiroConfig)
|
|
52
|
+
: undefined,
|
|
53
|
+
source: "harness-config",
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Precedence 2: top-level frontmatter.inclusion (treat "auto" as unset)
|
|
59
|
+
const topLevelInclusion: InclusionMode = artifact.frontmatter.inclusion;
|
|
60
|
+
if (VALID_KIRO_MODES.has(topLevelInclusion)) {
|
|
61
|
+
const mode = topLevelInclusion as KiroInclusionMode;
|
|
62
|
+
return {
|
|
63
|
+
mode,
|
|
64
|
+
fileMatchPattern:
|
|
65
|
+
mode === "fileMatch" ? resolveFileMatchPattern(kiroConfig) : undefined,
|
|
66
|
+
source: "top-level",
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Precedence 3: default
|
|
71
|
+
return {
|
|
72
|
+
mode: "always",
|
|
73
|
+
fileMatchPattern: undefined,
|
|
74
|
+
source: "default",
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function resolveFileMatchPattern(
|
|
79
|
+
kiroConfig: Record<string, unknown> | undefined,
|
|
80
|
+
): string | undefined {
|
|
81
|
+
if (!kiroConfig || typeof kiroConfig !== "object") return undefined;
|
|
82
|
+
const pattern = kiroConfig.fileMatchPattern;
|
|
83
|
+
return typeof pattern === "string" && pattern.length > 0
|
|
84
|
+
? pattern
|
|
85
|
+
: undefined;
|
|
86
|
+
}
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import { resolveFormat } from "../format-registry";
|
|
2
|
+
import {
|
|
3
|
+
BUILTIN_PREDICATES,
|
|
4
|
+
type ExprNode,
|
|
5
|
+
type ParsedExpression,
|
|
6
|
+
parseExpression,
|
|
7
|
+
} from "../hooks/expression";
|
|
8
|
+
import type { CanonicalEvent, CanonicalHook } from "../schemas";
|
|
9
|
+
import { renderTemplate } from "../template-engine";
|
|
10
|
+
import type { HarnessCapabilityName } from "./capabilities";
|
|
11
|
+
import { applyDegradation } from "./degradation";
|
|
12
|
+
import { parseKiroSteeringFile } from "./kiro-frontmatter";
|
|
13
|
+
import {
|
|
14
|
+
type ResolvedKiroInclusion,
|
|
15
|
+
resolveKiroInclusion,
|
|
16
|
+
} from "./kiro-inclusion";
|
|
17
|
+
import type {
|
|
18
|
+
AdapterError,
|
|
19
|
+
AdapterWarning,
|
|
20
|
+
HarnessAdapter,
|
|
21
|
+
OutputFile,
|
|
22
|
+
} from "./types";
|
|
23
|
+
import { buildMcpConfig } from "./types";
|
|
24
|
+
|
|
25
|
+
const KIRO_EVENT_MAP: Record<CanonicalEvent, string> = {
|
|
26
|
+
file_edited: "fileEdited",
|
|
27
|
+
file_created: "fileCreated",
|
|
28
|
+
file_deleted: "fileDeleted",
|
|
29
|
+
agent_stop: "agentStop",
|
|
30
|
+
prompt_submit: "promptSubmit",
|
|
31
|
+
pre_tool_use: "preToolUse",
|
|
32
|
+
post_tool_use: "postToolUse",
|
|
33
|
+
pre_task: "preTaskExecution",
|
|
34
|
+
post_task: "postTaskExecution",
|
|
35
|
+
user_triggered: "userTriggered",
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** Fixed natural-language phrasings for the built-in predicates (Req 3.6). */
|
|
39
|
+
const BUILTIN_PREDICATE_PHRASING: Record<
|
|
40
|
+
(typeof BUILTIN_PREDICATES)[number],
|
|
41
|
+
{ positive: string; negative: string }
|
|
42
|
+
> = {
|
|
43
|
+
tests_pass: {
|
|
44
|
+
positive: "Confirm the test suite passes",
|
|
45
|
+
negative: "Confirm the test suite does not pass",
|
|
46
|
+
},
|
|
47
|
+
files_exist: {
|
|
48
|
+
positive: "Confirm the required files exist",
|
|
49
|
+
negative: "Confirm the required files do not exist",
|
|
50
|
+
},
|
|
51
|
+
lint_clean: {
|
|
52
|
+
positive: "Confirm linting passes",
|
|
53
|
+
negative: "Confirm linting does not pass",
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
function isBuiltinPredicate(
|
|
58
|
+
name: string,
|
|
59
|
+
): name is (typeof BUILTIN_PREDICATES)[number] {
|
|
60
|
+
return (BUILTIN_PREDICATES as readonly string[]).includes(name);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Render a literal value as it should read inside a precondition check. */
|
|
64
|
+
function renderLiteral(value: string | boolean): string {
|
|
65
|
+
return typeof value === "boolean" ? String(value) : value;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Walk a gate AST, accumulating one natural-language precondition check per
|
|
70
|
+
* leaf reference. `negate` carries an enclosing `!` down to the leaves so a
|
|
71
|
+
* negated predicate / comparison reads correctly. Pure.
|
|
72
|
+
*/
|
|
73
|
+
function collectChecks(
|
|
74
|
+
node: ExprNode,
|
|
75
|
+
negate: boolean,
|
|
76
|
+
checks: string[],
|
|
77
|
+
): void {
|
|
78
|
+
switch (node.type) {
|
|
79
|
+
case "or":
|
|
80
|
+
case "and":
|
|
81
|
+
collectChecks(node.left, negate, checks);
|
|
82
|
+
collectChecks(node.right, negate, checks);
|
|
83
|
+
break;
|
|
84
|
+
case "not":
|
|
85
|
+
collectChecks(node.operand, !negate, checks);
|
|
86
|
+
break;
|
|
87
|
+
case "equality": {
|
|
88
|
+
// `==` asserts equality; `!=` asserts inequality. An enclosing `!`
|
|
89
|
+
// flips that assertion.
|
|
90
|
+
const assertsEqual = (node.op === "==") !== negate;
|
|
91
|
+
const expected = renderLiteral(node.right.value);
|
|
92
|
+
const subject =
|
|
93
|
+
node.left.type === "stateRef" ? node.left.key : node.left.name;
|
|
94
|
+
checks.push(
|
|
95
|
+
assertsEqual
|
|
96
|
+
? `Confirm that ${subject} is ${expected}`
|
|
97
|
+
: `Confirm that ${subject} is not ${expected}`,
|
|
98
|
+
);
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
case "stateRef":
|
|
102
|
+
checks.push(
|
|
103
|
+
negate
|
|
104
|
+
? `Confirm that ${node.key} is not set`
|
|
105
|
+
: `Confirm that ${node.key} is set`,
|
|
106
|
+
);
|
|
107
|
+
break;
|
|
108
|
+
case "predicate":
|
|
109
|
+
if (isBuiltinPredicate(node.name)) {
|
|
110
|
+
const phrasing = BUILTIN_PREDICATE_PHRASING[node.name];
|
|
111
|
+
checks.push(negate ? phrasing.negative : phrasing.positive);
|
|
112
|
+
} else {
|
|
113
|
+
checks.push(
|
|
114
|
+
negate
|
|
115
|
+
? `Confirm that ${node.name} does not hold`
|
|
116
|
+
: `Confirm that ${node.name} holds`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Render a gate expression as a natural-language precondition checklist that
|
|
125
|
+
* can be prepended to a Kiro `askAgent` prompt. Pure. (Req 3.6)
|
|
126
|
+
*
|
|
127
|
+
* Built-in predicates map to fixed phrasings (e.g. `tests_pass` →
|
|
128
|
+
* "Confirm the test suite passes"); state-key references map to
|
|
129
|
+
* "Confirm that <key> is <expected>" for equality comparisons (and a sensible
|
|
130
|
+
* "is set" phrasing for bare references).
|
|
131
|
+
*/
|
|
132
|
+
export function translateGateToPreamble(gate: string): string {
|
|
133
|
+
let ast: ParsedExpression;
|
|
134
|
+
try {
|
|
135
|
+
ast = parseExpression(gate);
|
|
136
|
+
} catch {
|
|
137
|
+
// A malformed gate should not crash the (pure) adapter; fall back to
|
|
138
|
+
// surfacing the raw expression so the agent still sees the precondition.
|
|
139
|
+
return [
|
|
140
|
+
"Preconditions — verify the following before proceeding:",
|
|
141
|
+
`- Confirm that this condition holds: ${gate}`,
|
|
142
|
+
"",
|
|
143
|
+
"Only proceed with the action below if the precondition holds.",
|
|
144
|
+
].join("\n");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const checks: string[] = [];
|
|
148
|
+
collectChecks(ast, false, checks);
|
|
149
|
+
// Deduplicate while preserving first-seen order.
|
|
150
|
+
const seen = new Set<string>();
|
|
151
|
+
const uniqueChecks = checks.filter((c) => {
|
|
152
|
+
if (seen.has(c)) return false;
|
|
153
|
+
seen.add(c);
|
|
154
|
+
return true;
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
return [
|
|
158
|
+
"Preconditions — verify all of the following before proceeding:",
|
|
159
|
+
...uniqueChecks.map((c) => `- ${c}`),
|
|
160
|
+
"",
|
|
161
|
+
"Only proceed with the action below if every precondition holds.",
|
|
162
|
+
].join("\n");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function buildKiroHook(hook: CanonicalHook): Record<string, unknown> {
|
|
166
|
+
const kiroEvent = KIRO_EVENT_MAP[hook.event];
|
|
167
|
+
const when: Record<string, unknown> = { type: kiroEvent };
|
|
168
|
+
if (hook.condition?.file_patterns?.length) {
|
|
169
|
+
when.patterns = hook.condition.file_patterns;
|
|
170
|
+
}
|
|
171
|
+
if (hook.condition?.tool_types?.length) {
|
|
172
|
+
when.toolTypes = hook.condition.tool_types;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
let then: Record<string, unknown>;
|
|
176
|
+
if (hook.action.type === "ask_agent") {
|
|
177
|
+
const prompt = hook.gate
|
|
178
|
+
? `${translateGateToPreamble(hook.gate)}\n\n${hook.action.prompt}`
|
|
179
|
+
: hook.action.prompt;
|
|
180
|
+
then = { type: "askAgent", prompt };
|
|
181
|
+
} else {
|
|
182
|
+
then = { type: "runCommand", command: hook.action.command };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
name: hook.name,
|
|
187
|
+
version: "1.0.0",
|
|
188
|
+
description: hook.description || "",
|
|
189
|
+
when,
|
|
190
|
+
then,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Build the HTML audit comment for a resolved Kiro inclusion.
|
|
196
|
+
* Format: `<!-- forge:kiro-inclusion: <mode> [fileMatchPattern=<glob>] -->`
|
|
197
|
+
*/
|
|
198
|
+
function buildAuditComment(resolved: ResolvedKiroInclusion): string {
|
|
199
|
+
if (resolved.mode === "fileMatch" && resolved.fileMatchPattern) {
|
|
200
|
+
return `<!-- forge:kiro-inclusion: fileMatch fileMatchPattern=${resolved.fileMatchPattern} -->`;
|
|
201
|
+
}
|
|
202
|
+
return `<!-- forge:kiro-inclusion: ${resolved.mode} -->`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function allocateHookPath(baseName: string, usedPaths: Set<string>): string {
|
|
206
|
+
const normalizedBaseName = `${baseName}.kiro.hook`;
|
|
207
|
+
let candidate = normalizedBaseName;
|
|
208
|
+
let suffix = 1;
|
|
209
|
+
|
|
210
|
+
while (usedPaths.has(candidate)) {
|
|
211
|
+
candidate = `${baseName}-${suffix}.kiro.hook`;
|
|
212
|
+
suffix += 1;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
usedPaths.add(candidate);
|
|
216
|
+
return candidate;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export const kiroAdapter: HarnessAdapter = (
|
|
220
|
+
artifact,
|
|
221
|
+
templateEnv,
|
|
222
|
+
context?,
|
|
223
|
+
) => {
|
|
224
|
+
const files: OutputFile[] = [];
|
|
225
|
+
const usedHookPaths = new Set<string>();
|
|
226
|
+
const warnings: AdapterWarning[] = [];
|
|
227
|
+
const errors: AdapterError[] = [];
|
|
228
|
+
|
|
229
|
+
// Capability degradation checks
|
|
230
|
+
if (context) {
|
|
231
|
+
const checks: Array<{
|
|
232
|
+
capability: HarnessCapabilityName;
|
|
233
|
+
hasFeature: boolean;
|
|
234
|
+
}> = [
|
|
235
|
+
{ capability: "hooks", hasFeature: artifact.hooks.length > 0 },
|
|
236
|
+
{ capability: "mcp", hasFeature: artifact.mcpServers.length > 0 },
|
|
237
|
+
{ capability: "workflows", hasFeature: artifact.workflows.length > 0 },
|
|
238
|
+
];
|
|
239
|
+
for (const { capability, hasFeature } of checks) {
|
|
240
|
+
if (!hasFeature) continue;
|
|
241
|
+
const entry = context.capabilities[capability];
|
|
242
|
+
if (entry.support === "full") continue;
|
|
243
|
+
if (context.strict) {
|
|
244
|
+
warnings.push({
|
|
245
|
+
artifactName: artifact.name,
|
|
246
|
+
harnessName: "kiro",
|
|
247
|
+
message: `Strict mode: capability ${capability} not supported by harness kiro`,
|
|
248
|
+
});
|
|
249
|
+
return { files, warnings };
|
|
250
|
+
}
|
|
251
|
+
const degradation = applyDegradation(
|
|
252
|
+
entry.degradation ?? "inline",
|
|
253
|
+
capability,
|
|
254
|
+
artifact,
|
|
255
|
+
"kiro",
|
|
256
|
+
);
|
|
257
|
+
warnings.push(...degradation.warnings);
|
|
258
|
+
if (degradation.inlineText) {
|
|
259
|
+
files.push({
|
|
260
|
+
relativePath: `${artifact.name}.degraded.md`,
|
|
261
|
+
content: degradation.inlineText,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const harnessConfig = (artifact.frontmatter as Record<string, unknown>)[
|
|
268
|
+
"harness-config"
|
|
269
|
+
] as Record<string, unknown> | undefined;
|
|
270
|
+
const kiroConfig = (harnessConfig?.kiro ?? {}) as Record<string, unknown>;
|
|
271
|
+
const { format, deprecationWarning } = resolveFormat("kiro", kiroConfig);
|
|
272
|
+
|
|
273
|
+
if (deprecationWarning) {
|
|
274
|
+
warnings.push({
|
|
275
|
+
artifactName: artifact.name,
|
|
276
|
+
harnessName: "kiro",
|
|
277
|
+
message: deprecationWarning,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (format === "power") {
|
|
282
|
+
// Generate POWER.md
|
|
283
|
+
const content = renderTemplate(templateEnv, "kiro/power.md.njk", {
|
|
284
|
+
artifact,
|
|
285
|
+
harnessConfig: kiroConfig,
|
|
286
|
+
});
|
|
287
|
+
files.push({ relativePath: "POWER.md", content });
|
|
288
|
+
|
|
289
|
+
// Copy workflows to steering/ with progressive inspection
|
|
290
|
+
for (const wf of artifact.workflows) {
|
|
291
|
+
const parseResult = parseKiroSteeringFile(wf.content, wf.filename);
|
|
292
|
+
const wfInclusion =
|
|
293
|
+
parseResult.ok && parseResult.frontmatter
|
|
294
|
+
? parseResult.frontmatter.inclusion
|
|
295
|
+
: undefined;
|
|
296
|
+
|
|
297
|
+
// Req 10.3: warn when inclusion is absent or "always"
|
|
298
|
+
if (!wfInclusion || wfInclusion === "always") {
|
|
299
|
+
const message =
|
|
300
|
+
wfInclusion === "always"
|
|
301
|
+
? `Workflow file "${wf.filename}" has inclusion: always; workflow files should be disclosed progressively (fileMatch or manual).`
|
|
302
|
+
: `Workflow file "${wf.filename}" is missing an inclusion mode; workflow files should be disclosed progressively (fileMatch or manual).`;
|
|
303
|
+
|
|
304
|
+
warnings.push({
|
|
305
|
+
artifactName: artifact.name,
|
|
306
|
+
harnessName: "kiro",
|
|
307
|
+
message,
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// Req 10.4: strict mode → error + omit file
|
|
311
|
+
if (kiroConfig.progressiveWorkflowsStrict === true) {
|
|
312
|
+
errors.push({
|
|
313
|
+
artifactName: artifact.name,
|
|
314
|
+
harnessName: "kiro",
|
|
315
|
+
message,
|
|
316
|
+
field: wf.filename,
|
|
317
|
+
});
|
|
318
|
+
// Do not add this workflow file to files[]
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
files.push({
|
|
324
|
+
relativePath: `steering/${wf.filename}`,
|
|
325
|
+
content: wf.content,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Resolve Kiro inclusion for steering template
|
|
331
|
+
const resolved = resolveKiroInclusion(artifact);
|
|
332
|
+
|
|
333
|
+
// Generate steering .md file.
|
|
334
|
+
// For powers, this emits an always-included steering/{name}.md that mirrors
|
|
335
|
+
// POWER.md. Set harness-config.kiro.main-steering: false to suppress it
|
|
336
|
+
// (official-power style: POWER.md is the entry point, no duplicate).
|
|
337
|
+
const emitMainSteering =
|
|
338
|
+
format !== "power" || kiroConfig["main-steering"] !== false;
|
|
339
|
+
if (emitMainSteering) {
|
|
340
|
+
const steeringPath =
|
|
341
|
+
format === "power"
|
|
342
|
+
? `steering/${artifact.name}.md`
|
|
343
|
+
: `${artifact.name}.md`;
|
|
344
|
+
|
|
345
|
+
let steeringContent: string;
|
|
346
|
+
if (format === "power") {
|
|
347
|
+
// Power steering files use plain Markdown without frontmatter
|
|
348
|
+
// to match the official Kiro powers format
|
|
349
|
+
steeringContent = renderTemplate(
|
|
350
|
+
templateEnv,
|
|
351
|
+
"kiro/power-steering.md.njk",
|
|
352
|
+
{
|
|
353
|
+
artifact,
|
|
354
|
+
harnessConfig: kiroConfig,
|
|
355
|
+
},
|
|
356
|
+
);
|
|
357
|
+
} else {
|
|
358
|
+
const auditComment = buildAuditComment(resolved);
|
|
359
|
+
steeringContent = renderTemplate(templateEnv, "kiro/steering.md.njk", {
|
|
360
|
+
artifact,
|
|
361
|
+
harnessConfig: kiroConfig,
|
|
362
|
+
inclusion: resolved.mode,
|
|
363
|
+
fileMatchPattern: resolved.fileMatchPattern,
|
|
364
|
+
auditComment,
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
files.push({ relativePath: steeringPath, content: steeringContent });
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Generate hook JSON files
|
|
371
|
+
for (const hook of artifact.hooks) {
|
|
372
|
+
const kiroHook = buildKiroHook(hook);
|
|
373
|
+
const hookContent = renderTemplate(templateEnv, "kiro/hook.json.njk", {
|
|
374
|
+
hook: kiroHook,
|
|
375
|
+
});
|
|
376
|
+
const hookName = hook.name.toLowerCase().replace(/\s+/g, "-");
|
|
377
|
+
files.push({
|
|
378
|
+
relativePath: allocateHookPath(hookName, usedHookPaths),
|
|
379
|
+
content: hookContent,
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Handle spec-hooks from harness-config
|
|
384
|
+
const specHooks = kiroConfig["spec-hooks"] as
|
|
385
|
+
| Array<Record<string, unknown>>
|
|
386
|
+
| undefined;
|
|
387
|
+
if (specHooks && Array.isArray(specHooks)) {
|
|
388
|
+
for (const specHook of specHooks) {
|
|
389
|
+
const hookContent = renderTemplate(templateEnv, "kiro/hook.json.njk", {
|
|
390
|
+
hook: specHook,
|
|
391
|
+
});
|
|
392
|
+
const hookName = String(specHook.name || "spec-hook")
|
|
393
|
+
.toLowerCase()
|
|
394
|
+
.replace(/\s+/g, "-");
|
|
395
|
+
files.push({
|
|
396
|
+
relativePath: allocateHookPath(hookName, usedHookPaths),
|
|
397
|
+
content: hookContent,
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Generate mcp.json
|
|
403
|
+
if (artifact.mcpServers.length > 0) {
|
|
404
|
+
const mcpConfig = buildMcpConfig(artifact.mcpServers);
|
|
405
|
+
const mcpContent = renderTemplate(templateEnv, "kiro/mcp.json.njk", {
|
|
406
|
+
mcpConfig,
|
|
407
|
+
});
|
|
408
|
+
files.push({ relativePath: "mcp.json", content: mcpContent });
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return { files, warnings, errors: errors.length > 0 ? errors : undefined };
|
|
412
|
+
};
|