@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
package/src/build.ts ADDED
@@ -0,0 +1,1108 @@
1
+ /**
2
+ * Build Pipeline — Imperative Orchestration Shell
3
+ *
4
+ * This module is the imperative build shell that coordinates artifact
5
+ * compilation. It retains all side-effectful and orchestration concerns:
6
+ *
7
+ * - Artifact scanning and discovery (collectArtifactPaths)
8
+ * - Dependency composition (resolveComposition — MCP server + hook merging)
9
+ * - Shared MCP server merge (loadSharedMcpServers)
10
+ * - Workspace overrides (applyProjectOverrides, filterArtifactsForProject)
11
+ * - Dist directory cleanup/policy
12
+ * - Build summaries and counts (kiroInclusionSummary, threshold warnings)
13
+ * - File writes to dist/
14
+ *
15
+ * It DELEGATES to Rosetta Stone for:
16
+ *
17
+ * - Canonical parsing: loadKnowledgeArtifact (src/parser.ts) delegates to
18
+ * parseCanonical (src/rosetta/canonical.ts) for the pure parse step.
19
+ * - Target translation: adapterRegistry (src/adapters/index.ts) routes
20
+ * through Rosetta Stone target translators with immutable template bundles,
21
+ * mapping plans/diagnostics back to AdapterResult.
22
+ * - Format resolution: resolveFormat (src/format-registry.ts) projects from
23
+ * Rosetta Stone built-in format contracts.
24
+ *
25
+ * ADR-RS-001: Functional core, imperative shell.
26
+ * ADR-RS-002: One authoritative registry (adapters route through Rosetta Stone).
27
+ * ADR-RS-004: Templates preloaded into immutable bundles.
28
+ *
29
+ * Requirements: 1.3, 12.1, 12.2, 14.5, 14.10
30
+ */
31
+
32
+ import { chmod, exists, mkdir, readdir, rm, writeFile } from "node:fs/promises";
33
+ import { join, resolve } from "node:path";
34
+ import chalk from "chalk";
35
+ import { getCapabilities } from "./adapters/capabilities";
36
+ import { adapterRegistry } from "./adapters/index";
37
+ import {
38
+ type KiroInclusionMode,
39
+ resolveKiroInclusion,
40
+ } from "./adapters/kiro-inclusion";
41
+ import type { AdapterContext, AdapterWarning } from "./adapters/types";
42
+ import { getCompatibility } from "./compatibility";
43
+ import { loadForgeConfig } from "./config";
44
+ import { resolveFormat } from "./format-registry";
45
+ import {
46
+ isParseError,
47
+ loadKnowledgeArtifact,
48
+ parseMcpServersYaml,
49
+ } from "./parser";
50
+ import { resolveBody } from "./resolve-body";
51
+ import type {
52
+ CanonicalHook,
53
+ HarnessName,
54
+ KnowledgeArtifact,
55
+ McpServerDefinition,
56
+ WorkspaceConfig,
57
+ WorkspaceProject,
58
+ } from "./schemas";
59
+ import { SUPPORTED_HARNESSES } from "./schemas";
60
+ import { createTemplateEnv } from "./template-engine";
61
+ import { embedVersion } from "./versioning";
62
+ import { loadWorkspaceConfig, mergeKnowledgeSources } from "./workspace";
63
+
64
+ export interface BuildOptions {
65
+ /** One or more source directories to scan for artifacts. */
66
+ knowledgeDirs?: string[];
67
+ /** @deprecated Use knowledgeDirs instead. */
68
+ knowledgeDir?: string;
69
+ distDir: string;
70
+ templatesDir: string;
71
+ mcpServersDir: string;
72
+ harness?: HarnessName;
73
+ /** Treat compatibility warnings as errors. */
74
+ strict?: boolean;
75
+ /** Workspace root directory for workspace-aware builds. */
76
+ workspaceRoot?: string;
77
+ /** Threshold (0..1) for the always-on share warning. When the ratio of
78
+ * always-mode Kiro steering files exceeds this, a warning is emitted.
79
+ * Set to 1 to disable. Default: 0.5. */
80
+ kiroAlwaysWarnThreshold?: number;
81
+ }
82
+
83
+ export interface BuildError {
84
+ artifactName: string;
85
+ harnessName: string;
86
+ message: string;
87
+ }
88
+
89
+ export interface BuildResult {
90
+ artifactsCompiled: number;
91
+ filesWritten: number;
92
+ warnings: AdapterWarning[];
93
+ errors: BuildError[];
94
+ kiroInclusionSummary?: {
95
+ total: number;
96
+ byMode: Record<KiroInclusionMode, number>;
97
+ byFormat: Record<"steering" | "power", number>;
98
+ progressiveRatio: number; // (fileMatch + manual) / total
99
+ contributingArtifacts: Record<KiroInclusionMode, string[]>;
100
+ };
101
+ }
102
+
103
+ /** Tracking entry for one Kiro steering file emitted during a build. */
104
+ interface KiroSummaryEntry {
105
+ artifactName: string;
106
+ mode: KiroInclusionMode;
107
+ format: "steering" | "power";
108
+ }
109
+
110
+ /**
111
+ * Compute the BuildResult.kiroInclusionSummary from collected entries.
112
+ */
113
+ function computeKiroInclusionSummary(
114
+ entries: KiroSummaryEntry[],
115
+ ): NonNullable<BuildResult["kiroInclusionSummary"]> {
116
+ const byMode: Record<KiroInclusionMode, number> = {
117
+ always: 0,
118
+ fileMatch: 0,
119
+ manual: 0,
120
+ };
121
+ const byFormat: Record<"steering" | "power", number> = {
122
+ steering: 0,
123
+ power: 0,
124
+ };
125
+ const contributingArtifacts: Record<KiroInclusionMode, string[]> = {
126
+ always: [],
127
+ fileMatch: [],
128
+ manual: [],
129
+ };
130
+
131
+ for (const entry of entries) {
132
+ byMode[entry.mode]++;
133
+ byFormat[entry.format]++;
134
+ contributingArtifacts[entry.mode].push(entry.artifactName);
135
+ }
136
+
137
+ const total = entries.length;
138
+ const progressiveRatio =
139
+ total > 0 ? (byMode.fileMatch + byMode.manual) / total : 0;
140
+
141
+ return { total, byMode, byFormat, progressiveRatio, contributingArtifacts };
142
+ }
143
+
144
+ /**
145
+ * Print an Inclusion_Summary to stderr grouped by mode,
146
+ * showing totals, progressive ratio, and format breakdown.
147
+ */
148
+ function printKiroInclusionSummary(
149
+ summary: NonNullable<BuildResult["kiroInclusionSummary"]>,
150
+ ): void {
151
+ const lines: string[] = [];
152
+ lines.push("");
153
+ lines.push(chalk.cyan("Kiro Inclusion Summary:"));
154
+ lines.push(` Total steering files: ${summary.total}`);
155
+ lines.push(` By mode:`);
156
+ for (const mode of ["always", "fileMatch", "manual"] as KiroInclusionMode[]) {
157
+ const count = summary.byMode[mode];
158
+ if (count > 0) {
159
+ const artifacts = summary.contributingArtifacts[mode].join(", ");
160
+ lines.push(` ${mode}: ${count} (${artifacts})`);
161
+ }
162
+ }
163
+ lines.push(` By format:`);
164
+ if (summary.byFormat.steering > 0) {
165
+ lines.push(` steering: ${summary.byFormat.steering}`);
166
+ }
167
+ if (summary.byFormat.power > 0) {
168
+ lines.push(` power: ${summary.byFormat.power}`);
169
+ }
170
+ const pct = Math.round(summary.progressiveRatio * 100);
171
+ lines.push(` Progressive ratio: ${pct}% (fileMatch + manual)`);
172
+ console.error(lines.join("\n"));
173
+ }
174
+
175
+ // ═══════════════════════════════════════════════════════════════════════════════
176
+ // Imperative Shell — Shared MCP Server Loading
177
+ // ═══════════════════════════════════════════════════════════════════════════════
178
+
179
+ /**
180
+ * Load shared MCP server definitions from the project's mcp-servers/ directory.
181
+ * These are merged into each artifact's mcpServers array (artifact-local takes
182
+ * precedence) as part of the dependency composition step that remains in the
183
+ * imperative build shell.
184
+ */
185
+ async function loadSharedMcpServers(mcpServersDir: string) {
186
+ const servers: Map<string, Omit<McpServerDefinition, "name">> = new Map();
187
+ if (!(await exists(mcpServersDir))) return servers;
188
+
189
+ const entries = await readdir(mcpServersDir);
190
+ for (const entry of entries) {
191
+ if (!entry.endsWith(".yaml") && !entry.endsWith(".yml")) continue;
192
+ const result = await parseMcpServersYaml(join(mcpServersDir, entry));
193
+ if (!isParseError(result)) {
194
+ for (const s of result.data) {
195
+ const { name, ...rest } = s;
196
+ servers.set(name, rest);
197
+ }
198
+ }
199
+ }
200
+ return servers;
201
+ }
202
+
203
+ // ═══════════════════════════════════════════════════════════════════════════════
204
+ // Imperative Shell — Artifact Discovery
205
+ // ═══════════════════════════════════════════════════════════════════════════════
206
+
207
+ /**
208
+ * Collect all artifact paths from one or more source directories.
209
+ * Handles two layouts:
210
+ * Flat: <sourceDir>/<artifact>/knowledge.md
211
+ * Namespaced: <sourceDir>/<prefix>/<artifact>/knowledge.md
212
+ *
213
+ * This is an imperative concern (filesystem scanning) that remains in the
214
+ * build shell. The discovered paths are then fed to loadKnowledgeArtifact()
215
+ * which delegates the pure parse step to Rosetta Stone's parseCanonical().
216
+ */
217
+ async function collectArtifactPaths(sourceDirs: string[]): Promise<string[]> {
218
+ const paths: string[] = [];
219
+
220
+ for (const sourceDir of sourceDirs) {
221
+ if (!(await exists(sourceDir))) continue;
222
+
223
+ const dirEntries = await readdir(sourceDir, { withFileTypes: true });
224
+ const subdirs = dirEntries
225
+ .filter((e) => e.isDirectory())
226
+ .sort((a, b) => a.name.localeCompare(b.name));
227
+
228
+ for (const subdir of subdirs) {
229
+ const subdirPath = join(sourceDir, subdir.name);
230
+
231
+ if (await exists(join(subdirPath, "knowledge.md"))) {
232
+ // Flat layout
233
+ paths.push(subdirPath);
234
+ } else {
235
+ // Check for namespaced layout — recurse one level
236
+ const inner = await readdir(subdirPath, { withFileTypes: true });
237
+ const innerDirs = inner
238
+ .filter((e) => e.isDirectory())
239
+ .sort((a, b) => a.name.localeCompare(b.name));
240
+
241
+ let foundInnerArtifact = false;
242
+ for (const innerDir of innerDirs) {
243
+ const artifactPath = join(subdirPath, innerDir.name);
244
+ if (await exists(join(artifactPath, "knowledge.md"))) {
245
+ paths.push(artifactPath);
246
+ foundInnerArtifact = true;
247
+ }
248
+ }
249
+
250
+ // If neither a flat artifact nor a namespace prefix, warn
251
+ if (!foundInnerArtifact) {
252
+ const hasFiles = inner.some((e) => e.isFile());
253
+ if (hasFiles) {
254
+ console.error(
255
+ chalk.yellow(
256
+ `Warning: Skipping ${subdir.name} — no knowledge.md found`,
257
+ ),
258
+ );
259
+ }
260
+ }
261
+ }
262
+ }
263
+ }
264
+
265
+ return paths;
266
+ }
267
+
268
+ // ═══════════════════════════════════════════════════════════════════════════════
269
+ // Imperative Shell — Dependency Composition
270
+ // ═══════════════════════════════════════════════════════════════════════════════
271
+
272
+ /**
273
+ * Resolve composed mcpServers and hooks from an artifact's dependency tree.
274
+ * Returns merged arrays (deduped by name) plus a cycle error string if detected.
275
+ *
276
+ * This is a build-shell concern: composing resources from the dependency graph
277
+ * before handing the enriched artifact to the Rosetta Stone target translator
278
+ * (via adapterRegistry). The pure translator sees the final composed artifact.
279
+ */
280
+ async function resolveComposition(
281
+ artifact: KnowledgeArtifact,
282
+ sourceDirs: string[],
283
+ visited = new Set<string>(),
284
+ ): Promise<{
285
+ mcpServers: McpServerDefinition[];
286
+ hooks: CanonicalHook[];
287
+ cycleError?: string;
288
+ }> {
289
+ if (visited.has(artifact.name)) {
290
+ return {
291
+ mcpServers: [],
292
+ hooks: [],
293
+ cycleError: `Dependency cycle detected involving "${artifact.name}"`,
294
+ };
295
+ }
296
+ visited.add(artifact.name);
297
+
298
+ const mergedMcp: McpServerDefinition[] = [];
299
+ const mergedHooks: CanonicalHook[] = [];
300
+
301
+ for (const depName of artifact.frontmatter.depends) {
302
+ // Locate the dependency artifact
303
+ const depPaths = await collectArtifactPaths(sourceDirs);
304
+ const depPath = depPaths.find((p) => p.split("/").pop() === depName);
305
+ if (!depPath) continue;
306
+
307
+ const depResult = await loadKnowledgeArtifact(depPath);
308
+ if (isParseError(depResult)) continue;
309
+
310
+ const dep = depResult.data;
311
+
312
+ // Recurse if the dependency also has deps
313
+ if (dep.frontmatter.depends.length > 0) {
314
+ const nested = await resolveComposition(
315
+ dep,
316
+ sourceDirs,
317
+ new Set(visited),
318
+ );
319
+ if (nested.cycleError)
320
+ return { mcpServers: [], hooks: [], cycleError: nested.cycleError };
321
+ mergedMcp.push(...nested.mcpServers);
322
+ mergedHooks.push(...nested.hooks);
323
+ }
324
+
325
+ mergedMcp.push(...dep.mcpServers);
326
+ if (dep.frontmatter["inherit-hooks"]) {
327
+ mergedHooks.push(...dep.hooks);
328
+ }
329
+ }
330
+
331
+ return { mcpServers: mergedMcp, hooks: mergedHooks };
332
+ }
333
+
334
+ // ═══════════════════════════════════════════════════════════════════════════════
335
+ // Imperative Shell — Workspace Filtering and Overrides
336
+ // ═══════════════════════════════════════════════════════════════════════════════
337
+
338
+ /**
339
+ * Filter artifacts based on a project's include/exclude configuration.
340
+ */
341
+ function filterArtifactsForProject(
342
+ allArtifactNames: string[],
343
+ project: WorkspaceProject,
344
+ ): string[] {
345
+ let names = [...allArtifactNames];
346
+
347
+ if (project.artifacts?.include) {
348
+ const includeSet = new Set(project.artifacts.include);
349
+ names = names.filter((n) => includeSet.has(n));
350
+ }
351
+
352
+ if (project.artifacts?.exclude) {
353
+ const excludeSet = new Set(project.artifacts.exclude);
354
+ names = names.filter((n) => !excludeSet.has(n));
355
+ }
356
+
357
+ return names;
358
+ }
359
+
360
+ /**
361
+ * Apply project overrides to an artifact's harness-config.
362
+ * Project overrides take precedence over artifact harness-config.
363
+ */
364
+ function applyProjectOverrides(
365
+ artifact: KnowledgeArtifact,
366
+ project: WorkspaceProject,
367
+ harnessName: string,
368
+ ): void {
369
+ if (!project.overrides?.[harnessName]) return;
370
+
371
+ const fm = artifact.frontmatter as Record<string, unknown>;
372
+ const harnessConfig =
373
+ (fm["harness-config"] as Record<string, Record<string, unknown>>) ?? {};
374
+ const existingHarnessConf = harnessConfig[harnessName] ?? {};
375
+
376
+ // Merge: project overrides take precedence
377
+ harnessConfig[harnessName] = {
378
+ ...existingHarnessConf,
379
+ ...project.overrides[harnessName],
380
+ };
381
+ fm["harness-config"] = harnessConfig;
382
+ }
383
+
384
+ // ═══════════════════════════════════════════════════════════════════════════════
385
+ // Build Orchestration — Workspace-Aware
386
+ // ═══════════════════════════════════════════════════════════════════════════════
387
+
388
+ /**
389
+ * Workspace-aware build: compile artifacts per project according to workspace config.
390
+ *
391
+ * Orchestration flow:
392
+ * 1. Scan artifact paths from workspace knowledge sources (imperative: discovery)
393
+ * 2. Load and parse each artifact via loadKnowledgeArtifact (delegates to Rosetta Stone parseCanonical)
394
+ * 3. Apply workspace filtering/overrides (imperative: workspace policy)
395
+ * 4. Merge shared MCP servers and resolve dependencies (imperative: composition)
396
+ * 5. Call adapterRegistry[harness] for target translation (delegates to Rosetta Stone target translators)
397
+ * 6. Write output files to dist/ (imperative: file writes)
398
+ */
399
+ async function buildWithWorkspace(
400
+ wsConfig: WorkspaceConfig,
401
+ wsRoot: string,
402
+ options: BuildOptions,
403
+ ): Promise<BuildResult> {
404
+ const { distDir, templatesDir, mcpServersDir, harness, strict } = options;
405
+ const warnings: AdapterWarning[] = [];
406
+ const errors: BuildError[] = [];
407
+ let filesWritten = 0;
408
+ let artifactsCompiled = 0;
409
+ const kiroSummaryEntries: KiroSummaryEntry[] = [];
410
+
411
+ // Resolve knowledgeSources relative to workspace root
412
+ const resolvedSources = wsConfig.knowledgeSources.map((s) =>
413
+ resolve(wsRoot, s),
414
+ );
415
+
416
+ // Merge artifacts from all knowledge sources
417
+ const mergeResult = await mergeKnowledgeSources(
418
+ wsConfig.knowledgeSources,
419
+ wsRoot,
420
+ );
421
+
422
+ // If conflicts detected, return errors
423
+ if (mergeResult.conflicts.length > 0) {
424
+ for (const conflict of mergeResult.conflicts) {
425
+ errors.push({
426
+ artifactName: conflict.name,
427
+ harnessName: "workspace",
428
+ message: `Artifact name conflict: "${conflict.name}" found in multiple sources: ${conflict.sources.join(", ")}`,
429
+ });
430
+ }
431
+ return { artifactsCompiled, filesWritten, warnings, errors };
432
+ }
433
+
434
+ // Load shared MCP servers
435
+ const sharedMcp = await loadSharedMcpServers(resolve(wsRoot, mcpServersDir));
436
+
437
+ // Create template environment
438
+ const templateEnv = createTemplateEnv(templatesDir);
439
+
440
+ // Clear dist
441
+ if (harness) {
442
+ const harnessDistDir = join(distDir, harness);
443
+ if (await exists(harnessDistDir)) {
444
+ await rm(harnessDistDir, { recursive: true });
445
+ }
446
+ } else {
447
+ if (await exists(distDir)) {
448
+ await rm(distDir, { recursive: true });
449
+ }
450
+ }
451
+
452
+ // Collect all artifact paths from resolved sources
453
+ const artifactPaths = await collectArtifactPaths(resolvedSources);
454
+
455
+ if (artifactPaths.length === 0) {
456
+ return { artifactsCompiled: 0, filesWritten: 0, warnings, errors };
457
+ }
458
+
459
+ // Load all artifacts
460
+ const loadedArtifacts = new Map<string, KnowledgeArtifact>();
461
+ for (const artifactPath of artifactPaths) {
462
+ const parseResult = await loadKnowledgeArtifact(artifactPath);
463
+ if (isParseError(parseResult)) {
464
+ const artifactName = artifactPath.split("/").pop() ?? artifactPath;
465
+ for (const err of parseResult.errors) {
466
+ errors.push({
467
+ artifactName,
468
+ harnessName: "parse",
469
+ message: err.message,
470
+ });
471
+ }
472
+ continue;
473
+ }
474
+ loadedArtifacts.set(parseResult.data.name, parseResult.data);
475
+ }
476
+
477
+ // For each project, compile only matching artifacts for the project's harnesses
478
+ for (const project of wsConfig.projects) {
479
+ const allArtifactNames = [...loadedArtifacts.keys()];
480
+ const projectArtifactNames = filterArtifactsForProject(
481
+ allArtifactNames,
482
+ project,
483
+ );
484
+
485
+ // Determine target harnesses for this project
486
+ const projectHarnesses = harness
487
+ ? project.harnesses.includes(harness)
488
+ ? [harness]
489
+ : []
490
+ : project.harnesses;
491
+
492
+ if (projectHarnesses.length === 0) continue;
493
+
494
+ for (const artifactName of projectArtifactNames) {
495
+ const artifact = loadedArtifacts.get(artifactName);
496
+ if (!artifact) continue;
497
+
498
+ // Clone the artifact to avoid mutating the shared instance across projects
499
+ const projectArtifact: KnowledgeArtifact = {
500
+ ...artifact,
501
+ frontmatter: { ...artifact.frontmatter },
502
+ mcpServers: [...artifact.mcpServers],
503
+ hooks: [...artifact.hooks],
504
+ };
505
+
506
+ // Merge shared MCP servers (artifact-local takes precedence)
507
+ const localMcpNames = new Set(
508
+ projectArtifact.mcpServers.map((s) => s.name),
509
+ );
510
+ for (const [name, server] of sharedMcp) {
511
+ if (!localMcpNames.has(name)) {
512
+ projectArtifact.mcpServers.push({
513
+ name,
514
+ ...server,
515
+ } as McpServerDefinition);
516
+ }
517
+ }
518
+
519
+ // Filter target harnesses to those the artifact actually supports
520
+ const artifactHarnesses = projectHarnesses.filter((h) =>
521
+ projectArtifact.frontmatter.harnesses.includes(h),
522
+ );
523
+
524
+ if (artifactHarnesses.length === 0) continue;
525
+
526
+ artifactsCompiled++;
527
+
528
+ // Resolve version for embedding
529
+ const artifactVersion = projectArtifact.frontmatter.version;
530
+
531
+ // Warn about default version only for artifacts mature enough to need explicit versioning
532
+ if (
533
+ artifactVersion === "0.1.0" &&
534
+ projectArtifact.frontmatter.maturity !== "experimental"
535
+ ) {
536
+ warnings.push({
537
+ artifactName: projectArtifact.name,
538
+ harnessName: "build",
539
+ message: `Maturity is "${projectArtifact.frontmatter.maturity}" but version is still the default 0.1.0. Consider setting an explicit version.`,
540
+ });
541
+ }
542
+
543
+ for (const h of artifactHarnesses) {
544
+ const adapter = adapterRegistry[h];
545
+ if (!adapter) continue;
546
+
547
+ // Apply project overrides before compilation
548
+ applyProjectOverrides(projectArtifact, project, h);
549
+
550
+ // projectArtifact is shared across harness iterations; build a
551
+ // per-harness clone so the resolved body doesn't leak across h.
552
+ const harnessArtifact: KnowledgeArtifact = {
553
+ ...projectArtifact,
554
+ body: resolveBody(projectArtifact, h),
555
+ };
556
+
557
+ // Compatibility check
558
+ const compat = getCompatibility(projectArtifact.frontmatter.type, h);
559
+ if (compat === "none") {
560
+ const msg = `Asset type "${projectArtifact.frontmatter.type}" has no output for harness "${h}" — skipping`;
561
+ if (strict) {
562
+ errors.push({
563
+ artifactName: projectArtifact.name,
564
+ harnessName: h,
565
+ message: msg,
566
+ });
567
+ } else {
568
+ warnings.push({
569
+ artifactName: projectArtifact.name,
570
+ harnessName: h,
571
+ message: msg,
572
+ });
573
+ }
574
+ continue;
575
+ }
576
+ if (compat === "partial") {
577
+ warnings.push({
578
+ artifactName: projectArtifact.name,
579
+ harnessName: h,
580
+ message: `Asset type "${projectArtifact.frontmatter.type}" has partial support in harness "${h}" — output may be degraded`,
581
+ });
582
+ }
583
+
584
+ try {
585
+ const adapterContext: AdapterContext = {
586
+ capabilities: getCapabilities(h),
587
+ strict: strict ?? false,
588
+ };
589
+ const result = adapter(harnessArtifact, templateEnv, adapterContext);
590
+ warnings.push(...result.warnings);
591
+
592
+ // Aggregate adapter errors into build errors
593
+ for (const adapterErr of result.errors ?? []) {
594
+ errors.push({
595
+ artifactName: adapterErr.artifactName,
596
+ harnessName: adapterErr.harnessName,
597
+ message: adapterErr.message,
598
+ });
599
+ }
600
+
601
+ // Write output files
602
+ // Skip version embedding for Kiro power format to match
603
+ // the official Kiro powers structure (no _forgeVersion in
604
+ // mcp.json, no forge:version HTML comments in .md files)
605
+ const skipVersionEmbed1 =
606
+ h === "kiro" &&
607
+ (() => {
608
+ const hcRaw = (
609
+ projectArtifact.frontmatter as Record<string, unknown>
610
+ )["harness-config"] as Record<string, unknown> | undefined;
611
+ const kcRaw = (hcRaw?.kiro ?? {}) as Record<string, unknown>;
612
+ return resolveFormat("kiro", kcRaw).format === "power";
613
+ })();
614
+ for (const file of result.files) {
615
+ let content = file.content;
616
+ if (!skipVersionEmbed1) {
617
+ if (file.relativePath.endsWith(".md")) {
618
+ content = embedVersion(content, artifactVersion, "markdown");
619
+ } else if (file.relativePath.endsWith(".json")) {
620
+ content = embedVersion(content, artifactVersion, "json");
621
+ }
622
+ }
623
+
624
+ const outPath = join(
625
+ distDir,
626
+ h,
627
+ projectArtifact.name,
628
+ file.relativePath,
629
+ );
630
+ const outDir = outPath.substring(0, outPath.lastIndexOf("/"));
631
+ await mkdir(outDir, { recursive: true });
632
+ await writeFile(outPath, content, "utf-8");
633
+ if (file.executable) {
634
+ await chmod(outPath, 0o755);
635
+ }
636
+ filesWritten++;
637
+
638
+ // Track Kiro steering files for inclusion summary
639
+ if (h === "kiro" && file.relativePath.endsWith(".md")) {
640
+ const isMainSteering =
641
+ file.relativePath === `${projectArtifact.name}.md`;
642
+ const isPowerSteering =
643
+ file.relativePath === `steering/${projectArtifact.name}.md`;
644
+ if (isMainSteering || isPowerSteering) {
645
+ const harnessConfigRaw = (
646
+ projectArtifact.frontmatter as Record<string, unknown>
647
+ )["harness-config"] as Record<string, unknown> | undefined;
648
+ const kiroConfigRaw = (harnessConfigRaw?.kiro ?? {}) as Record<
649
+ string,
650
+ unknown
651
+ >;
652
+ const resolved = resolveKiroInclusion(projectArtifact);
653
+ const { format } = resolveFormat("kiro", kiroConfigRaw);
654
+ kiroSummaryEntries.push({
655
+ artifactName: projectArtifact.name,
656
+ mode: resolved.mode,
657
+ format: format as "steering" | "power",
658
+ });
659
+ }
660
+ }
661
+ }
662
+ } catch (e: unknown) {
663
+ const msg = e instanceof Error ? e.message : String(e);
664
+ errors.push({
665
+ artifactName: projectArtifact.name,
666
+ harnessName: h,
667
+ message: msg,
668
+ });
669
+ console.error(
670
+ chalk.red(`Error: ${projectArtifact.name}/${h}: ${msg}`),
671
+ );
672
+ }
673
+ }
674
+ }
675
+ }
676
+
677
+ // Compute and print Kiro inclusion summary (Req 5.1, 5.2, 5.3, 5.4, 5.5)
678
+ let kiroInclusionSummary: BuildResult["kiroInclusionSummary"];
679
+ if (kiroSummaryEntries.length > 0) {
680
+ kiroInclusionSummary = computeKiroInclusionSummary(kiroSummaryEntries);
681
+ printKiroInclusionSummary(kiroInclusionSummary);
682
+
683
+ // Threshold warning (Req 6.1, 6.2, 6.3, 6.4, 6.5)
684
+ const threshold = options.kiroAlwaysWarnThreshold ?? 0.5;
685
+ const total = kiroInclusionSummary.total;
686
+ const alwaysCount = kiroInclusionSummary.byMode.always;
687
+ if (threshold !== 1 && total >= 2 && alwaysCount / total > threshold) {
688
+ const alwaysArtifacts = kiroInclusionSummary.contributingArtifacts.always;
689
+ for (const name of alwaysArtifacts) {
690
+ if (strict) {
691
+ errors.push({
692
+ artifactName: name,
693
+ harnessName: "kiro",
694
+ message: `Always-on share exceeds threshold (${((alwaysCount / total) * 100).toFixed(0)}% > ${(threshold * 100).toFixed(0)}%): consider using fileMatch or manual inclusion`,
695
+ });
696
+ } else {
697
+ warnings.push({
698
+ artifactName: name,
699
+ harnessName: "kiro",
700
+ message: `Always-on share exceeds threshold (${((alwaysCount / total) * 100).toFixed(0)}% > ${(threshold * 100).toFixed(0)}%): consider using fileMatch or manual inclusion`,
701
+ });
702
+ }
703
+ }
704
+ }
705
+ }
706
+
707
+ return {
708
+ artifactsCompiled,
709
+ filesWritten,
710
+ warnings,
711
+ errors,
712
+ kiroInclusionSummary,
713
+ };
714
+ }
715
+
716
+ // ═══════════════════════════════════════════════════════════════════════════════
717
+ // Build Orchestration — Standard (Non-Workspace)
718
+ // ═══════════════════════════════════════════════════════════════════════════════
719
+
720
+ /**
721
+ * Build all artifacts from source directories into harness-specific output.
722
+ *
723
+ * This is the main build entry point. It:
724
+ * - Delegates canonical parsing to Rosetta Stone (via loadKnowledgeArtifact → parseCanonical)
725
+ * - Delegates target translation to Rosetta Stone (via adapterRegistry → target translators)
726
+ * - Retains imperative concerns: scanning, dependency composition, shared MCP merge,
727
+ * workspace overrides, dist policy, summaries, version embedding, and file writes
728
+ *
729
+ * Requirements: 1.3, 12.1, 12.2, 14.5, 14.10
730
+ */
731
+ export async function build(options: BuildOptions): Promise<BuildResult> {
732
+ // Resolve source dirs — support both new knowledgeDirs and legacy knowledgeDir
733
+ const sourceDirs =
734
+ options.knowledgeDirs && options.knowledgeDirs.length > 0
735
+ ? options.knowledgeDirs
736
+ : options.knowledgeDir
737
+ ? [options.knowledgeDir]
738
+ : ["knowledge"];
739
+
740
+ const {
741
+ distDir,
742
+ templatesDir,
743
+ mcpServersDir,
744
+ harness,
745
+ strict,
746
+ workspaceRoot,
747
+ } = options;
748
+
749
+ // Check for workspace config — if present, delegate to workspace-aware build
750
+ const wsRoot = workspaceRoot ?? process.cwd();
751
+ const wsResult = await loadWorkspaceConfig(wsRoot);
752
+ if (wsResult) {
753
+ return buildWithWorkspace(wsResult.config, wsRoot, options);
754
+ }
755
+
756
+ // Fall back to existing single-directory behavior
757
+ const warnings: AdapterWarning[] = [];
758
+ const errors: BuildError[] = [];
759
+ let filesWritten = 0;
760
+ let artifactsCompiled = 0;
761
+ const kiroSummaryEntries: KiroSummaryEntry[] = [];
762
+
763
+ // Load shared MCP servers
764
+ const sharedMcp = await loadSharedMcpServers(mcpServersDir);
765
+
766
+ // Create template environment
767
+ const templateEnv = createTemplateEnv(templatesDir);
768
+
769
+ // Clear dist
770
+ if (harness) {
771
+ const harnessDistDir = join(distDir, harness);
772
+ if (await exists(harnessDistDir)) {
773
+ await rm(harnessDistDir, { recursive: true });
774
+ }
775
+ } else {
776
+ if (await exists(distDir)) {
777
+ await rm(distDir, { recursive: true });
778
+ }
779
+ }
780
+
781
+ // Collect all artifact paths from all source dirs
782
+ const artifactPaths = await collectArtifactPaths(sourceDirs);
783
+
784
+ if (artifactPaths.length === 0) {
785
+ return { artifactsCompiled: 0, filesWritten: 0, warnings, errors };
786
+ }
787
+
788
+ for (const artifactPath of artifactPaths) {
789
+ const parseResult = await loadKnowledgeArtifact(artifactPath);
790
+ if (isParseError(parseResult)) {
791
+ const artifactName = artifactPath.split("/").pop() ?? artifactPath;
792
+ for (const err of parseResult.errors) {
793
+ errors.push({
794
+ artifactName,
795
+ harnessName: "parse",
796
+ message: err.message,
797
+ });
798
+ }
799
+ continue;
800
+ }
801
+
802
+ const artifact = parseResult.data;
803
+
804
+ // Dependency composition for workflow and agent types
805
+ const fm = artifact.frontmatter;
806
+ if (
807
+ (fm.type === "workflow" || fm.type === "agent") &&
808
+ fm.depends.length > 0
809
+ ) {
810
+ const compositionResult = await resolveComposition(artifact, sourceDirs);
811
+ if (compositionResult.cycleError) {
812
+ errors.push({
813
+ artifactName: artifact.name,
814
+ harnessName: "compose",
815
+ message: compositionResult.cycleError,
816
+ });
817
+ continue;
818
+ }
819
+ // Merge dependency mcpServers (artifact-local takes precedence)
820
+ const localMcpNamesComp = new Set(artifact.mcpServers.map((s) => s.name));
821
+ for (const depServer of compositionResult.mcpServers) {
822
+ if (!localMcpNamesComp.has(depServer.name)) {
823
+ artifact.mcpServers.push(depServer);
824
+ }
825
+ }
826
+ // Merge hooks only if artifact opts in
827
+ if (fm["inherit-hooks"]) {
828
+ const localHookNames = new Set(artifact.hooks.map((h) => h.name));
829
+ for (const depHook of compositionResult.hooks) {
830
+ if (!localHookNames.has(depHook.name)) {
831
+ artifact.hooks.push(depHook);
832
+ }
833
+ }
834
+ }
835
+ }
836
+
837
+ // Merge shared MCP servers (artifact-local takes precedence)
838
+ const localMcpNames = new Set(artifact.mcpServers.map((s) => s.name));
839
+ for (const [name, server] of sharedMcp) {
840
+ if (!localMcpNames.has(name)) {
841
+ artifact.mcpServers.push({ name, ...server } as McpServerDefinition);
842
+ }
843
+ }
844
+
845
+ // Determine target harnesses
846
+ const targetHarnesses = harness
847
+ ? artifact.frontmatter.harnesses.includes(harness)
848
+ ? [harness]
849
+ : []
850
+ : artifact.frontmatter.harnesses;
851
+
852
+ if (targetHarnesses.length === 0) continue;
853
+
854
+ artifactsCompiled++;
855
+
856
+ // Resolve version for embedding
857
+ const artifactVersion = artifact.frontmatter.version;
858
+
859
+ // Warn about default version only for artifacts mature enough to need explicit versioning
860
+ if (
861
+ artifactVersion === "0.1.0" &&
862
+ artifact.frontmatter.maturity !== "experimental"
863
+ ) {
864
+ warnings.push({
865
+ artifactName: artifact.name,
866
+ harnessName: "build",
867
+ message: `Maturity is "${artifact.frontmatter.maturity}" but version is still the default 0.1.0. Consider setting an explicit version.`,
868
+ });
869
+ }
870
+
871
+ for (const h of targetHarnesses) {
872
+ const adapter = adapterRegistry[h];
873
+ if (!adapter) continue;
874
+
875
+ // Compatibility check
876
+ const compat = getCompatibility(artifact.frontmatter.type, h);
877
+ if (compat === "none") {
878
+ const msg = `Asset type "${artifact.frontmatter.type}" has no output for harness "${h}" — skipping`;
879
+ if (strict) {
880
+ errors.push({
881
+ artifactName: artifact.name,
882
+ harnessName: h,
883
+ message: msg,
884
+ });
885
+ } else {
886
+ warnings.push({
887
+ artifactName: artifact.name,
888
+ harnessName: h,
889
+ message: msg,
890
+ });
891
+ }
892
+ continue;
893
+ }
894
+ if (compat === "partial") {
895
+ warnings.push({
896
+ artifactName: artifact.name,
897
+ harnessName: h,
898
+ message: `Asset type "${artifact.frontmatter.type}" has partial support in harness "${h}" — output may be degraded`,
899
+ });
900
+ }
901
+
902
+ try {
903
+ const adapterContext: AdapterContext = {
904
+ capabilities: getCapabilities(h),
905
+ strict: strict ?? false,
906
+ };
907
+ // artifact is shared across harness iterations; build a
908
+ // per-harness clone so the resolved body doesn't leak across h.
909
+ const harnessArtifact: KnowledgeArtifact = {
910
+ ...artifact,
911
+ body: resolveBody(artifact, h),
912
+ };
913
+ const result = adapter(harnessArtifact, templateEnv, adapterContext);
914
+ warnings.push(...result.warnings);
915
+
916
+ // Aggregate adapter errors into build errors
917
+ for (const adapterErr of result.errors ?? []) {
918
+ errors.push({
919
+ artifactName: adapterErr.artifactName,
920
+ harnessName: adapterErr.harnessName,
921
+ message: adapterErr.message,
922
+ });
923
+ }
924
+
925
+ // Write output files — dist path uses leaf artifact name (not scoped @org/name)
926
+ // Skip version embedding for Kiro power format to match
927
+ // the official Kiro powers structure (no _forgeVersion in
928
+ // mcp.json, no forge:version HTML comments in .md files)
929
+ const skipVersionEmbed2 =
930
+ h === "kiro" &&
931
+ (() => {
932
+ const hcRaw = (artifact.frontmatter as Record<string, unknown>)[
933
+ "harness-config"
934
+ ] as Record<string, unknown> | undefined;
935
+ const kcRaw = (hcRaw?.kiro ?? {}) as Record<string, unknown>;
936
+ return resolveFormat("kiro", kcRaw).format === "power";
937
+ })();
938
+ for (const file of result.files) {
939
+ // Embed version in markdown and JSON files
940
+ let content = file.content;
941
+ if (!skipVersionEmbed2) {
942
+ if (file.relativePath.endsWith(".md")) {
943
+ content = embedVersion(content, artifactVersion, "markdown");
944
+ } else if (file.relativePath.endsWith(".json")) {
945
+ content = embedVersion(content, artifactVersion, "json");
946
+ }
947
+ }
948
+
949
+ const outPath = join(distDir, h, artifact.name, file.relativePath);
950
+ const outDir = outPath.substring(0, outPath.lastIndexOf("/"));
951
+ await mkdir(outDir, { recursive: true });
952
+ await writeFile(outPath, content, "utf-8");
953
+ if (file.executable) {
954
+ await chmod(outPath, 0o755);
955
+ }
956
+ filesWritten++;
957
+
958
+ // Track Kiro steering files for inclusion summary
959
+ if (h === "kiro" && file.relativePath.endsWith(".md")) {
960
+ const isMainSteering = file.relativePath === `${artifact.name}.md`;
961
+ const isPowerSteering =
962
+ file.relativePath === `steering/${artifact.name}.md`;
963
+ if (isMainSteering || isPowerSteering) {
964
+ const harnessConfigRaw = (
965
+ artifact.frontmatter as Record<string, unknown>
966
+ )["harness-config"] as Record<string, unknown> | undefined;
967
+ const kiroConfigRaw = (harnessConfigRaw?.kiro ?? {}) as Record<
968
+ string,
969
+ unknown
970
+ >;
971
+ const resolved = resolveKiroInclusion(artifact);
972
+ const { format } = resolveFormat("kiro", kiroConfigRaw);
973
+ kiroSummaryEntries.push({
974
+ artifactName: artifact.name,
975
+ mode: resolved.mode,
976
+ format: format as "steering" | "power",
977
+ });
978
+ }
979
+ }
980
+ }
981
+ } catch (e: unknown) {
982
+ const msg = e instanceof Error ? e.message : String(e);
983
+ errors.push({
984
+ artifactName: artifact.name,
985
+ harnessName: h,
986
+ message: msg,
987
+ });
988
+ console.error(chalk.red(`Error: ${artifact.name}/${h}: ${msg}`));
989
+ }
990
+ }
991
+ }
992
+
993
+ // Compute and print Kiro inclusion summary (Req 5.1, 5.2, 5.3, 5.4, 5.5)
994
+ let kiroInclusionSummary: BuildResult["kiroInclusionSummary"];
995
+ if (kiroSummaryEntries.length > 0) {
996
+ kiroInclusionSummary = computeKiroInclusionSummary(kiroSummaryEntries);
997
+ printKiroInclusionSummary(kiroInclusionSummary);
998
+
999
+ // Threshold warning (Req 6.1, 6.2, 6.3, 6.4, 6.5)
1000
+ const threshold = options.kiroAlwaysWarnThreshold ?? 0.5;
1001
+ const total = kiroInclusionSummary.total;
1002
+ const alwaysCount = kiroInclusionSummary.byMode.always;
1003
+ if (threshold !== 1 && total >= 2 && alwaysCount / total > threshold) {
1004
+ const alwaysArtifacts = kiroInclusionSummary.contributingArtifacts.always;
1005
+ for (const name of alwaysArtifacts) {
1006
+ if (strict) {
1007
+ errors.push({
1008
+ artifactName: name,
1009
+ harnessName: "kiro",
1010
+ message: `Always-on share exceeds threshold (${((alwaysCount / total) * 100).toFixed(0)}% > ${(threshold * 100).toFixed(0)}%): consider using fileMatch or manual inclusion`,
1011
+ });
1012
+ } else {
1013
+ warnings.push({
1014
+ artifactName: name,
1015
+ harnessName: "kiro",
1016
+ message: `Always-on share exceeds threshold (${((alwaysCount / total) * 100).toFixed(0)}% > ${(threshold * 100).toFixed(0)}%): consider using fileMatch or manual inclusion`,
1017
+ });
1018
+ }
1019
+ }
1020
+ }
1021
+ }
1022
+
1023
+ return {
1024
+ artifactsCompiled,
1025
+ filesWritten,
1026
+ warnings,
1027
+ errors,
1028
+ kiroInclusionSummary,
1029
+ };
1030
+ }
1031
+
1032
+ export const SOURCE_DIRS = ["knowledge", "packages"] as const;
1033
+
1034
+ export async function buildCommand(options: {
1035
+ harness?: string;
1036
+ strict?: boolean;
1037
+ }): Promise<void> {
1038
+ const knowledgeDirs = [...SOURCE_DIRS];
1039
+ const distDir = "dist";
1040
+ const templatesDir = "templates/harness-adapters";
1041
+ const mcpServersDir = "mcp-servers";
1042
+
1043
+ // Load forge config to extract threshold (Req 6.2)
1044
+ const config = await loadForgeConfig();
1045
+ const kiroAlwaysWarnThreshold =
1046
+ config.kiro?.progressiveSteering?.alwaysWarnThreshold ?? 0.5;
1047
+
1048
+ // Validate harness name if provided
1049
+ if (options.harness) {
1050
+ if (!(SUPPORTED_HARNESSES as readonly string[]).includes(options.harness)) {
1051
+ console.error(
1052
+ chalk.red(
1053
+ `Error: Unknown harness "${options.harness}". Valid harnesses: ${SUPPORTED_HARNESSES.join(", ")}`,
1054
+ ),
1055
+ );
1056
+ process.exit(1);
1057
+ }
1058
+ }
1059
+
1060
+ // Check that at least one source directory exists
1061
+ const anyExists = await Promise.any(
1062
+ knowledgeDirs.map((d) =>
1063
+ exists(d).then((e) => {
1064
+ if (!e) throw new Error();
1065
+ return e;
1066
+ }),
1067
+ ),
1068
+ ).catch(() => false);
1069
+ if (!anyExists) {
1070
+ console.error(
1071
+ chalk.yellow(
1072
+ "No knowledge/ or packages/ directory found. Run `kanon new <name>` to create your first artifact.",
1073
+ ),
1074
+ );
1075
+ process.exit(1);
1076
+ }
1077
+
1078
+ const result = await build({
1079
+ knowledgeDirs,
1080
+ distDir,
1081
+ templatesDir,
1082
+ mcpServersDir,
1083
+ harness: options.harness as HarnessName | undefined,
1084
+ strict: options.strict,
1085
+ kiroAlwaysWarnThreshold,
1086
+ });
1087
+
1088
+ // Print summary
1089
+ console.error(
1090
+ chalk.green(
1091
+ `\n✓ Build complete: ${result.artifactsCompiled} artifacts, ${result.filesWritten} files written`,
1092
+ ),
1093
+ );
1094
+ if (options.harness) {
1095
+ console.error(` Harness: ${options.harness}`);
1096
+ }
1097
+ for (const w of result.warnings) {
1098
+ console.error(
1099
+ chalk.yellow(
1100
+ ` Warning: ${w.artifactName}/${w.harnessName}: ${w.message}`,
1101
+ ),
1102
+ );
1103
+ }
1104
+ if (result.errors.length > 0) {
1105
+ console.error(chalk.red(` ${result.errors.length} error(s) encountered`));
1106
+ process.exit(1);
1107
+ }
1108
+ }