@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/temper.ts ADDED
@@ -0,0 +1,747 @@
1
+ import { exists, readdir } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import chalk from "chalk";
4
+ import type { HarnessCapabilityName } from "./adapters/capabilities";
5
+ import {
6
+ getCapabilities,
7
+ getDegradation,
8
+ HARNESS_CAPABILITIES,
9
+ } from "./adapters/capabilities";
10
+ import { adapterRegistry } from "./adapters/index";
11
+ import type { AdapterContext } from "./adapters/types";
12
+ import { documentsAgentLoop } from "./asset-conventions";
13
+ import { generateCatalog, SOURCE_DIRS } from "./catalog";
14
+ import { isParseError, loadKnowledgeArtifact } from "./parser";
15
+ import type {
16
+ HarnessName,
17
+ KnowledgeArtifact,
18
+ TemperOutput,
19
+ TemperSection,
20
+ } from "./schemas";
21
+ import { isStdioServer, SUPPORTED_HARNESSES } from "./schemas";
22
+ import { createTemplateEnv } from "./template-engine";
23
+
24
+ export interface TemperOptions {
25
+ artifactName: string;
26
+ harness: HarnessName;
27
+ noColor?: boolean;
28
+ knowledgeDirs?: string[];
29
+ templatesDir?: string;
30
+ }
31
+
32
+ /**
33
+ * Collect all artifact paths from one or more source directories.
34
+ * Mirrors the logic in build.ts for locating artifacts.
35
+ */
36
+ async function collectArtifactPaths(sourceDirs: string[]): Promise<string[]> {
37
+ const paths: string[] = [];
38
+
39
+ for (const sourceDir of sourceDirs) {
40
+ if (!(await exists(sourceDir))) continue;
41
+
42
+ const dirEntries = await readdir(sourceDir, { withFileTypes: true });
43
+ const subdirs = dirEntries
44
+ .filter((e) => e.isDirectory())
45
+ .sort((a, b) => a.name.localeCompare(b.name));
46
+
47
+ for (const subdir of subdirs) {
48
+ const subdirPath = join(sourceDir, subdir.name);
49
+
50
+ if (await exists(join(subdirPath, "knowledge.md"))) {
51
+ paths.push(subdirPath);
52
+ } else {
53
+ // Namespaced layout — recurse one level
54
+ const inner = await readdir(subdirPath, { withFileTypes: true });
55
+ const innerDirs = inner
56
+ .filter((e) => e.isDirectory())
57
+ .sort((a, b) => a.name.localeCompare(b.name));
58
+
59
+ for (const innerDir of innerDirs) {
60
+ const artifactPath = join(subdirPath, innerDir.name);
61
+ if (await exists(join(artifactPath, "knowledge.md"))) {
62
+ paths.push(artifactPath);
63
+ }
64
+ }
65
+ }
66
+ }
67
+ }
68
+
69
+ return paths;
70
+ }
71
+
72
+ /**
73
+ * Find an artifact by name from the knowledge directories.
74
+ */
75
+ async function findArtifact(
76
+ artifactName: string,
77
+ knowledgeDirs: string[],
78
+ ): Promise<KnowledgeArtifact | null> {
79
+ const artifactPaths = await collectArtifactPaths(knowledgeDirs);
80
+
81
+ for (const artifactPath of artifactPaths) {
82
+ const result = await loadKnowledgeArtifact(artifactPath);
83
+ if (isParseError(result)) continue;
84
+ if (result.data.name === artifactName) {
85
+ return result.data;
86
+ }
87
+ }
88
+
89
+ return null;
90
+ }
91
+
92
+ /**
93
+ * Render a system prompt section using eval-contexts templates.
94
+ * Falls back to raw steering content if no template exists for the harness.
95
+ */
96
+ function renderSystemPrompt(
97
+ harness: HarnessName,
98
+ steeringContent: string,
99
+ templatesDir: string,
100
+ ): string {
101
+ const evalContextsDir = join(templatesDir, "../eval-contexts");
102
+ const templateEnv = createTemplateEnv(evalContextsDir);
103
+
104
+ try {
105
+ const rendered = templateEnv.render(`${harness}.md.njk`, {
106
+ prompt: steeringContent,
107
+ user_query: "[user query placeholder]",
108
+ });
109
+ return rendered;
110
+ } catch {
111
+ // Template not found — return raw content
112
+ return steeringContent;
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Compile an artifact for a harness and produce a TemperOutput with all sections.
118
+ *
119
+ * Sections produced:
120
+ * - system-prompt: from eval-contexts template wrapping the compiled steering
121
+ * - steering: the main compiled markdown body
122
+ * - hooks: translated hooks (or degraded hooks info)
123
+ * - mcp-servers: MCP server definitions
124
+ * - degradation-report: when any capabilities were degraded
125
+ */
126
+ export async function renderTemper(
127
+ options: TemperOptions,
128
+ ): Promise<TemperOutput> {
129
+ const {
130
+ artifactName,
131
+ harness,
132
+ knowledgeDirs = [...SOURCE_DIRS],
133
+ templatesDir = "templates/harness-adapters",
134
+ } = options;
135
+
136
+ // 1. Load the artifact from knowledge dirs
137
+ const artifact = await findArtifact(artifactName, knowledgeDirs);
138
+ if (!artifact) {
139
+ // Return an error output with available artifacts listed
140
+ const catalog = await generateCatalog(knowledgeDirs);
141
+ const available = catalog.map((e) => e.name).join(", ");
142
+ return {
143
+ artifactName,
144
+ harnessName: harness,
145
+ sections: [
146
+ {
147
+ title: "Error",
148
+ content: `Artifact "${artifactName}" not found. Available artifacts: ${available}`,
149
+ type: "steering",
150
+ },
151
+ ],
152
+ degradations: [],
153
+ fileCount: 0,
154
+ hooksTranslated: 0,
155
+ hooksDegraded: 0,
156
+ mcpServers: [],
157
+ };
158
+ }
159
+
160
+ // Check if harness is in the artifact's harnesses list
161
+ if (!artifact.frontmatter.harnesses.includes(harness)) {
162
+ return {
163
+ artifactName,
164
+ harnessName: harness,
165
+ sections: [
166
+ {
167
+ title: "Error",
168
+ content: `Artifact "${artifactName}" does not target harness "${harness}". Supported harnesses: ${artifact.frontmatter.harnesses.join(", ")}`,
169
+ type: "steering",
170
+ },
171
+ ],
172
+ degradations: [],
173
+ fileCount: 0,
174
+ hooksTranslated: 0,
175
+ hooksDegraded: 0,
176
+ mcpServers: [],
177
+ };
178
+ }
179
+
180
+ // 2. Compile it for the specified harness using the adapter
181
+ const adapter = adapterRegistry[harness];
182
+ const templateEnv = createTemplateEnv(templatesDir);
183
+ const capabilities = getCapabilities(harness);
184
+ const adapterContext: AdapterContext = {
185
+ capabilities,
186
+ strict: false,
187
+ };
188
+
189
+ const adapterResult = adapter(artifact, templateEnv, adapterContext);
190
+
191
+ // 3. Check capabilities and identify degradations
192
+ const degradations: string[] = [];
193
+ let hooksDegraded = 0;
194
+ let hooksTranslated = 0;
195
+
196
+ for (const cap of HARNESS_CAPABILITIES) {
197
+ const degradation = getDegradation(harness, cap as HarnessCapabilityName);
198
+ if (degradation) {
199
+ // Check if this capability is actually used by the artifact
200
+ const isUsed = isCapabilityUsed(artifact, cap as HarnessCapabilityName);
201
+ if (isUsed) {
202
+ degradations.push(`${cap}: ${degradation}`);
203
+ if (cap === "hooks") {
204
+ hooksDegraded = artifact.hooks.length;
205
+ }
206
+ }
207
+ }
208
+ }
209
+
210
+ // Count hooks translated (fully supported)
211
+ if (capabilities.hooks.support === "full") {
212
+ hooksTranslated = artifact.hooks.length;
213
+ } else if (capabilities.hooks.support === "partial") {
214
+ // Partial: some translated, some degraded
215
+ hooksTranslated = Math.ceil(artifact.hooks.length / 2);
216
+ hooksDegraded = artifact.hooks.length - hooksTranslated;
217
+ }
218
+
219
+ // 4. Build sections array
220
+ const sections: TemperSection[] = [];
221
+
222
+ // Get the main steering content from compiled files
223
+ const steeringContent = adapterResult.files
224
+ .filter((f) => f.relativePath.endsWith(".md"))
225
+ .map((f) => f.content)
226
+ .join("\n\n");
227
+
228
+ // System prompt section — use eval-contexts template
229
+ const systemPromptContent = renderSystemPrompt(
230
+ harness,
231
+ steeringContent,
232
+ templatesDir,
233
+ );
234
+ sections.push({
235
+ title: "System Prompt",
236
+ content: systemPromptContent,
237
+ type: "system-prompt",
238
+ });
239
+
240
+ // Steering section — the raw compiled body
241
+ sections.push({
242
+ title: "Steering",
243
+ content: steeringContent || artifact.body,
244
+ type: "steering",
245
+ });
246
+
247
+ // Hooks section
248
+ if (artifact.hooks.length > 0) {
249
+ const hooksContent = artifact.hooks
250
+ .map((hook) => {
251
+ const action =
252
+ hook.action.type === "ask_agent"
253
+ ? `Ask agent: ${hook.action.prompt}`
254
+ : `Run: ${hook.action.command}`;
255
+ const condition = hook.condition
256
+ ? ` (${hook.condition.file_patterns?.join(", ") || hook.condition.tool_types?.join(", ") || ""})`
257
+ : "";
258
+ return `- ${hook.name}: on ${hook.event}${condition} → ${action}`;
259
+ })
260
+ .join("\n");
261
+ sections.push({
262
+ title: "Hooks",
263
+ content: hooksContent,
264
+ type: "hooks",
265
+ });
266
+ }
267
+
268
+ // MCP servers section
269
+ if (artifact.mcpServers.length > 0) {
270
+ const mcpContent = artifact.mcpServers
271
+ .map((server) => {
272
+ if (isStdioServer(server)) {
273
+ const args =
274
+ server.args.length > 0 ? ` ${server.args.join(" ")}` : "";
275
+ const envStr =
276
+ Object.keys(server.env).length > 0
277
+ ? `\n env: ${JSON.stringify(server.env)}`
278
+ : "";
279
+ return `- ${server.name}: ${server.command}${args}${envStr}`;
280
+ }
281
+ return `- ${server.name}: ${server.url}`;
282
+ })
283
+ .join("\n");
284
+ sections.push({
285
+ title: "MCP Servers",
286
+ content: mcpContent,
287
+ type: "mcp-servers",
288
+ });
289
+ }
290
+
291
+ // Degradation report section (only when applicable)
292
+ if (degradations.length > 0) {
293
+ const reportContent = degradations.map((d) => `- ${d}`).join("\n");
294
+ sections.push({
295
+ title: "Degradation Report",
296
+ content: reportContent,
297
+ type: "degradation-report",
298
+ });
299
+ }
300
+
301
+ // 5. Return the TemperOutput
302
+ return {
303
+ artifactName,
304
+ harnessName: harness,
305
+ sections,
306
+ degradations,
307
+ fileCount: adapterResult.files.length,
308
+ hooksTranslated,
309
+ hooksDegraded,
310
+ mcpServers: artifact.mcpServers.map((s) => s.name),
311
+ };
312
+ }
313
+
314
+ /**
315
+ * Check if a capability is actually used by the artifact.
316
+ */
317
+ function isCapabilityUsed(
318
+ artifact: KnowledgeArtifact,
319
+ capability: HarnessCapabilityName,
320
+ ): boolean {
321
+ switch (capability) {
322
+ case "hooks":
323
+ return artifact.hooks.length > 0;
324
+ case "mcp":
325
+ return artifact.mcpServers.length > 0;
326
+ case "workflows":
327
+ return artifact.workflows.length > 0;
328
+ case "path_scoping":
329
+ return (
330
+ artifact.frontmatter.inclusion === "fileMatch" &&
331
+ (artifact.frontmatter.file_patterns?.length ?? 0) > 0
332
+ );
333
+ case "toggleable_rules":
334
+ return artifact.frontmatter.inclusion === "manual";
335
+ case "agents":
336
+ // Type-aware, not type-literal: an artifact whose body documents
337
+ // agent-loop behavior (goal/inputs/outputs/loop headings) gets
338
+ // degradation reporting even when its declared `type` is something
339
+ // else (e.g. "power") — see ADR-0050.
340
+ return (
341
+ artifact.frontmatter.type === "agent" ||
342
+ documentsAgentLoop(artifact.body)
343
+ );
344
+ case "file_match_inclusion":
345
+ return (artifact.frontmatter.file_patterns?.length ?? 0) > 0;
346
+ case "system_prompt_merging":
347
+ // Always relevant — steering content is always merged into system prompt
348
+ return true;
349
+ default:
350
+ return false;
351
+ }
352
+ }
353
+
354
+ // --- Task 10.2: Terminal Output Formatting ---
355
+
356
+ export interface ComparisonResult {
357
+ artifactName: string;
358
+ harnesses: Array<{
359
+ harnessName: string;
360
+ fileCount: number;
361
+ hooksTranslated: number;
362
+ hooksDegraded: number;
363
+ mcpServers: string[];
364
+ degradations: string[];
365
+ }>;
366
+ }
367
+
368
+ /**
369
+ * Format TemperOutput as terminal text with chalk syntax highlighting.
370
+ * When noColor is true, strips all ANSI codes for deterministic output.
371
+ */
372
+ export function formatTerminalOutput(
373
+ output: TemperOutput,
374
+ noColor?: boolean,
375
+ ): string {
376
+ const lines: string[] = [];
377
+
378
+ if (noColor) {
379
+ // Deterministic plain-text output
380
+ lines.push(
381
+ `=== Temper: ${output.artifactName} @ ${output.harnessName} ===`,
382
+ );
383
+ lines.push("");
384
+ lines.push(
385
+ `Files: ${output.fileCount} | Hooks translated: ${output.hooksTranslated} | Hooks degraded: ${output.hooksDegraded} | MCP servers: ${output.mcpServers.length}`,
386
+ );
387
+ lines.push("");
388
+
389
+ for (const section of output.sections) {
390
+ lines.push(`--- ${section.title} (${section.type}) ---`);
391
+ lines.push(section.content);
392
+ lines.push("");
393
+ }
394
+
395
+ return lines.join("\n");
396
+ }
397
+
398
+ // Colorized terminal output
399
+ lines.push(
400
+ chalk.bold.cyan(
401
+ `=== Temper: ${output.artifactName} @ ${output.harnessName} ===`,
402
+ ),
403
+ );
404
+ lines.push("");
405
+
406
+ const stats = [
407
+ `${chalk.dim("Files:")} ${chalk.white(String(output.fileCount))}`,
408
+ `${chalk.dim("Hooks translated:")} ${chalk.green(String(output.hooksTranslated))}`,
409
+ `${chalk.dim("Hooks degraded:")} ${output.hooksDegraded > 0 ? chalk.yellow(String(output.hooksDegraded)) : chalk.white("0")}`,
410
+ `${chalk.dim("MCP servers:")} ${chalk.white(String(output.mcpServers.length))}`,
411
+ ];
412
+ lines.push(stats.join(" | "));
413
+ lines.push("");
414
+
415
+ for (const section of output.sections) {
416
+ const typeColor = getSectionColor(section.type);
417
+ lines.push(
418
+ chalk.bold(typeColor(`--- ${section.title} (${section.type}) ---`)),
419
+ );
420
+ lines.push(section.content);
421
+ lines.push("");
422
+ }
423
+
424
+ return lines.join("\n");
425
+ }
426
+
427
+ function getSectionColor(type: string): (text: string) => string {
428
+ switch (type) {
429
+ case "system-prompt":
430
+ return chalk.blue;
431
+ case "steering":
432
+ return chalk.green;
433
+ case "hooks":
434
+ return chalk.magenta;
435
+ case "mcp-servers":
436
+ return chalk.cyan;
437
+ case "degradation-report":
438
+ return chalk.yellow;
439
+ default:
440
+ return chalk.white;
441
+ }
442
+ }
443
+
444
+ // --- Task 10.3: JSON Output Mode ---
445
+
446
+ /**
447
+ * Serialize TemperOutput as valid JSON conforming to TemperOutputSchema.
448
+ */
449
+ export function formatJsonOutput(output: TemperOutput): string {
450
+ return JSON.stringify(output, null, 2);
451
+ }
452
+
453
+ // --- Task 10.4: Side-by-Side Comparison ---
454
+
455
+ export interface RenderComparisonOptions {
456
+ artifactName: string;
457
+ harnesses: HarnessName[];
458
+ knowledgeDirs?: string[];
459
+ templatesDir?: string;
460
+ }
461
+
462
+ /**
463
+ * Compile artifact for multiple harnesses and produce a comparison summary.
464
+ */
465
+ export async function renderComparison(
466
+ options: RenderComparisonOptions,
467
+ ): Promise<ComparisonResult> {
468
+ const {
469
+ artifactName,
470
+ harnesses,
471
+ knowledgeDirs = [...SOURCE_DIRS],
472
+ templatesDir = "templates/harness-adapters",
473
+ } = options;
474
+
475
+ const harnessResults: ComparisonResult["harnesses"] = [];
476
+
477
+ for (const harness of harnesses) {
478
+ const output = await renderTemper({
479
+ artifactName,
480
+ harness,
481
+ knowledgeDirs,
482
+ templatesDir,
483
+ });
484
+
485
+ harnessResults.push({
486
+ harnessName: harness,
487
+ fileCount: output.fileCount,
488
+ hooksTranslated: output.hooksTranslated,
489
+ hooksDegraded: output.hooksDegraded,
490
+ mcpServers: output.mcpServers,
491
+ degradations: output.degradations,
492
+ });
493
+ }
494
+
495
+ return {
496
+ artifactName,
497
+ harnesses: harnessResults,
498
+ };
499
+ }
500
+
501
+ /**
502
+ * Format a ComparisonResult as a terminal-friendly table string.
503
+ */
504
+ export function formatComparisonOutput(
505
+ result: ComparisonResult,
506
+ noColor?: boolean,
507
+ ): string {
508
+ const lines: string[] = [];
509
+
510
+ if (noColor) {
511
+ lines.push(`=== Comparison: ${result.artifactName} ===`);
512
+ lines.push("");
513
+
514
+ // Header
515
+ const headers = [
516
+ "Harness",
517
+ "Files",
518
+ "Hooks OK",
519
+ "Hooks Degraded",
520
+ "MCP Servers",
521
+ "Degradations",
522
+ ];
523
+ lines.push(headers.join(" | "));
524
+ lines.push(headers.map((h) => "-".repeat(h.length)).join("-+-"));
525
+
526
+ for (const h of result.harnesses) {
527
+ const row = [
528
+ h.harnessName.padEnd(headers[0].length),
529
+ String(h.fileCount).padEnd(headers[1].length),
530
+ String(h.hooksTranslated).padEnd(headers[2].length),
531
+ String(h.hooksDegraded).padEnd(headers[3].length),
532
+ String(h.mcpServers.length).padEnd(headers[4].length),
533
+ h.degradations.length > 0 ? h.degradations.join("; ") : "none",
534
+ ];
535
+ lines.push(row.join(" | "));
536
+ }
537
+ } else {
538
+ lines.push(chalk.bold.cyan(`=== Comparison: ${result.artifactName} ===`));
539
+ lines.push("");
540
+
541
+ for (const h of result.harnesses) {
542
+ lines.push(chalk.bold.white(` ${h.harnessName}`));
543
+ lines.push(` ${chalk.dim("Files:")} ${h.fileCount}`);
544
+ lines.push(
545
+ ` ${chalk.dim("Hooks translated:")} ${chalk.green(String(h.hooksTranslated))}`,
546
+ );
547
+ lines.push(
548
+ ` ${chalk.dim("Hooks degraded:")} ${h.hooksDegraded > 0 ? chalk.yellow(String(h.hooksDegraded)) : "0"}`,
549
+ );
550
+ lines.push(
551
+ ` ${chalk.dim("MCP servers:")} ${h.mcpServers.join(", ") || "none"}`,
552
+ );
553
+ if (h.degradations.length > 0) {
554
+ lines.push(
555
+ ` ${chalk.dim("Degradations:")} ${chalk.yellow(h.degradations.join(", "))}`,
556
+ );
557
+ }
558
+ lines.push("");
559
+ }
560
+ }
561
+
562
+ return lines.join("\n");
563
+ }
564
+
565
+ // --- Task 10.5: Web Preview Mode ---
566
+
567
+ /**
568
+ * Generate a self-contained HTML page for the temper web preview.
569
+ * Uses inline CSS/JS, syntax highlighting, collapsible sections, and harness selector.
570
+ */
571
+ export function generateTemperHtml(output: TemperOutput): string {
572
+ const allHarnesses = [...SUPPORTED_HARNESSES];
573
+ const _sectionsJson = JSON.stringify(output.sections);
574
+ const _outputJson = JSON.stringify(output);
575
+
576
+ return `<!DOCTYPE html>
577
+ <html lang="en">
578
+ <head>
579
+ <meta charset="UTF-8">
580
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
581
+ <title>Temper Preview: ${escapeHtml(output.artifactName)} @ ${escapeHtml(output.harnessName)}</title>
582
+ <style>
583
+ * { box-sizing: border-box; margin: 0; padding: 0; }
584
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #1a1a2e; color: #e0e0e0; padding: 2rem; line-height: 1.6; }
585
+ .container { max-width: 960px; margin: 0 auto; }
586
+ header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 2rem; padding-bottom: 1rem; border-bottom: 1px solid #333; }
587
+ h1 { font-size: 1.5rem; color: #64ffda; }
588
+ .stats { display: flex; gap: 1.5rem; margin-bottom: 1.5rem; flex-wrap: wrap; }
589
+ .stat { background: #16213e; padding: 0.5rem 1rem; border-radius: 6px; font-size: 0.875rem; }
590
+ .stat-label { color: #888; }
591
+ .stat-value { color: #fff; font-weight: 600; }
592
+ .harness-selector { background: #16213e; border: 1px solid #333; color: #e0e0e0; padding: 0.5rem 1rem; border-radius: 6px; font-size: 0.875rem; cursor: pointer; }
593
+ .section { margin-bottom: 1.5rem; border: 1px solid #333; border-radius: 8px; overflow: hidden; }
594
+ .section-header { background: #16213e; padding: 0.75rem 1rem; cursor: pointer; display: flex; align-items: center; justify-content: space-between; user-select: none; }
595
+ .section-header:hover { background: #1a2744; }
596
+ .section-title { font-weight: 600; font-size: 0.95rem; }
597
+ .section-type { font-size: 0.75rem; padding: 0.2rem 0.5rem; border-radius: 4px; }
598
+ .type-system-prompt { background: #1e3a5f; color: #64b5f6; }
599
+ .type-steering { background: #1b5e20; color: #81c784; }
600
+ .type-hooks { background: #4a148c; color: #ce93d8; }
601
+ .type-mcp-servers { background: #006064; color: #4dd0e1; }
602
+ .type-degradation-report { background: #e65100; color: #ffb74d; }
603
+ .section-content { padding: 1rem; background: #0f0f23; display: none; }
604
+ .section-content.open { display: block; }
605
+ .section-content pre { white-space: pre-wrap; word-wrap: break-word; font-family: 'JetBrains Mono', 'Fira Code', monospace; font-size: 0.85rem; line-height: 1.5; }
606
+ .chevron { transition: transform 0.2s; }
607
+ .chevron.open { transform: rotate(90deg); }
608
+ </style>
609
+ </head>
610
+ <body>
611
+ <div class="container">
612
+ <header>
613
+ <h1>Temper: ${escapeHtml(output.artifactName)}</h1>
614
+ <select class="harness-selector" id="harnessSelect">
615
+ ${allHarnesses.map((h) => `<option value="${h}"${h === output.harnessName ? " selected" : ""}>${h}</option>`).join("\n ")}
616
+ </select>
617
+ </header>
618
+
619
+ <div class="stats">
620
+ <div class="stat"><span class="stat-label">Harness:</span> <span class="stat-value">${escapeHtml(output.harnessName)}</span></div>
621
+ <div class="stat"><span class="stat-label">Files:</span> <span class="stat-value">${output.fileCount}</span></div>
622
+ <div class="stat"><span class="stat-label">Hooks translated:</span> <span class="stat-value">${output.hooksTranslated}</span></div>
623
+ <div class="stat"><span class="stat-label">Hooks degraded:</span> <span class="stat-value">${output.hooksDegraded}</span></div>
624
+ <div class="stat"><span class="stat-label">MCP servers:</span> <span class="stat-value">${output.mcpServers.length}</span></div>
625
+ </div>
626
+
627
+ <div id="sections">
628
+ ${output.sections
629
+ .map(
630
+ (s, i) => `<div class="section">
631
+ <div class="section-header" onclick="toggleSection(${i})">
632
+ <span class="section-title"><span class="chevron" id="chevron-${i}">&#9654;</span> ${escapeHtml(s.title)}</span>
633
+ <span class="section-type type-${s.type}">${s.type}</span>
634
+ </div>
635
+ <div class="section-content" id="section-${i}">
636
+ <pre>${escapeHtml(s.content)}</pre>
637
+ </div>
638
+ </div>`,
639
+ )
640
+ .join("\n")}
641
+ </div>
642
+ </div>
643
+
644
+ <script>
645
+ function toggleSection(idx) {
646
+ const el = document.getElementById('section-' + idx);
647
+ const chevron = document.getElementById('chevron-' + idx);
648
+ el.classList.toggle('open');
649
+ chevron.classList.toggle('open');
650
+ }
651
+
652
+ // Expand first section by default
653
+ if (document.getElementById('section-0')) {
654
+ document.getElementById('section-0').classList.add('open');
655
+ document.getElementById('chevron-0').classList.add('open');
656
+ }
657
+
658
+ // Harness selector — reload with new harness
659
+ document.getElementById('harnessSelect').addEventListener('change', function() {
660
+ const harness = this.value;
661
+ window.location.href = '/?harness=' + encodeURIComponent(harness);
662
+ });
663
+ </script>
664
+ </body>
665
+ </html>`;
666
+ }
667
+
668
+ function escapeHtml(str: string): string {
669
+ return str
670
+ .replace(/&/g, "&amp;")
671
+ .replace(/</g, "&lt;")
672
+ .replace(/>/g, "&gt;")
673
+ .replace(/"/g, "&quot;")
674
+ .replace(/'/g, "&#039;");
675
+ }
676
+
677
+ /**
678
+ * Start a local HTTP server serving the temper HTML preview.
679
+ * Opens the browser and prints the URL to stderr.
680
+ */
681
+ export async function startTemperServer(
682
+ output: TemperOutput,
683
+ port = 4400,
684
+ ): Promise<void> {
685
+ const temperOptions: TemperOptions = {
686
+ artifactName: output.artifactName,
687
+ harness: output.harnessName as HarnessName,
688
+ };
689
+
690
+ const server = Bun.serve({
691
+ hostname: "localhost",
692
+ port,
693
+ async fetch(req) {
694
+ const url = new URL(req.url);
695
+ const harness = url.searchParams.get("harness") as HarnessName | null;
696
+
697
+ let currentOutput = output;
698
+ if (
699
+ harness &&
700
+ harness !== output.harnessName &&
701
+ SUPPORTED_HARNESSES.includes(harness as HarnessName)
702
+ ) {
703
+ currentOutput = await renderTemper({
704
+ ...temperOptions,
705
+ harness: harness as HarnessName,
706
+ });
707
+ }
708
+
709
+ const html = generateTemperHtml(currentOutput);
710
+ return new Response(html, {
711
+ headers: { "Content-Type": "text/html; charset=utf-8" },
712
+ });
713
+ },
714
+ });
715
+
716
+ const serverUrl = `http://localhost:${port}`;
717
+ console.error(
718
+ chalk.green(`Temper preview running at ${chalk.bold(serverUrl)}`),
719
+ );
720
+
721
+ // Attempt to open browser (best-effort)
722
+ try {
723
+ const platform = process.platform;
724
+ const cmd =
725
+ platform === "darwin"
726
+ ? "open"
727
+ : platform === "win32"
728
+ ? "start"
729
+ : "xdg-open";
730
+ Bun.spawn([cmd, serverUrl], { stdout: "ignore", stderr: "ignore" });
731
+ } catch {
732
+ // Silently ignore
733
+ }
734
+
735
+ // Keep server running until SIGINT
736
+ process.on("SIGINT", () => {
737
+ server.stop();
738
+ console.error(chalk.yellow("\nTemper server shut down."));
739
+ process.exit(0);
740
+ });
741
+ }
742
+
743
+ // --- Task 10.6: Error Handling ---
744
+ // Error handling is already implemented in renderTemper() above:
745
+ // - Returns error listing available artifacts when artifact not found
746
+ // - Returns error when harness not in artifact's harnesses list
747
+ // Both error messages include actionable suggestions.