@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/wizard.ts ADDED
@@ -0,0 +1,660 @@
1
+ import * as p from "@clack/prompts";
2
+ import type { z } from "zod";
3
+ import { HARNESS_FORMAT_REGISTRY } from "./format-registry";
4
+ import type {
5
+ AssetType,
6
+ CanonicalHook,
7
+ Frontmatter,
8
+ McpServerDefinition,
9
+ } from "./schemas";
10
+ import {
11
+ CATEGORIES,
12
+ CanonicalEventSchema,
13
+ CanonicalHookSchema,
14
+ FrontmatterSchema,
15
+ McpServerDefinitionSchema,
16
+ SUPPORTED_HARNESSES,
17
+ } from "./schemas";
18
+
19
+ /** Collected wizard results — everything needed to write files. */
20
+ export interface WizardResult {
21
+ frontmatter: Frontmatter;
22
+ knowledgeBody: string;
23
+ hooks: CanonicalHook[];
24
+ mcpServers: McpServerDefinition[];
25
+ }
26
+
27
+ /**
28
+ * Split a comma-separated string into a trimmed, non-empty array.
29
+ */
30
+ export function parseCommaSeparated(input: string): string[] {
31
+ return input
32
+ .split(",")
33
+ .map((s) => s.trim())
34
+ .filter((s) => s.length > 0);
35
+ }
36
+
37
+ /**
38
+ * Parse `KEY=VALUE,KEY=VALUE` format into a record.
39
+ * Entries without an `=` sign are silently skipped.
40
+ */
41
+ export function parseKeyValuePairs(input: string): Record<string, string> {
42
+ const result: Record<string, string> = {};
43
+ const entries = parseCommaSeparated(input);
44
+ for (const entry of entries) {
45
+ const eqIndex = entry.indexOf("=");
46
+ if (eqIndex === -1) continue;
47
+ const key = entry.slice(0, eqIndex).trim();
48
+ const value = entry.slice(eqIndex + 1).trim();
49
+ if (key.length > 0) {
50
+ result[key] = value;
51
+ }
52
+ }
53
+ return result;
54
+ }
55
+
56
+ /**
57
+ * Run a Zod schema's `.safeParse()` on a value.
58
+ * Returns `undefined` on success, or a user-friendly error message string on failure.
59
+ * Designed to be used directly as a `@clack/prompts` validate callback return value.
60
+ */
61
+ export function validateField<T>(
62
+ schema: z.ZodType<T>,
63
+ value: unknown,
64
+ ): string | undefined {
65
+ const result = schema.safeParse(value);
66
+ if (result.success) {
67
+ return undefined;
68
+ }
69
+ const messages = result.error.issues.map((issue) => issue.message);
70
+ return messages.join("; ");
71
+ }
72
+
73
+ /**
74
+ * Check if the user cancelled a prompt. If so, display a message and exit.
75
+ */
76
+ export function handleCancel(value: unknown): void {
77
+ if (p.isCancel(value)) {
78
+ p.cancel("Wizard cancelled. Your scaffold files remain intact.");
79
+ process.exit(0);
80
+ }
81
+ }
82
+
83
+ const ASSET_TYPE_DESCRIPTIONS: Record<AssetType, string> = {
84
+ skill: "General-purpose knowledge injected into AI context",
85
+ // Deprecated alias for "skill" — omitted from the interactive type prompt
86
+ // (see PROMPTABLE_ASSET_TYPES below) but described here since the Record
87
+ // type must cover every AssetType. See ADR-0051.
88
+ power:
89
+ 'Deprecated alias for "skill" — use skill + harness-config.kiro.format: "power" instead',
90
+ rule: "Lint-style rules for harnesses that support them",
91
+ workflow: "Step-by-step process guide with workflow files",
92
+ agent: "Agent definition with hooks and MCP tools",
93
+ prompt: "Reusable prompt template",
94
+ template: "Reference scaffold or boilerplate",
95
+ "reference-pack": "Background reference (included on demand only)",
96
+ };
97
+
98
+ // Types offered in the interactive wizard prompt. Excludes "power", a
99
+ // deprecated alias for "skill" — Kiro's power format is chosen via the
100
+ // per-harness format prompt, not the type field. Still valid if passed
101
+ // explicitly via --type for backward compat (see promptFrontmatter below).
102
+ const PROMPTABLE_ASSET_TYPES: AssetType[] = (
103
+ Object.keys(ASSET_TYPE_DESCRIPTIONS) as AssetType[]
104
+ ).filter((t) => t !== "power");
105
+
106
+ /**
107
+ * Collect all frontmatter fields via interactive prompts.
108
+ * Returns a validated Frontmatter object.
109
+ *
110
+ * @param preSelectedType - If provided, skip the type prompt and use this value.
111
+ */
112
+ export async function promptFrontmatter(
113
+ name: string,
114
+ displayName: string,
115
+ preSelectedType?: AssetType,
116
+ ): Promise<Frontmatter> {
117
+ const description = await p.text({
118
+ message: "Describe your artifact in a sentence or two",
119
+ validate: (val) => {
120
+ return validateField(FrontmatterSchema.shape.description, val);
121
+ },
122
+ });
123
+ handleCancel(description);
124
+
125
+ const keywordsRaw = await p.text({
126
+ message: "Keywords (comma-separated, e.g. react, testing, hooks)",
127
+ validate: (val) => {
128
+ const parsed = parseCommaSeparated(val ?? "");
129
+ return validateField(FrontmatterSchema.shape.keywords, parsed);
130
+ },
131
+ });
132
+ handleCancel(keywordsRaw);
133
+
134
+ const author = await p.text({
135
+ message: "Author name",
136
+ validate: (val) => {
137
+ return validateField(FrontmatterSchema.shape.author, val);
138
+ },
139
+ });
140
+ handleCancel(author);
141
+
142
+ // Type selection — skip if pre-selected via --type flag
143
+ let selectedType: AssetType;
144
+ if (preSelectedType) {
145
+ selectedType = preSelectedType;
146
+ } else {
147
+ const typeOptions = PROMPTABLE_ASSET_TYPES.map((t) => ({
148
+ value: t,
149
+ label: t,
150
+ hint: ASSET_TYPE_DESCRIPTIONS[t],
151
+ }));
152
+ const typeRaw = await p.select({
153
+ message: "What kind of artifact is this?",
154
+ options: typeOptions,
155
+ initialValue: "skill" as AssetType,
156
+ });
157
+ handleCancel(typeRaw);
158
+ selectedType = typeRaw as AssetType;
159
+ }
160
+
161
+ const inclusion = await p.select({
162
+ message: "When should this artifact be included?",
163
+ options: [
164
+ {
165
+ value: "always" as const,
166
+ label: "always",
167
+ hint: "Included in every AI session automatically",
168
+ },
169
+ {
170
+ value: "fileMatch" as const,
171
+ label: "fileMatch",
172
+ hint: "Included only when matching files are open",
173
+ },
174
+ {
175
+ value: "manual" as const,
176
+ label: "manual",
177
+ hint: "Included only when explicitly referenced",
178
+ },
179
+ ],
180
+ });
181
+ handleCancel(inclusion);
182
+
183
+ let filePatterns: string[] | undefined;
184
+ if (inclusion === "fileMatch") {
185
+ const filePatternsRaw = await p.text({
186
+ message: "File patterns to match (comma-separated globs)",
187
+ validate: (val) => {
188
+ const parsed = parseCommaSeparated(val ?? "");
189
+ if (parsed.length === 0) return "At least one file pattern is required";
190
+ return validateField(FrontmatterSchema.shape.file_patterns, parsed);
191
+ },
192
+ });
193
+ handleCancel(filePatternsRaw);
194
+ filePatterns = parseCommaSeparated(filePatternsRaw as string);
195
+ }
196
+
197
+ const categories = await p.multiselect({
198
+ message: "Pick the categories that apply",
199
+ options: CATEGORIES.map((cat) => ({
200
+ value: cat,
201
+ label: cat,
202
+ })),
203
+ required: false,
204
+ });
205
+ handleCancel(categories);
206
+
207
+ const HARNESS_DESCRIPTIONS: Record<string, string> = {
208
+ kiro: "Steering files or powers for Kiro IDE",
209
+ cursor: "Rule files for Cursor",
210
+ copilot: "Instructions or agents for GitHub Copilot",
211
+ "claude-code": "CLAUDE.md for Claude Code",
212
+ codex: "AGENTS.md and skills for OpenAI Codex",
213
+ windsurf: "Rule files for Windsurf",
214
+ cline: "Rule files for Cline",
215
+ qdeveloper: "Rules or agents for Amazon Q Developer",
216
+ };
217
+
218
+ const harnesses = await p.multiselect({
219
+ message: "Which AI coding tools should this target?",
220
+ options: SUPPORTED_HARNESSES.map((h) => ({
221
+ value: h,
222
+ label: `${h} — ${HARNESS_DESCRIPTIONS[h]}`,
223
+ })),
224
+ initialValues: [...SUPPORTED_HARNESSES],
225
+ required: false,
226
+ });
227
+ handleCancel(harnesses);
228
+
229
+ // Per-harness format prompts for multi-format harnesses
230
+ const FORMAT_DESCRIPTIONS: Record<string, Record<string, string>> = {
231
+ kiro: {
232
+ steering: "A markdown knowledge file included in AI context",
233
+ power: "A capability bundle with POWER.md and steering directory",
234
+ },
235
+ copilot: {
236
+ instructions: "A copilot-instructions.md file for GitHub Copilot",
237
+ agent: "An AGENTS.md file for GitHub Copilot agents",
238
+ },
239
+ codex: {
240
+ "agents-md": "An AGENTS.md repo guide loaded into every Codex session",
241
+ skill: "A discoverable skill under .codex/skills/<name>/SKILL.md",
242
+ },
243
+ qdeveloper: {
244
+ rule: "A rule file in .q/rules/",
245
+ agent: "An agent file in .q/agents/",
246
+ },
247
+ };
248
+
249
+ const harnessConfig: Record<string, Record<string, unknown>> = {};
250
+ const selectedHarnesses = harnesses as string[];
251
+
252
+ for (const harness of selectedHarnesses) {
253
+ const registryEntry =
254
+ HARNESS_FORMAT_REGISTRY[harness as keyof typeof HARNESS_FORMAT_REGISTRY];
255
+ if (registryEntry.formats.length > 1) {
256
+ const formatDescriptions = FORMAT_DESCRIPTIONS[harness];
257
+ const format = await p.select({
258
+ message: `Output format for ${harness}`,
259
+ options: registryEntry.formats.map((f) => ({
260
+ value: f,
261
+ label: f,
262
+ hint: formatDescriptions?.[f] ?? "",
263
+ })),
264
+ });
265
+ handleCancel(format);
266
+
267
+ // Only write non-default format selections to keep frontmatter minimal
268
+ if (format !== registryEntry.default) {
269
+ harnessConfig[harness] = { format: format as string };
270
+ }
271
+ }
272
+ }
273
+
274
+ // Kiro-specific inclusion mode prompt (Req 9.1–9.6)
275
+ if (selectedHarnesses.includes("kiro")) {
276
+ let kiroHarnessConfig: Record<string, unknown> = harnessConfig.kiro ?? {};
277
+
278
+ const initialKiroInclusion: "always" | "fileMatch" | "manual" =
279
+ selectedType === "power" || selectedType === "reference-pack"
280
+ ? "manual"
281
+ : "always";
282
+
283
+ const kiroInclusion = await p.select({
284
+ message:
285
+ "Kiro inclusion mode — when should this steering file be loaded?",
286
+ initialValue: initialKiroInclusion,
287
+ options: [
288
+ {
289
+ value: "always" as const,
290
+ label: "always",
291
+ hint: "Loaded into every agent interaction",
292
+ },
293
+ {
294
+ value: "fileMatch" as const,
295
+ label: "fileMatch",
296
+ hint: "Loaded when a matching file is in context",
297
+ },
298
+ {
299
+ value: "manual" as const,
300
+ label: "manual",
301
+ hint:
302
+ selectedType === "power"
303
+ ? "Recommended for powers — progressively disclosed via POWER.md"
304
+ : selectedType === "reference-pack"
305
+ ? "Recommended — follows the reference-pack-must-be-manual convention"
306
+ : "Loaded only when the user references the file with #",
307
+ },
308
+ ],
309
+ });
310
+ handleCancel(kiroInclusion);
311
+ kiroHarnessConfig = { ...kiroHarnessConfig, inclusion: kiroInclusion };
312
+
313
+ if (kiroInclusion === "fileMatch") {
314
+ const pattern = await p.text({
315
+ message: "fileMatchPattern (glob, e.g. src/**/*.ts)",
316
+ validate: (val) =>
317
+ !val || val.trim().length === 0
318
+ ? "fileMatchPattern is required for fileMatch"
319
+ : undefined,
320
+ });
321
+ handleCancel(pattern);
322
+ kiroHarnessConfig = {
323
+ ...kiroHarnessConfig,
324
+ fileMatchPattern: (pattern as string).trim(),
325
+ };
326
+ }
327
+
328
+ harnessConfig.kiro = kiroHarnessConfig;
329
+ }
330
+
331
+ const ecosystemRaw = await p.text({
332
+ message: "Ecosystem tags (comma-separated, e.g. typescript, bun, react)",
333
+ validate: (val) => {
334
+ if (!val) return undefined;
335
+ const parsed = parseCommaSeparated(val);
336
+ return validateField(FrontmatterSchema.shape.ecosystem, parsed);
337
+ },
338
+ });
339
+ handleCancel(ecosystemRaw);
340
+
341
+ const frontmatter: Frontmatter = {
342
+ name,
343
+ displayName,
344
+ version: "0.1.0",
345
+ description: description as string,
346
+ keywords: parseCommaSeparated(keywordsRaw as string),
347
+ author: author as string,
348
+ type: selectedType,
349
+ inclusion: inclusion as Frontmatter["inclusion"],
350
+ ...(filePatterns ? { file_patterns: filePatterns } : {}),
351
+ categories: categories as Frontmatter["categories"],
352
+ harnesses: harnesses as Frontmatter["harnesses"],
353
+ ecosystem: ecosystemRaw ? parseCommaSeparated(ecosystemRaw as string) : [],
354
+ depends: [],
355
+ enhances: [],
356
+ maturity: "experimental",
357
+ "model-assumptions": [],
358
+ collections: [],
359
+ "inherit-hooks": false,
360
+ outcomes: [],
361
+ ...(Object.keys(harnessConfig).length > 0
362
+ ? { "harness-config": harnessConfig }
363
+ : {}),
364
+ };
365
+
366
+ return frontmatter;
367
+ }
368
+
369
+ /**
370
+ * Collect the markdown body content for the knowledge file.
371
+ * Returns the user-provided content, or an empty string if left blank.
372
+ */
373
+ export async function promptKnowledgeBody(): Promise<string> {
374
+ const body = await p.text({
375
+ message: "Write your knowledge content (or leave blank to fill in later)",
376
+ defaultValue: "",
377
+ });
378
+ handleCancel(body);
379
+
380
+ const trimmed = (body as string).trim();
381
+ return trimmed;
382
+ }
383
+
384
+ const FILE_EVENTS = new Set(["file_edited", "file_created", "file_deleted"]);
385
+ const TOOL_EVENTS = new Set(["pre_tool_use", "post_tool_use"]);
386
+
387
+ /**
388
+ * Collect one hook definition via interactive prompts.
389
+ * Returns the validated hook, or null if the user chooses to skip after a validation failure.
390
+ */
391
+ async function promptSingleHook(): Promise<CanonicalHook | null> {
392
+ const event = await p.select({
393
+ message: "Select the event type for this hook",
394
+ options: CanonicalEventSchema.options.map((evt) => ({
395
+ value: evt,
396
+ label: evt,
397
+ })),
398
+ });
399
+ handleCancel(event);
400
+
401
+ let filePatterns: string[] | undefined;
402
+ let toolTypes: string[] | undefined;
403
+
404
+ if (FILE_EVENTS.has(event as string)) {
405
+ const raw = await p.text({
406
+ message: "File patterns to match (comma-separated globs)",
407
+ validate: (val) => {
408
+ if (!val || val.trim().length === 0)
409
+ return "At least one file pattern is required";
410
+ return undefined;
411
+ },
412
+ });
413
+ handleCancel(raw);
414
+ filePatterns = parseCommaSeparated(raw as string);
415
+ }
416
+
417
+ if (TOOL_EVENTS.has(event as string)) {
418
+ const raw = await p.text({
419
+ message: "Tool types to match (comma-separated)",
420
+ validate: (val) => {
421
+ if (!val || val.trim().length === 0)
422
+ return "At least one tool type is required";
423
+ return undefined;
424
+ },
425
+ });
426
+ handleCancel(raw);
427
+ toolTypes = parseCommaSeparated(raw as string);
428
+ }
429
+
430
+ const actionType = await p.select({
431
+ message: "Select the action type",
432
+ options: [
433
+ {
434
+ value: "ask_agent" as const,
435
+ label: "ask_agent",
436
+ hint: "Send a prompt to the AI agent",
437
+ },
438
+ {
439
+ value: "run_command" as const,
440
+ label: "run_command",
441
+ hint: "Execute a shell command",
442
+ },
443
+ ],
444
+ });
445
+ handleCancel(actionType);
446
+
447
+ let action: CanonicalHook["action"];
448
+
449
+ if (actionType === "ask_agent") {
450
+ const prompt = await p.text({
451
+ message: "Agent prompt",
452
+ validate: (val) => {
453
+ if (!val || val.trim().length === 0) return "Prompt cannot be empty";
454
+ return undefined;
455
+ },
456
+ });
457
+ handleCancel(prompt);
458
+ action = { type: "ask_agent", prompt: prompt as string };
459
+ } else {
460
+ const command = await p.text({
461
+ message: "Shell command",
462
+ validate: (val) => {
463
+ if (!val || val.trim().length === 0) return "Command cannot be empty";
464
+ return undefined;
465
+ },
466
+ });
467
+ handleCancel(command);
468
+ action = { type: "run_command", command: command as string };
469
+ }
470
+
471
+ const hookName = await p.text({
472
+ message: "Hook name",
473
+ validate: (val) => {
474
+ if (!val || val.trim().length === 0) return "Hook name cannot be empty";
475
+ return undefined;
476
+ },
477
+ });
478
+ handleCancel(hookName);
479
+
480
+ const condition: CanonicalHook["condition"] =
481
+ filePatterns || toolTypes
482
+ ? {
483
+ ...(filePatterns ? { file_patterns: filePatterns } : {}),
484
+ ...(toolTypes ? { tool_types: toolTypes } : {}),
485
+ }
486
+ : undefined;
487
+
488
+ const hook = {
489
+ name: hookName as string,
490
+ event: event as CanonicalHook["event"],
491
+ ...(condition ? { condition } : {}),
492
+ action,
493
+ };
494
+
495
+ const validation = CanonicalHookSchema.safeParse(hook);
496
+ if (!validation.success) {
497
+ const messages = validation.error.issues.map((i) => i.message).join("; ");
498
+ p.log.error(`Hook validation failed: ${messages}`);
499
+ const retry = await p.confirm({
500
+ message: "Would you like to retry this hook?",
501
+ });
502
+ handleCancel(retry);
503
+ if (retry) {
504
+ return promptSingleHook();
505
+ }
506
+ return null;
507
+ }
508
+
509
+ return validation.data;
510
+ }
511
+
512
+ /**
513
+ * Loop collecting hook definitions until the user declines to add more.
514
+ * Returns an array of validated hooks (may be empty).
515
+ */
516
+ export async function promptHooks(): Promise<CanonicalHook[]> {
517
+ const hooks: CanonicalHook[] = [];
518
+
519
+ let addHook = await p.confirm({
520
+ message: "Would you like to add a hook?",
521
+ });
522
+ handleCancel(addHook);
523
+
524
+ while (addHook) {
525
+ const hook = await promptSingleHook();
526
+ if (hook) {
527
+ hooks.push(hook);
528
+ }
529
+
530
+ addHook = await p.confirm({
531
+ message: "Would you like to add another hook?",
532
+ });
533
+ handleCancel(addHook);
534
+ }
535
+
536
+ return hooks;
537
+ }
538
+
539
+ /**
540
+ * Collect one MCP server definition via interactive prompts.
541
+ * Returns the validated server, or null if the user chooses to skip after a validation failure.
542
+ */
543
+ async function promptSingleMcpServer(): Promise<McpServerDefinition | null> {
544
+ const name = await p.text({
545
+ message: "Server name",
546
+ validate: (val) => {
547
+ if (!val || val.trim().length === 0) return "Server name cannot be empty";
548
+ return undefined;
549
+ },
550
+ });
551
+ handleCancel(name);
552
+
553
+ const command = await p.text({
554
+ message: "Server command",
555
+ validate: (val) => {
556
+ if (!val || val.trim().length === 0) return "Command cannot be empty";
557
+ return undefined;
558
+ },
559
+ });
560
+ handleCancel(command);
561
+
562
+ const argsRaw = await p.text({
563
+ message: "Command arguments (space-separated)",
564
+ defaultValue: "",
565
+ });
566
+ handleCancel(argsRaw);
567
+
568
+ const args = (argsRaw as string)
569
+ .split(" ")
570
+ .map((s) => s.trim())
571
+ .filter((s) => s.length > 0);
572
+
573
+ const envRaw = await p.text({
574
+ message: "Environment variables (KEY=VALUE, comma-separated)",
575
+ defaultValue: "",
576
+ });
577
+ handleCancel(envRaw);
578
+
579
+ const env = (envRaw as string).trim()
580
+ ? parseKeyValuePairs(envRaw as string)
581
+ : {};
582
+
583
+ const server = {
584
+ name: name as string,
585
+ command: command as string,
586
+ args,
587
+ env,
588
+ };
589
+
590
+ const validation = McpServerDefinitionSchema.safeParse(server);
591
+ if (!validation.success) {
592
+ const messages = validation.error.issues.map((i) => i.message).join("; ");
593
+ p.log.error(`MCP server validation failed: ${messages}`);
594
+ const retry = await p.confirm({
595
+ message: "Would you like to retry this MCP server?",
596
+ });
597
+ handleCancel(retry);
598
+ if (retry) {
599
+ return promptSingleMcpServer();
600
+ }
601
+ return null;
602
+ }
603
+
604
+ return validation.data;
605
+ }
606
+
607
+ /**
608
+ * Loop collecting MCP server definitions until the user declines to add more.
609
+ * Returns an array of validated MCP servers (may be empty).
610
+ */
611
+ export async function promptMcpServers(): Promise<McpServerDefinition[]> {
612
+ const servers: McpServerDefinition[] = [];
613
+
614
+ let addServer = await p.confirm({
615
+ message: "Would you like to add an MCP server?",
616
+ });
617
+ handleCancel(addServer);
618
+
619
+ while (addServer) {
620
+ const server = await promptSingleMcpServer();
621
+ if (server) {
622
+ servers.push(server);
623
+ }
624
+
625
+ addServer = await p.confirm({
626
+ message: "Would you like to add another MCP server?",
627
+ });
628
+ handleCancel(addServer);
629
+ }
630
+
631
+ return servers;
632
+ }
633
+
634
+ /**
635
+ * Run the full interactive wizard.
636
+ * Orchestrates the prompt flow: intro → frontmatter → knowledge body → hooks → MCP servers.
637
+ */
638
+ export async function runWizard(
639
+ artifactName: string,
640
+ displayName: string,
641
+ preSelectedType?: AssetType,
642
+ ): Promise<WizardResult> {
643
+ p.intro(`Configuring artifact: ${displayName}`);
644
+
645
+ const frontmatter = await promptFrontmatter(
646
+ artifactName,
647
+ displayName,
648
+ preSelectedType,
649
+ );
650
+ const knowledgeBody = await promptKnowledgeBody();
651
+ const hooks = await promptHooks();
652
+ const mcpServers = await promptMcpServers();
653
+
654
+ return {
655
+ frontmatter,
656
+ knowledgeBody,
657
+ hooks,
658
+ mcpServers,
659
+ };
660
+ }