@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/import.ts ADDED
@@ -0,0 +1,773 @@
1
+ /**
2
+ * Legacy Path-Import Facade
3
+ *
4
+ * Preserves the public interface of `kanon import`: ImportFormat, ImportOptions,
5
+ * ImportResult, and importCommand. Internally delegates source translation and
6
+ * canonical plan generation to Rosetta Stone while retaining all scanning,
7
+ * --all grouping, format/auto detection, collection injection, collision
8
+ * behavior, destination override, and dry-run logic in this imperative shell.
9
+ *
10
+ * Requirements: 14.1, 14.3, 14.4, 14.10, 14.11
11
+ */
12
+
13
+ import { exists, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
14
+ import { basename, extname, join } from "node:path";
15
+ import * as p from "@clack/prompts";
16
+ import chalk from "chalk";
17
+ import {
18
+ type AttributionPrompts,
19
+ type AttributionWizardMode,
20
+ deriveAttributionDraft,
21
+ runAttributionWizard,
22
+ } from "./attribution";
23
+ import {
24
+ type AcquisitionContext,
25
+ buildProvenanceRecord,
26
+ writeBaseArtifact,
27
+ } from "./base-cache";
28
+ import { isParseError, parseKnowledgeMd } from "./parser";
29
+ import { serializeCanonical } from "./rosetta/canonical";
30
+ import { getSharedEngine } from "./rosetta/engine-bootstrap";
31
+ import type {
32
+ FormatIdentifier,
33
+ KnowledgeArtifact,
34
+ NormalizedRelativePath,
35
+ SourceDocument,
36
+ TranslationRequest,
37
+ } from "./schemas";
38
+
39
+ // ═══════════════════════════════════════════════════════════════════════════════
40
+ // Public Types (preserved for backward compatibility)
41
+ // ═══════════════════════════════════════════════════════════════════════════════
42
+
43
+ export type ImportFormat = "kiro-power" | "kiro-skill" | "superpowers" | "auto";
44
+
45
+ export interface ImportOptions {
46
+ /** Import all subdirectories within the given path. */
47
+ all?: boolean;
48
+ /** Force a specific source format (default: auto-detect). */
49
+ format?: ImportFormat;
50
+ /** Show what would be created without writing anything. */
51
+ dryRun?: boolean;
52
+ /** Target knowledge directory (default: "knowledge"). */
53
+ knowledgeDir?: string;
54
+ /** Collection names to add to all imported artifacts. */
55
+ collections?: string[];
56
+ /**
57
+ * Acquisition-driven import context. When supplied (by the Sync_Orchestrator
58
+ * for an upstream-sourced import), a machine-managed ProvenanceRecord is
59
+ * populated on the imported artifact and its normalized base is cached for
60
+ * later three-way reconciliation. Absent for plain local-path imports, which
61
+ * carry no provenance and are excluded from reconciliation (Requirement 18.17).
62
+ */
63
+ acquisition?: ImportAcquisitionOptions;
64
+ /**
65
+ * How the import-time Attribution_Wizard behaves (ADR-0064, Requirement 3/4).
66
+ * `interactive` prompts to confirm the derived draft and pick a relationship;
67
+ * `defaults` writes the derived draft unchanged; `skip` writes no block.
68
+ * Absent → treated as `defaults` with a recorded warning (non-TTY path).
69
+ */
70
+ attributionMode?: AttributionWizardMode;
71
+ /** Prompt bindings for the wizard (injected in tests; real @clack in the CLI). */
72
+ attributionPrompts?: AttributionPrompts;
73
+ /** Curator identity recorded as `curated-by` in a derived draft. */
74
+ curatedBy?: string;
75
+ }
76
+
77
+ /**
78
+ * The subset of AcquisitionContext an import caller supplies plus the workspace
79
+ * root used to anchor the git-ignored base cache. `importedAt` and the resolved
80
+ * `sourceFormat`/`contract` are derived at import time and need not be provided.
81
+ */
82
+ export interface ImportAcquisitionOptions {
83
+ /** The upstream identifier — matches a key in config `upstreams`. */
84
+ upstream: string;
85
+ /** The source subpath within the upstream repository. */
86
+ sourcePath: string;
87
+ /** The upstream revision (subtree commit) the import was taken from. */
88
+ sourceRevision: string;
89
+ /** Workspace root anchoring `upstream/.kanon-base/` (default: process.cwd()). */
90
+ workspaceRoot?: string;
91
+ }
92
+
93
+ export interface ImportResult {
94
+ name: string;
95
+ sourcePath: string;
96
+ targetPath: string;
97
+ filesWritten: string[];
98
+ workflowsCopied: number;
99
+ skipped?: string;
100
+ /** True when a ProvenanceRecord was written for an acquisition import. */
101
+ provenanceWritten?: boolean;
102
+ /** The base-cache directory written, when an acquisition import cached its base. */
103
+ baseCachePath?: string;
104
+ }
105
+
106
+ // ═══════════════════════════════════════════════════════════════════════════════
107
+ // Format Detection
108
+ // ═══════════════════════════════════════════════════════════════════════════════
109
+
110
+ function detectFormat(_sourceDir: string, entries: string[]): ImportFormat {
111
+ if (entries.includes("POWER.md")) return "kiro-power";
112
+ if (entries.includes("SKILL.md")) return "kiro-skill";
113
+ return "auto";
114
+ }
115
+
116
+ // ═══════════════════════════════════════════════════════════════════════════════
117
+ // Document Building — Read source dir into SourceDocument[] for Rosetta Stone
118
+ // ═══════════════════════════════════════════════════════════════════════════════
119
+
120
+ /**
121
+ * Reads a source directory into an in-memory SourceDocument[] suitable for
122
+ * Rosetta Stone source translators. Determines which files to include based
123
+ * on the expected format structure.
124
+ */
125
+ async function buildSourceDocuments(
126
+ sourceDir: string,
127
+ format: "kiro-power" | "kiro-skill" | "superpowers",
128
+ ): Promise<SourceDocument[]> {
129
+ const documents: SourceDocument[] = [];
130
+ const entries = await readdir(sourceDir);
131
+
132
+ if (format === "kiro-power") {
133
+ // POWER.md (required) + steering/*.md (optional)
134
+ const powerMdPath = join(sourceDir, "POWER.md");
135
+ if (entries.includes("POWER.md")) {
136
+ const content = await readFile(powerMdPath, "utf-8");
137
+ documents.push({
138
+ path: "POWER.md" as NormalizedRelativePath,
139
+ content,
140
+ executable: false,
141
+ });
142
+ }
143
+ const steeringDir = join(sourceDir, "steering");
144
+ if (await exists(steeringDir)) {
145
+ const steeringFiles = (await readdir(steeringDir))
146
+ .filter((f) => extname(f) === ".md")
147
+ .sort();
148
+ for (const file of steeringFiles) {
149
+ const content = await readFile(join(steeringDir, file), "utf-8");
150
+ documents.push({
151
+ path: `steering/${file}` as NormalizedRelativePath,
152
+ content,
153
+ executable: false,
154
+ });
155
+ }
156
+ }
157
+ } else if (format === "kiro-skill") {
158
+ // SKILL.md (required) + references/*.md (optional)
159
+ const skillMdPath = join(sourceDir, "SKILL.md");
160
+ if (entries.includes("SKILL.md")) {
161
+ const content = await readFile(skillMdPath, "utf-8");
162
+ documents.push({
163
+ path: "SKILL.md" as NormalizedRelativePath,
164
+ content,
165
+ executable: false,
166
+ });
167
+ }
168
+ const refsDir = join(sourceDir, "references");
169
+ if (await exists(refsDir)) {
170
+ const refFiles = (await readdir(refsDir))
171
+ .filter((f) => extname(f) === ".md")
172
+ .sort();
173
+ for (const file of refFiles) {
174
+ const content = await readFile(join(refsDir, file), "utf-8");
175
+ documents.push({
176
+ path: `references/${file}` as NormalizedRelativePath,
177
+ content,
178
+ executable: false,
179
+ });
180
+ }
181
+ }
182
+ } else {
183
+ // superpowers: SKILL.md (required) + companion *.md files (optional)
184
+ const skillMdPath = join(sourceDir, "SKILL.md");
185
+ if (entries.includes("SKILL.md")) {
186
+ const content = await readFile(skillMdPath, "utf-8");
187
+ documents.push({
188
+ path: "SKILL.md" as NormalizedRelativePath,
189
+ content,
190
+ executable: false,
191
+ });
192
+ }
193
+ const additionalMd = entries
194
+ .filter((f) => extname(f) === ".md" && f !== "SKILL.md")
195
+ .sort();
196
+ for (const file of additionalMd) {
197
+ const content = await readFile(join(sourceDir, file), "utf-8");
198
+ documents.push({
199
+ path: file as NormalizedRelativePath,
200
+ content,
201
+ executable: false,
202
+ });
203
+ }
204
+ }
205
+
206
+ return documents;
207
+ }
208
+
209
+ // ═══════════════════════════════════════════════════════════════════════════════
210
+ // Rosetta Stone Delegation
211
+ // ═══════════════════════════════════════════════════════════════════════════════
212
+
213
+ /**
214
+ * Selects and invokes the appropriate Rosetta Stone source translator, then
215
+ * requests a canonical serializer plan. Injects collections from CLI options
216
+ * into the resulting artifact before serialization.
217
+ */
218
+ /**
219
+ * The Rosetta Stone Format_Contract identifier and version recorded in
220
+ * provenance for each source format, e.g. `kiro-power@1`. All built-in source
221
+ * contracts declare contractVersion "1.0" (see builtins/contracts.ts); the
222
+ * major component is recorded here.
223
+ */
224
+ const SOURCE_CONTRACT_IDENTIFIERS: Record<
225
+ "kiro-power" | "kiro-skill" | "superpowers",
226
+ string
227
+ > = {
228
+ "kiro-power": "kiro-power@1",
229
+ "kiro-skill": "kiro-skill@1",
230
+ superpowers: "superpowers@1",
231
+ };
232
+
233
+ async function translateViaRosetta(
234
+ documents: readonly SourceDocument[],
235
+ format: "kiro-power" | "kiro-skill" | "superpowers",
236
+ artifactNameHint: string,
237
+ collections: string[],
238
+ acquisition: ImportAcquisitionOptions | undefined,
239
+ importedAt: string,
240
+ attribution?: {
241
+ mode: AttributionWizardMode;
242
+ prompts?: AttributionPrompts;
243
+ curatedBy?: string;
244
+ /** True when the on-disk target already carries an attribution block. */
245
+ existingHasAttribution: boolean;
246
+ },
247
+ ): Promise<{
248
+ artifact: KnowledgeArtifact | undefined;
249
+ plan:
250
+ | {
251
+ outputFiles: Array<{
252
+ relativePath: string;
253
+ content: string | Uint8Array;
254
+ executable: boolean;
255
+ }>;
256
+ }
257
+ | undefined;
258
+ baseDigest: string | undefined;
259
+ diagnostics: Array<{ severity: string; message: string }>;
260
+ }> {
261
+ // Route source translation through the SHARED Rosetta Stone engine — the same
262
+ // pipeline `kanon rosetta translate` uses (ADR-0065). Building an inbound
263
+ // TranslationRequest and calling engine.translate() gives us the engine's
264
+ // request guard, registry-driven format resolution, and canonical-schema
265
+ // validation, instead of calling the source translators by hand. The engine
266
+ // is pure: for an inbound request it returns the validated `canonical`
267
+ // KnowledgeArtifact and NO plan, so this facade decorates that artifact
268
+ // (collections, provenance, attribution) and serializes it below.
269
+ const engine = getSharedEngine();
270
+ const request: TranslationRequest = {
271
+ mode: "inbound",
272
+ sourceDocuments: documents.map((d) => ({
273
+ path: d.path,
274
+ content: d.content,
275
+ executable: d.executable,
276
+ ...(d.mediaType ? { mediaType: d.mediaType } : {}),
277
+ })),
278
+ source: {
279
+ formatId: format,
280
+ options: {},
281
+ },
282
+ canonical: {
283
+ emitEmptyAuxiliaryFiles: false,
284
+ },
285
+ canonicalSchemaVersion: "1.0.0",
286
+ strict: false,
287
+ callerContext: { artifactNameHint },
288
+ };
289
+
290
+ const translationResult = engine.translate(request);
291
+
292
+ const mappedDiagnostics = translationResult.diagnostics.map((d) => ({
293
+ severity: d.severity,
294
+ message: d.message,
295
+ }));
296
+
297
+ if (!translationResult.canonical) {
298
+ return {
299
+ artifact: undefined,
300
+ plan: undefined,
301
+ baseDigest: undefined,
302
+ diagnostics: mappedDiagnostics,
303
+ };
304
+ }
305
+
306
+ // The engine already validated this against KnowledgeArtifactSchema; it is a
307
+ // freshly-parsed object this facade owns and may decorate before serializing.
308
+ const artifact = translationResult.canonical;
309
+
310
+ // Inject CLI-provided collections into the candidate
311
+ if (collections.length > 0) {
312
+ artifact.frontmatter.collections = collections;
313
+ }
314
+
315
+ // For an acquisition-driven import, populate a machine-managed
316
+ // ProvenanceRecord BEFORE serialization so the digest is computed over the
317
+ // distilled content and the record is written into knowledge.md
318
+ // (Requirements 18.1, 18.2). The digest of the artifact WITHOUT provenance
319
+ // is the Base_Digest — the fingerprint of the translated upstream — and it is
320
+ // what a later re-sync recomputes and compares against.
321
+ let baseDigest: string | undefined;
322
+ if (acquisition) {
323
+ const context: AcquisitionContext = {
324
+ upstream: acquisition.upstream,
325
+ sourcePath: acquisition.sourcePath,
326
+ sourceFormat: format as FormatIdentifier,
327
+ sourceRevision: acquisition.sourceRevision,
328
+ contract: SOURCE_CONTRACT_IDENTIFIERS[format],
329
+ importedAt,
330
+ };
331
+ const { provenance, diagnostics: provDiagnostics } = buildProvenanceRecord(
332
+ artifact,
333
+ context,
334
+ );
335
+ for (const d of provDiagnostics) {
336
+ mappedDiagnostics.push({ severity: d.severity, message: d.message });
337
+ }
338
+ if (provenance) {
339
+ artifact.frontmatter.provenance = provenance;
340
+ baseDigest = provenance.baseDigest;
341
+ }
342
+ }
343
+
344
+ // Resolve curation-owned attribution BEFORE serialization so the block is
345
+ // written into knowledge.md (Requirement 3). First-import-only: if the target
346
+ // already carries an attribution block, leave it to reconciliation and skip
347
+ // the wizard entirely (Requirement 3, 5).
348
+ if (attribution && !attribution.existingHasAttribution) {
349
+ const draft = deriveAttributionDraft({
350
+ upstreamFrontmatter: artifact.frontmatter as Record<string, unknown>,
351
+ sourceRepo: acquisition?.upstream,
352
+ sourceCommit: acquisition?.sourceRevision,
353
+ sourcePath: acquisition?.sourcePath,
354
+ curatedBy: attribution.curatedBy,
355
+ });
356
+ const resolved = await runAttributionWizard(draft, {
357
+ mode: attribution.mode,
358
+ prompts: attribution.prompts,
359
+ });
360
+ if (resolved) {
361
+ // Only write a block that actually credits something — skip a no-signal
362
+ // draft (empty authors and no source-repo), which would otherwise add an
363
+ // empty attribution block to an author-less local import.
364
+ const first = resolved.upstream[0];
365
+ const hasSignal =
366
+ resolved.upstream.length > 1 ||
367
+ (first?.authors?.length ?? 0) > 0 ||
368
+ Boolean(first?.["source-repo"]);
369
+ if (hasSignal) {
370
+ artifact.frontmatter.attribution = resolved;
371
+ }
372
+ }
373
+ }
374
+
375
+ // Request a canonical serializer plan from Rosetta Stone
376
+ const serializerOutput = serializeCanonical(artifact, {
377
+ emitEmptyAuxiliaryFiles: true,
378
+ emitBodyOverrides: true,
379
+ emitWorkflows: true,
380
+ });
381
+
382
+ return {
383
+ artifact,
384
+ plan: serializerOutput.plan
385
+ ? {
386
+ outputFiles: serializerOutput.plan.outputFiles.map((f) => ({
387
+ relativePath: f.relativePath,
388
+ content: f.content,
389
+ executable: f.executable,
390
+ })),
391
+ }
392
+ : undefined,
393
+ baseDigest,
394
+ diagnostics: mappedDiagnostics,
395
+ };
396
+ }
397
+
398
+ // ═══════════════════════════════════════════════════════════════════════════════
399
+ // Single Directory Import (preserves collision and skip behavior)
400
+ // ═══════════════════════════════════════════════════════════════════════════════
401
+
402
+ /**
403
+ * Import a single source directory into the canonical knowledge tree.
404
+ *
405
+ * Preserves the legacy collision/skip/dry-run behavior. When `opts.acquisition`
406
+ * is supplied (an upstream-sourced, acquisition-driven import), a machine-managed
407
+ * ProvenanceRecord is written into the artifact's frontmatter and its normalized
408
+ * Base_Artifact is cached under the git-ignored `upstream/.kanon-base/` tree for
409
+ * later three-way reconciliation (Requirements 18.1, 18.2). Exported so the
410
+ * Sync_Orchestrator can drive a provenance-aware import per acquired artifact.
411
+ */
412
+ export async function importOne(
413
+ sourceDir: string,
414
+ opts: ImportOptions & { dryRun: boolean; knowledgeDir: string },
415
+ ): Promise<ImportResult> {
416
+ const entries = await readdir(sourceDir);
417
+
418
+ // Format detection (same logic as before)
419
+ const detectedFormat =
420
+ opts.format === "auto" || !opts.format
421
+ ? detectFormat(sourceDir, entries)
422
+ : opts.format;
423
+
424
+ // Validate the expected primary file exists
425
+ if (detectedFormat === "kiro-power") {
426
+ if (!entries.includes("POWER.md")) {
427
+ return {
428
+ name: basename(sourceDir),
429
+ sourcePath: sourceDir,
430
+ targetPath: "",
431
+ filesWritten: [],
432
+ workflowsCopied: 0,
433
+ skipped: `No POWER.md found in ${sourceDir}`,
434
+ };
435
+ }
436
+ } else if (detectedFormat === "kiro-skill") {
437
+ if (!entries.includes("SKILL.md")) {
438
+ return {
439
+ name: basename(sourceDir),
440
+ sourcePath: sourceDir,
441
+ targetPath: "",
442
+ filesWritten: [],
443
+ workflowsCopied: 0,
444
+ skipped: `No SKILL.md found in ${sourceDir}`,
445
+ };
446
+ }
447
+ } else if (detectedFormat === "superpowers") {
448
+ if (!entries.includes("SKILL.md")) {
449
+ return {
450
+ name: basename(sourceDir),
451
+ sourcePath: sourceDir,
452
+ targetPath: "",
453
+ filesWritten: [],
454
+ workflowsCopied: 0,
455
+ skipped: `No SKILL.md found in ${sourceDir}`,
456
+ };
457
+ }
458
+ } else {
459
+ // auto detection returned "auto" — could not detect format
460
+ return {
461
+ name: basename(sourceDir),
462
+ sourcePath: sourceDir,
463
+ targetPath: "",
464
+ filesWritten: [],
465
+ workflowsCopied: 0,
466
+ skipped: `Could not detect format in ${sourceDir} (no POWER.md or SKILL.md found)`,
467
+ };
468
+ }
469
+
470
+ // Build in-memory SourceDocuments from the filesystem
471
+ const documents = await buildSourceDocuments(sourceDir, detectedFormat);
472
+
473
+ // Delegate to Rosetta Stone for translation and canonical plan generation
474
+ const artifactNameHint = basename(sourceDir);
475
+ const collections = opts.collections ?? [];
476
+ const importedAt = new Date().toISOString();
477
+
478
+ // First-import-only guard for attribution: if the predicted target already
479
+ // carries a curation-owned attribution block, the wizard must not run — the
480
+ // block is preserved across re-sync (Requirement 3, 5). The importer derives
481
+ // the artifact name from the source dir, so the predicted target matches.
482
+ const attributionMode = opts.attributionMode ?? "skip";
483
+ let existingHasAttribution = false;
484
+ if (attributionMode !== "skip") {
485
+ const predictedTarget = join(
486
+ opts.knowledgeDir,
487
+ artifactNameHint,
488
+ "knowledge.md",
489
+ );
490
+ if (await exists(predictedTarget)) {
491
+ const existing = await parseKnowledgeMd(predictedTarget);
492
+ if (
493
+ !isParseError(existing) &&
494
+ existing.data.frontmatter.attribution?.upstream?.length
495
+ ) {
496
+ existingHasAttribution = true;
497
+ }
498
+ }
499
+ }
500
+
501
+ const { artifact, plan, baseDigest } = await translateViaRosetta(
502
+ documents,
503
+ detectedFormat,
504
+ artifactNameHint,
505
+ collections,
506
+ opts.acquisition,
507
+ importedAt,
508
+ {
509
+ mode: attributionMode,
510
+ prompts: opts.attributionPrompts,
511
+ curatedBy: opts.curatedBy,
512
+ existingHasAttribution,
513
+ },
514
+ );
515
+
516
+ if (!artifact || !plan) {
517
+ return {
518
+ name: basename(sourceDir),
519
+ sourcePath: sourceDir,
520
+ targetPath: "",
521
+ filesWritten: [],
522
+ workflowsCopied: 0,
523
+ skipped: `Translation failed for ${sourceDir}`,
524
+ };
525
+ }
526
+
527
+ const name = artifact.name;
528
+ const targetPath = join(opts.knowledgeDir, name);
529
+
530
+ // Collision check (existing behavior: error/skip)
531
+ if (await exists(targetPath)) {
532
+ return {
533
+ name,
534
+ sourcePath: sourceDir,
535
+ targetPath,
536
+ filesWritten: [],
537
+ workflowsCopied: 0,
538
+ skipped: `${targetPath} already exists — use --force to overwrite`,
539
+ };
540
+ }
541
+
542
+ // Apply the canonical plan — write output files
543
+ const filesWritten: string[] = [];
544
+ let workflowsCopied = 0;
545
+
546
+ if (!opts.dryRun) {
547
+ // Ensure the target directory and workflows subdirectory exist
548
+ await mkdir(join(targetPath, "workflows"), { recursive: true });
549
+ }
550
+
551
+ for (const file of plan.outputFiles) {
552
+ const destPath = join(targetPath, file.relativePath);
553
+ const content =
554
+ typeof file.content === "string"
555
+ ? file.content
556
+ : new TextDecoder().decode(file.content);
557
+
558
+ if (!opts.dryRun) {
559
+ // Ensure parent directory exists for nested paths (e.g., workflows/)
560
+ const dir = join(
561
+ targetPath,
562
+ file.relativePath.includes("/")
563
+ ? file.relativePath.slice(0, file.relativePath.lastIndexOf("/"))
564
+ : "",
565
+ );
566
+ if (dir !== targetPath) {
567
+ await mkdir(dir, { recursive: true });
568
+ }
569
+ await writeFile(destPath, content, "utf-8");
570
+ }
571
+
572
+ filesWritten.push(destPath);
573
+
574
+ if (file.relativePath.startsWith("workflows/")) {
575
+ workflowsCopied++;
576
+ }
577
+ }
578
+
579
+ // For an acquisition-driven import, cache the normalized Base_Artifact so a
580
+ // later re-sync can reconstruct the common ancestor for three-way
581
+ // reconciliation (Requirement 18.2). The cached base is the artifact WITHOUT
582
+ // its ProvenanceRecord — the exact content the baseDigest fingerprints — so
583
+ // that self-verification (verifyProvenanceBase) recomputes an identical
584
+ // digest on re-sync. Skipped in dry-run.
585
+ const provenanceWritten = Boolean(opts.acquisition && baseDigest);
586
+ let baseCachePath: string | undefined;
587
+ if (!opts.dryRun && opts.acquisition && baseDigest) {
588
+ const baseArtifact = stripProvenance(artifact);
589
+ const workspaceRoot = opts.acquisition.workspaceRoot ?? process.cwd();
590
+ const cacheResult = await writeBaseArtifact(
591
+ baseArtifact,
592
+ baseDigest,
593
+ { upstream: opts.acquisition.upstream },
594
+ workspaceRoot,
595
+ );
596
+ baseCachePath = cacheResult.cachePath;
597
+ }
598
+
599
+ return {
600
+ name,
601
+ sourcePath: sourceDir,
602
+ targetPath,
603
+ filesWritten,
604
+ workflowsCopied,
605
+ provenanceWritten,
606
+ baseCachePath,
607
+ };
608
+ }
609
+
610
+ /**
611
+ * Return a shallow clone of an artifact with any ProvenanceRecord removed from
612
+ * its frontmatter. The Base_Artifact cached for reconciliation must exclude
613
+ * provenance so its recomputed digest equals the recorded baseDigest (which is
614
+ * computed over the provenance-free artifact).
615
+ */
616
+ function stripProvenance(artifact: KnowledgeArtifact): KnowledgeArtifact {
617
+ const { provenance: _provenance, ...frontmatterWithoutProvenance } =
618
+ artifact.frontmatter;
619
+ return {
620
+ ...artifact,
621
+ frontmatter: frontmatterWithoutProvenance,
622
+ };
623
+ }
624
+
625
+ // ═══════════════════════════════════════════════════════════════════════════════
626
+ // CLI Command (public interface preserved)
627
+ // ═══════════════════════════════════════════════════════════════════════════════
628
+
629
+ /**
630
+ * `@clack/prompts`-backed bindings for the interactive attribution wizard.
631
+ * Reuses the same cancel semantics as the `kanon new` wizard: a cancelled
632
+ * prompt aborts the import cleanly without writing a partial artifact.
633
+ */
634
+ const clackPrompts: AttributionPrompts = {
635
+ text: (opts) =>
636
+ p.text({
637
+ message: opts.message,
638
+ initialValue: opts.initialValue,
639
+ placeholder: opts.placeholder,
640
+ }),
641
+ select: (opts) =>
642
+ p.select({
643
+ message: opts.message,
644
+ // The AttributionPrompts.select option shape ({value,label,hint}) matches
645
+ // clack's Option at runtime; cast at this adapter boundary to bridge the
646
+ // generic-parameter variance clack's stricter Option<T> type imposes.
647
+ options: opts.options as Parameters<typeof p.select>[0]["options"],
648
+ initialValue: opts.initialValue,
649
+ }),
650
+ handleCancel: (value) => {
651
+ if (p.isCancel(value)) {
652
+ p.cancel("Import cancelled. No files were written.");
653
+ process.exit(0);
654
+ }
655
+ },
656
+ };
657
+
658
+ export async function importCommand(
659
+ sourcePath: string,
660
+ options: Record<string, unknown> = {},
661
+ ): Promise<void> {
662
+ const dryRun = Boolean(options.dryRun);
663
+ const all = Boolean(options.all);
664
+ const knowledgeDir = String(options.knowledgeDir ?? "knowledge");
665
+ const collections = options.collections
666
+ ? String(options.collections)
667
+ .split(",")
668
+ .map((c) => c.trim())
669
+ : [];
670
+ const format = (options.format as ImportFormat | undefined) ?? "auto";
671
+
672
+ // Attribution wizard mode (Requirement 3, 4). `--no-attribution` writes no
673
+ // block; `--attribution-defaults` accepts the derived draft with no prompt;
674
+ // an interactive TTY prompts. A plain non-interactive local import defaults
675
+ // to `skip` — attribution is an upstream-credit concern, so a local import of
676
+ // your own content is not forced to carry a block (this also keeps legacy
677
+ // import byte-output stable). Upstream/acquisition imports and the backfill
678
+ // are the paths that populate attribution.
679
+ let attributionMode: AttributionWizardMode;
680
+ if (options.attribution === false) {
681
+ // commander sets `attribution: false` for `--no-attribution`
682
+ attributionMode = "skip";
683
+ } else if (options.attributionDefaults) {
684
+ attributionMode = "defaults";
685
+ } else if (process.stdout.isTTY) {
686
+ attributionMode = "interactive";
687
+ } else {
688
+ attributionMode = "skip";
689
+ }
690
+
691
+ const resolved = sourcePath.replace(/^~/, process.env.HOME ?? "~");
692
+
693
+ if (dryRun) {
694
+ console.error(chalk.dim(" Dry run — no files will be written\n"));
695
+ }
696
+
697
+ const opts = {
698
+ dryRun,
699
+ knowledgeDir,
700
+ collections,
701
+ format,
702
+ attributionMode,
703
+ attributionPrompts:
704
+ attributionMode === "interactive" ? clackPrompts : undefined,
705
+ };
706
+
707
+ let sources: string[];
708
+
709
+ if (all) {
710
+ // Scan sourcePath for subdirectories
711
+ if (!(await exists(resolved))) {
712
+ console.error(chalk.red(`Error: Path not found: ${resolved}`));
713
+ process.exit(1);
714
+ }
715
+ const entries = await readdir(resolved, { withFileTypes: true });
716
+ sources = entries
717
+ .filter((e) => e.isDirectory() && !e.name.startsWith("."))
718
+ .map((e) => join(resolved, e.name))
719
+ .sort();
720
+ } else {
721
+ sources = [resolved];
722
+ }
723
+
724
+ if (sources.length === 0) {
725
+ console.error(chalk.yellow("No source directories found."));
726
+ return;
727
+ }
728
+
729
+ const results: ImportResult[] = [];
730
+ for (const src of sources) {
731
+ const result = await importOne(src, opts);
732
+ results.push(result);
733
+ }
734
+
735
+ // Print results
736
+ console.error("");
737
+ let imported = 0;
738
+ let skipped = 0;
739
+
740
+ for (const r of results) {
741
+ if (r.skipped) {
742
+ console.error(chalk.yellow(` ⚠ ${r.name} — ${r.skipped}`));
743
+ skipped++;
744
+ } else {
745
+ const wf =
746
+ r.workflowsCopied > 0
747
+ ? chalk.dim(
748
+ ` + ${r.workflowsCopied} workflow${r.workflowsCopied !== 1 ? "s" : ""}`,
749
+ )
750
+ : "";
751
+ const prefix = dryRun ? chalk.dim(" → ") : chalk.green(" ✓ ");
752
+ console.error(
753
+ `${prefix}${chalk.bold(r.name)}${wf} ${chalk.dim(r.targetPath)}`,
754
+ );
755
+ imported++;
756
+ }
757
+ }
758
+
759
+ console.error("");
760
+ const verb = dryRun ? "would import" : "imported";
761
+ console.error(
762
+ chalk.green(` ${imported} artifact${imported !== 1 ? "s" : ""} ${verb}`) +
763
+ (skipped > 0 ? chalk.yellow(`, ${skipped} skipped`) : ""),
764
+ );
765
+
766
+ if (!dryRun && imported > 0) {
767
+ console.error(
768
+ chalk.dim(" Run `forge validate` to check the imported artifacts."),
769
+ );
770
+ console.error(chalk.dim(" Run `forge build` to compile them."));
771
+ }
772
+ console.error("");
773
+ }