@thinkingsage/kanon 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. package/CHANGELOG.md +410 -0
  2. package/LICENSE +21 -0
  3. package/README.md +168 -0
  4. package/bridge/mcp-server.cjs +14171 -0
  5. package/package.json +98 -0
  6. package/src/adapters/capabilities.ts +178 -0
  7. package/src/adapters/claude-code.ts +110 -0
  8. package/src/adapters/cline.ts +98 -0
  9. package/src/adapters/codex.ts +173 -0
  10. package/src/adapters/copilot.ts +106 -0
  11. package/src/adapters/cursor.ts +97 -0
  12. package/src/adapters/degradation.ts +95 -0
  13. package/src/adapters/index.ts +324 -0
  14. package/src/adapters/kiro-frontmatter.ts +139 -0
  15. package/src/adapters/kiro-inclusion.ts +86 -0
  16. package/src/adapters/kiro.ts +412 -0
  17. package/src/adapters/qdeveloper.ts +115 -0
  18. package/src/adapters/types.ts +81 -0
  19. package/src/adapters/windsurf.ts +96 -0
  20. package/src/admin.ts +283 -0
  21. package/src/asset-conventions.ts +118 -0
  22. package/src/attribution-backfill.ts +319 -0
  23. package/src/attribution-report.ts +95 -0
  24. package/src/attribution.ts +239 -0
  25. package/src/backends/github.ts +194 -0
  26. package/src/backends/http.ts +122 -0
  27. package/src/backends/index.ts +39 -0
  28. package/src/backends/local.ts +47 -0
  29. package/src/backends/s3.ts +157 -0
  30. package/src/backends/types.ts +59 -0
  31. package/src/base-cache.ts +270 -0
  32. package/src/browse-ui.ts +3754 -0
  33. package/src/browse.ts +1038 -0
  34. package/src/build.ts +1108 -0
  35. package/src/catalog.ts +204 -0
  36. package/src/cli-deprecated.ts +29 -0
  37. package/src/cli.ts +773 -0
  38. package/src/collection-admin.ts +287 -0
  39. package/src/collection-builder.ts +464 -0
  40. package/src/collections.ts +116 -0
  41. package/src/compatibility.ts +105 -0
  42. package/src/config.ts +743 -0
  43. package/src/eval/rubrics/kiro-progressive-steering.ts +841 -0
  44. package/src/eval.ts +1169 -0
  45. package/src/file-writer.ts +61 -0
  46. package/src/format-registry.ts +141 -0
  47. package/src/guild/auto-updater.ts +163 -0
  48. package/src/guild/backend-resolver.ts +49 -0
  49. package/src/guild/cli.ts +592 -0
  50. package/src/guild/collection-expander.ts +47 -0
  51. package/src/guild/global-cache.ts +247 -0
  52. package/src/guild/hook-generator.ts +100 -0
  53. package/src/guild/manifest.ts +154 -0
  54. package/src/guild/path-utils.ts +12 -0
  55. package/src/guild/sync.ts +622 -0
  56. package/src/guild/version-resolver.ts +42 -0
  57. package/src/help/metadata.ts +445 -0
  58. package/src/help/renderer.ts +265 -0
  59. package/src/help/typo-suggester.ts +25 -0
  60. package/src/hooks/expression.ts +493 -0
  61. package/src/hooks/pipeline.ts +141 -0
  62. package/src/import.ts +773 -0
  63. package/src/importers/claude-code.ts +134 -0
  64. package/src/importers/cline.ts +103 -0
  65. package/src/importers/codex.ts +140 -0
  66. package/src/importers/copilot.ts +103 -0
  67. package/src/importers/cursor.ts +105 -0
  68. package/src/importers/index.ts +390 -0
  69. package/src/importers/kiro.ts +110 -0
  70. package/src/importers/qdeveloper.ts +103 -0
  71. package/src/importers/types.ts +54 -0
  72. package/src/importers/windsurf.ts +104 -0
  73. package/src/install.ts +1005 -0
  74. package/src/manifest-admin.ts +306 -0
  75. package/src/mcp-bridge.ts +240 -0
  76. package/src/mutation/delta.ts +50 -0
  77. package/src/mutation/history.ts +66 -0
  78. package/src/mutation/operators.ts +524 -0
  79. package/src/mutation/runner.ts +332 -0
  80. package/src/new.ts +106 -0
  81. package/src/outcomes/collision.ts +127 -0
  82. package/src/outcomes/normalize.ts +208 -0
  83. package/src/outcomes/registry.ts +173 -0
  84. package/src/parser.ts +446 -0
  85. package/src/provenance-backfill-cli.ts +319 -0
  86. package/src/provenance-backfill.ts +520 -0
  87. package/src/publish.ts +354 -0
  88. package/src/reconcile-orchestrator.ts +502 -0
  89. package/src/reconcile-report-renderer.ts +176 -0
  90. package/src/resolve-body.ts +15 -0
  91. package/src/rosetta/builtins/compatibility-profiles.ts +297 -0
  92. package/src/rosetta/builtins/contracts.ts +1033 -0
  93. package/src/rosetta/builtins/pretty-printers/claude-code-native.ts +122 -0
  94. package/src/rosetta/builtins/pretty-printers/cline-native.ts +50 -0
  95. package/src/rosetta/builtins/pretty-printers/codex-native.ts +127 -0
  96. package/src/rosetta/builtins/pretty-printers/copilot-native.ts +50 -0
  97. package/src/rosetta/builtins/pretty-printers/cursor-native.ts +50 -0
  98. package/src/rosetta/builtins/pretty-printers/index.ts +81 -0
  99. package/src/rosetta/builtins/pretty-printers/kiro-native.ts +166 -0
  100. package/src/rosetta/builtins/pretty-printers/kiro-power.ts +108 -0
  101. package/src/rosetta/builtins/pretty-printers/kiro-skill.ts +88 -0
  102. package/src/rosetta/builtins/pretty-printers/qdeveloper-native.ts +51 -0
  103. package/src/rosetta/builtins/pretty-printers/superpowers.ts +97 -0
  104. package/src/rosetta/builtins/pretty-printers/windsurf-native.ts +50 -0
  105. package/src/rosetta/builtins/sources/claude-code-native.ts +348 -0
  106. package/src/rosetta/builtins/sources/cline-native.ts +176 -0
  107. package/src/rosetta/builtins/sources/codex-native.ts +343 -0
  108. package/src/rosetta/builtins/sources/copilot-native.ts +178 -0
  109. package/src/rosetta/builtins/sources/cursor-native.ts +176 -0
  110. package/src/rosetta/builtins/sources/index.ts +95 -0
  111. package/src/rosetta/builtins/sources/kiro-native.ts +462 -0
  112. package/src/rosetta/builtins/sources/kiro-power.ts +285 -0
  113. package/src/rosetta/builtins/sources/kiro-skill.ts +230 -0
  114. package/src/rosetta/builtins/sources/qdeveloper-native.ts +181 -0
  115. package/src/rosetta/builtins/sources/superpowers.ts +240 -0
  116. package/src/rosetta/builtins/sources/windsurf-native.ts +176 -0
  117. package/src/rosetta/builtins/targets/claude-code.ts +181 -0
  118. package/src/rosetta/builtins/targets/cline.ts +87 -0
  119. package/src/rosetta/builtins/targets/codex.ts +226 -0
  120. package/src/rosetta/builtins/targets/copilot.ts +103 -0
  121. package/src/rosetta/builtins/targets/cursor.ts +87 -0
  122. package/src/rosetta/builtins/targets/index.ts +60 -0
  123. package/src/rosetta/builtins/targets/kiro.ts +278 -0
  124. package/src/rosetta/builtins/targets/qdeveloper.ts +103 -0
  125. package/src/rosetta/builtins/targets/windsurf.ts +87 -0
  126. package/src/rosetta/canonical.ts +729 -0
  127. package/src/rosetta/compatibility.ts +432 -0
  128. package/src/rosetta/contracts.ts +329 -0
  129. package/src/rosetta/detector.ts +724 -0
  130. package/src/rosetta/diagnostics.ts +630 -0
  131. package/src/rosetta/engine-bootstrap.ts +103 -0
  132. package/src/rosetta/engine.ts +744 -0
  133. package/src/rosetta/index.ts +381 -0
  134. package/src/rosetta/inspection.ts +530 -0
  135. package/src/rosetta/plan.ts +448 -0
  136. package/src/rosetta/provenance-digest.ts +369 -0
  137. package/src/rosetta/reconcile.ts +812 -0
  138. package/src/rosetta/redaction.ts +467 -0
  139. package/src/rosetta/registry.ts +712 -0
  140. package/src/rosetta/renderers.ts +571 -0
  141. package/src/rosetta/request-guard.ts +335 -0
  142. package/src/rosetta/resolution.ts +419 -0
  143. package/src/rosetta/source-accounting.ts +233 -0
  144. package/src/rosetta/templates.ts +129 -0
  145. package/src/rosetta-cli.ts +717 -0
  146. package/src/rosetta-docs-generator.ts +793 -0
  147. package/src/rosetta-profiles-cli.ts +367 -0
  148. package/src/schemas.ts +1712 -0
  149. package/src/spec-coordination.ts +1141 -0
  150. package/src/temper.ts +747 -0
  151. package/src/template-bundle-loader.ts +312 -0
  152. package/src/template-engine.ts +53 -0
  153. package/src/translation-application-policy.ts +496 -0
  154. package/src/translation-orchestrator.ts +1013 -0
  155. package/src/translation-plan-applier.ts +473 -0
  156. package/src/tutorial.ts +305 -0
  157. package/src/validate.ts +1093 -0
  158. package/src/versioning.ts +553 -0
  159. package/src/wizard.ts +660 -0
  160. package/src/workspace.ts +237 -0
  161. package/templates/eval-contexts/claude-code.md.njk +6 -0
  162. package/templates/eval-contexts/cline.md.njk +6 -0
  163. package/templates/eval-contexts/copilot.md.njk +6 -0
  164. package/templates/eval-contexts/cursor.md.njk +6 -0
  165. package/templates/eval-contexts/kiro.md.njk +10 -0
  166. package/templates/eval-contexts/qdeveloper.md.njk +6 -0
  167. package/templates/eval-contexts/windsurf.md.njk +6 -0
  168. package/templates/harness-adapters/_base/attribution-footer.md.njk +17 -0
  169. package/templates/harness-adapters/_base/base.md.njk +16 -0
  170. package/templates/harness-adapters/claude-code/claude.md.njk +1 -0
  171. package/templates/harness-adapters/claude-code/mcp.json.njk +1 -0
  172. package/templates/harness-adapters/claude-code/settings.json.njk +1 -0
  173. package/templates/harness-adapters/claude-code/skill-library-index.md.njk +13 -0
  174. package/templates/harness-adapters/claude-code/skill.md.njk +19 -0
  175. package/templates/harness-adapters/cline/hook.sh.njk +4 -0
  176. package/templates/harness-adapters/cline/mcp.json.njk +1 -0
  177. package/templates/harness-adapters/cline/rule.md.njk +1 -0
  178. package/templates/harness-adapters/codex/agents-md.md.njk +6 -0
  179. package/templates/harness-adapters/codex/agents-pointer.md.njk +16 -0
  180. package/templates/harness-adapters/codex/skill.md.njk +27 -0
  181. package/templates/harness-adapters/copilot/agents.md.njk +1 -0
  182. package/templates/harness-adapters/copilot/instructions.md.njk +1 -0
  183. package/templates/harness-adapters/copilot/scoped.md.njk +6 -0
  184. package/templates/harness-adapters/cursor/mcp.json.njk +1 -0
  185. package/templates/harness-adapters/cursor/rule.md.njk +6 -0
  186. package/templates/harness-adapters/kiro/hook.json.njk +1 -0
  187. package/templates/harness-adapters/kiro/mcp.json.njk +1 -0
  188. package/templates/harness-adapters/kiro/power-steering.md.njk +3 -0
  189. package/templates/harness-adapters/kiro/power.md.njk +12 -0
  190. package/templates/harness-adapters/kiro/steering.md.njk +16 -0
  191. package/templates/harness-adapters/qdeveloper/agent.md.njk +1 -0
  192. package/templates/harness-adapters/qdeveloper/mcp.json.njk +1 -0
  193. package/templates/harness-adapters/qdeveloper/rule.md.njk +1 -0
  194. package/templates/harness-adapters/windsurf/mcp.json.njk +1 -0
  195. package/templates/harness-adapters/windsurf/rule.md.njk +1 -0
  196. package/templates/harness-adapters/windsurf/workflow.md.njk +1 -0
  197. package/templates/knowledge/hooks.yaml.njk +4 -0
  198. package/templates/knowledge/knowledge.md.njk +53 -0
  199. package/templates/knowledge/mcp-servers.yaml.njk +2 -0
@@ -0,0 +1,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
+ };