@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
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
|
+
}
|