@thinkingsage/kanon 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +410 -0
- package/LICENSE +21 -0
- package/README.md +168 -0
- package/bridge/mcp-server.cjs +14171 -0
- package/package.json +98 -0
- package/src/adapters/capabilities.ts +178 -0
- package/src/adapters/claude-code.ts +110 -0
- package/src/adapters/cline.ts +98 -0
- package/src/adapters/codex.ts +173 -0
- package/src/adapters/copilot.ts +106 -0
- package/src/adapters/cursor.ts +97 -0
- package/src/adapters/degradation.ts +95 -0
- package/src/adapters/index.ts +324 -0
- package/src/adapters/kiro-frontmatter.ts +139 -0
- package/src/adapters/kiro-inclusion.ts +86 -0
- package/src/adapters/kiro.ts +412 -0
- package/src/adapters/qdeveloper.ts +115 -0
- package/src/adapters/types.ts +81 -0
- package/src/adapters/windsurf.ts +96 -0
- package/src/admin.ts +283 -0
- package/src/asset-conventions.ts +118 -0
- package/src/attribution-backfill.ts +319 -0
- package/src/attribution-report.ts +95 -0
- package/src/attribution.ts +239 -0
- package/src/backends/github.ts +194 -0
- package/src/backends/http.ts +122 -0
- package/src/backends/index.ts +39 -0
- package/src/backends/local.ts +47 -0
- package/src/backends/s3.ts +157 -0
- package/src/backends/types.ts +59 -0
- package/src/base-cache.ts +270 -0
- package/src/browse-ui.ts +3754 -0
- package/src/browse.ts +1038 -0
- package/src/build.ts +1108 -0
- package/src/catalog.ts +204 -0
- package/src/cli-deprecated.ts +29 -0
- package/src/cli.ts +773 -0
- package/src/collection-admin.ts +287 -0
- package/src/collection-builder.ts +464 -0
- package/src/collections.ts +116 -0
- package/src/compatibility.ts +105 -0
- package/src/config.ts +743 -0
- package/src/eval/rubrics/kiro-progressive-steering.ts +841 -0
- package/src/eval.ts +1169 -0
- package/src/file-writer.ts +61 -0
- package/src/format-registry.ts +141 -0
- package/src/guild/auto-updater.ts +163 -0
- package/src/guild/backend-resolver.ts +49 -0
- package/src/guild/cli.ts +592 -0
- package/src/guild/collection-expander.ts +47 -0
- package/src/guild/global-cache.ts +247 -0
- package/src/guild/hook-generator.ts +100 -0
- package/src/guild/manifest.ts +154 -0
- package/src/guild/path-utils.ts +12 -0
- package/src/guild/sync.ts +622 -0
- package/src/guild/version-resolver.ts +42 -0
- package/src/help/metadata.ts +445 -0
- package/src/help/renderer.ts +265 -0
- package/src/help/typo-suggester.ts +25 -0
- package/src/hooks/expression.ts +493 -0
- package/src/hooks/pipeline.ts +141 -0
- package/src/import.ts +773 -0
- package/src/importers/claude-code.ts +134 -0
- package/src/importers/cline.ts +103 -0
- package/src/importers/codex.ts +140 -0
- package/src/importers/copilot.ts +103 -0
- package/src/importers/cursor.ts +105 -0
- package/src/importers/index.ts +390 -0
- package/src/importers/kiro.ts +110 -0
- package/src/importers/qdeveloper.ts +103 -0
- package/src/importers/types.ts +54 -0
- package/src/importers/windsurf.ts +104 -0
- package/src/install.ts +1005 -0
- package/src/manifest-admin.ts +306 -0
- package/src/mcp-bridge.ts +240 -0
- package/src/mutation/delta.ts +50 -0
- package/src/mutation/history.ts +66 -0
- package/src/mutation/operators.ts +524 -0
- package/src/mutation/runner.ts +332 -0
- package/src/new.ts +106 -0
- package/src/outcomes/collision.ts +127 -0
- package/src/outcomes/normalize.ts +208 -0
- package/src/outcomes/registry.ts +173 -0
- package/src/parser.ts +446 -0
- package/src/provenance-backfill-cli.ts +319 -0
- package/src/provenance-backfill.ts +520 -0
- package/src/publish.ts +354 -0
- package/src/reconcile-orchestrator.ts +502 -0
- package/src/reconcile-report-renderer.ts +176 -0
- package/src/resolve-body.ts +15 -0
- package/src/rosetta/builtins/compatibility-profiles.ts +297 -0
- package/src/rosetta/builtins/contracts.ts +1033 -0
- package/src/rosetta/builtins/pretty-printers/claude-code-native.ts +122 -0
- package/src/rosetta/builtins/pretty-printers/cline-native.ts +50 -0
- package/src/rosetta/builtins/pretty-printers/codex-native.ts +127 -0
- package/src/rosetta/builtins/pretty-printers/copilot-native.ts +50 -0
- package/src/rosetta/builtins/pretty-printers/cursor-native.ts +50 -0
- package/src/rosetta/builtins/pretty-printers/index.ts +81 -0
- package/src/rosetta/builtins/pretty-printers/kiro-native.ts +166 -0
- package/src/rosetta/builtins/pretty-printers/kiro-power.ts +108 -0
- package/src/rosetta/builtins/pretty-printers/kiro-skill.ts +88 -0
- package/src/rosetta/builtins/pretty-printers/qdeveloper-native.ts +51 -0
- package/src/rosetta/builtins/pretty-printers/superpowers.ts +97 -0
- package/src/rosetta/builtins/pretty-printers/windsurf-native.ts +50 -0
- package/src/rosetta/builtins/sources/claude-code-native.ts +348 -0
- package/src/rosetta/builtins/sources/cline-native.ts +176 -0
- package/src/rosetta/builtins/sources/codex-native.ts +343 -0
- package/src/rosetta/builtins/sources/copilot-native.ts +178 -0
- package/src/rosetta/builtins/sources/cursor-native.ts +176 -0
- package/src/rosetta/builtins/sources/index.ts +95 -0
- package/src/rosetta/builtins/sources/kiro-native.ts +462 -0
- package/src/rosetta/builtins/sources/kiro-power.ts +285 -0
- package/src/rosetta/builtins/sources/kiro-skill.ts +230 -0
- package/src/rosetta/builtins/sources/qdeveloper-native.ts +181 -0
- package/src/rosetta/builtins/sources/superpowers.ts +240 -0
- package/src/rosetta/builtins/sources/windsurf-native.ts +176 -0
- package/src/rosetta/builtins/targets/claude-code.ts +181 -0
- package/src/rosetta/builtins/targets/cline.ts +87 -0
- package/src/rosetta/builtins/targets/codex.ts +226 -0
- package/src/rosetta/builtins/targets/copilot.ts +103 -0
- package/src/rosetta/builtins/targets/cursor.ts +87 -0
- package/src/rosetta/builtins/targets/index.ts +60 -0
- package/src/rosetta/builtins/targets/kiro.ts +278 -0
- package/src/rosetta/builtins/targets/qdeveloper.ts +103 -0
- package/src/rosetta/builtins/targets/windsurf.ts +87 -0
- package/src/rosetta/canonical.ts +729 -0
- package/src/rosetta/compatibility.ts +432 -0
- package/src/rosetta/contracts.ts +329 -0
- package/src/rosetta/detector.ts +724 -0
- package/src/rosetta/diagnostics.ts +630 -0
- package/src/rosetta/engine-bootstrap.ts +103 -0
- package/src/rosetta/engine.ts +744 -0
- package/src/rosetta/index.ts +381 -0
- package/src/rosetta/inspection.ts +530 -0
- package/src/rosetta/plan.ts +448 -0
- package/src/rosetta/provenance-digest.ts +369 -0
- package/src/rosetta/reconcile.ts +812 -0
- package/src/rosetta/redaction.ts +467 -0
- package/src/rosetta/registry.ts +712 -0
- package/src/rosetta/renderers.ts +571 -0
- package/src/rosetta/request-guard.ts +335 -0
- package/src/rosetta/resolution.ts +419 -0
- package/src/rosetta/source-accounting.ts +233 -0
- package/src/rosetta/templates.ts +129 -0
- package/src/rosetta-cli.ts +717 -0
- package/src/rosetta-docs-generator.ts +793 -0
- package/src/rosetta-profiles-cli.ts +367 -0
- package/src/schemas.ts +1712 -0
- package/src/spec-coordination.ts +1141 -0
- package/src/temper.ts +747 -0
- package/src/template-bundle-loader.ts +312 -0
- package/src/template-engine.ts +53 -0
- package/src/translation-application-policy.ts +496 -0
- package/src/translation-orchestrator.ts +1013 -0
- package/src/translation-plan-applier.ts +473 -0
- package/src/tutorial.ts +305 -0
- package/src/validate.ts +1093 -0
- package/src/versioning.ts +553 -0
- package/src/wizard.ts +660 -0
- package/src/workspace.ts +237 -0
- package/templates/eval-contexts/claude-code.md.njk +6 -0
- package/templates/eval-contexts/cline.md.njk +6 -0
- package/templates/eval-contexts/copilot.md.njk +6 -0
- package/templates/eval-contexts/cursor.md.njk +6 -0
- package/templates/eval-contexts/kiro.md.njk +10 -0
- package/templates/eval-contexts/qdeveloper.md.njk +6 -0
- package/templates/eval-contexts/windsurf.md.njk +6 -0
- package/templates/harness-adapters/_base/attribution-footer.md.njk +17 -0
- package/templates/harness-adapters/_base/base.md.njk +16 -0
- package/templates/harness-adapters/claude-code/claude.md.njk +1 -0
- package/templates/harness-adapters/claude-code/mcp.json.njk +1 -0
- package/templates/harness-adapters/claude-code/settings.json.njk +1 -0
- package/templates/harness-adapters/claude-code/skill-library-index.md.njk +13 -0
- package/templates/harness-adapters/claude-code/skill.md.njk +19 -0
- package/templates/harness-adapters/cline/hook.sh.njk +4 -0
- package/templates/harness-adapters/cline/mcp.json.njk +1 -0
- package/templates/harness-adapters/cline/rule.md.njk +1 -0
- package/templates/harness-adapters/codex/agents-md.md.njk +6 -0
- package/templates/harness-adapters/codex/agents-pointer.md.njk +16 -0
- package/templates/harness-adapters/codex/skill.md.njk +27 -0
- package/templates/harness-adapters/copilot/agents.md.njk +1 -0
- package/templates/harness-adapters/copilot/instructions.md.njk +1 -0
- package/templates/harness-adapters/copilot/scoped.md.njk +6 -0
- package/templates/harness-adapters/cursor/mcp.json.njk +1 -0
- package/templates/harness-adapters/cursor/rule.md.njk +6 -0
- package/templates/harness-adapters/kiro/hook.json.njk +1 -0
- package/templates/harness-adapters/kiro/mcp.json.njk +1 -0
- package/templates/harness-adapters/kiro/power-steering.md.njk +3 -0
- package/templates/harness-adapters/kiro/power.md.njk +12 -0
- package/templates/harness-adapters/kiro/steering.md.njk +16 -0
- package/templates/harness-adapters/qdeveloper/agent.md.njk +1 -0
- package/templates/harness-adapters/qdeveloper/mcp.json.njk +1 -0
- package/templates/harness-adapters/qdeveloper/rule.md.njk +1 -0
- package/templates/harness-adapters/windsurf/mcp.json.njk +1 -0
- package/templates/harness-adapters/windsurf/rule.md.njk +1 -0
- package/templates/harness-adapters/windsurf/workflow.md.njk +1 -0
- package/templates/knowledge/hooks.yaml.njk +4 -0
- package/templates/knowledge/knowledge.md.njk +53 -0
- package/templates/knowledge/mcp-servers.yaml.njk +2 -0
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
import { exists, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import * as p from "@clack/prompts";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import { generateCatalog, SOURCE_DIRS } from "./catalog";
|
|
6
|
+
import {
|
|
7
|
+
buildCollectionMembership,
|
|
8
|
+
loadCollections,
|
|
9
|
+
validateArtifactCollectionRefs,
|
|
10
|
+
} from "./collections";
|
|
11
|
+
import type { Collection, HarnessName } from "./schemas";
|
|
12
|
+
import { SUPPORTED_HARNESSES, TrustLaneSchema } from "./schemas";
|
|
13
|
+
|
|
14
|
+
export interface CollectionBuildResult {
|
|
15
|
+
collection: string;
|
|
16
|
+
harness: string;
|
|
17
|
+
artifactsIncluded: number;
|
|
18
|
+
filesWritten: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Build a single collection bundle for a single harness.
|
|
23
|
+
*
|
|
24
|
+
* Copies all pre-built dist artifacts whose `collections` field includes
|
|
25
|
+
* this collection into `dist/<harness>/collections/<collection-name>/`.
|
|
26
|
+
* Each artifact's files land in a subdirectory named after the artifact,
|
|
27
|
+
* preserving the same structure as a normal artifact dist dir.
|
|
28
|
+
*/
|
|
29
|
+
async function buildCollectionForHarness(
|
|
30
|
+
collection: Collection,
|
|
31
|
+
members: string[], // artifact names belonging to this collection
|
|
32
|
+
harness: HarnessName,
|
|
33
|
+
distDir: string,
|
|
34
|
+
): Promise<{ artifactsIncluded: number; filesWritten: number }> {
|
|
35
|
+
const collectionOutDir = join(
|
|
36
|
+
distDir,
|
|
37
|
+
harness,
|
|
38
|
+
"collections",
|
|
39
|
+
collection.name,
|
|
40
|
+
);
|
|
41
|
+
await mkdir(collectionOutDir, { recursive: true });
|
|
42
|
+
|
|
43
|
+
let artifactsIncluded = 0;
|
|
44
|
+
let filesWritten = 0;
|
|
45
|
+
|
|
46
|
+
for (const artifactName of members) {
|
|
47
|
+
const artifactDistDir = join(distDir, harness, artifactName);
|
|
48
|
+
|
|
49
|
+
if (!(await exists(artifactDistDir))) continue;
|
|
50
|
+
|
|
51
|
+
const destDir = join(collectionOutDir, artifactName);
|
|
52
|
+
await mkdir(destDir, { recursive: true });
|
|
53
|
+
|
|
54
|
+
// Recursively copy all files from the artifact's dist dir
|
|
55
|
+
const copied = await copyDir(artifactDistDir, destDir);
|
|
56
|
+
if (copied > 0) {
|
|
57
|
+
artifactsIncluded++;
|
|
58
|
+
filesWritten += copied;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return { artifactsIncluded, filesWritten };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function copyDir(src: string, dest: string): Promise<number> {
|
|
66
|
+
let count = 0;
|
|
67
|
+
const entries = await readdir(src, { withFileTypes: true });
|
|
68
|
+
|
|
69
|
+
for (const entry of entries) {
|
|
70
|
+
const srcPath = join(src, entry.name);
|
|
71
|
+
const destPath = join(dest, entry.name);
|
|
72
|
+
|
|
73
|
+
if (entry.isDirectory()) {
|
|
74
|
+
await mkdir(destPath, { recursive: true });
|
|
75
|
+
count += await copyDir(srcPath, destPath);
|
|
76
|
+
} else if (entry.isFile()) {
|
|
77
|
+
const content = await readFile(srcPath);
|
|
78
|
+
await writeFile(destPath, content);
|
|
79
|
+
count++;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return count;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Build all collection bundles for all (or a specific) harness.
|
|
88
|
+
*
|
|
89
|
+
* Requires `kanon build` to have already run — this command reads from
|
|
90
|
+
* the existing dist/ output rather than re-compiling artifacts.
|
|
91
|
+
*/
|
|
92
|
+
export async function buildCollections(options: {
|
|
93
|
+
collectionsDir: string;
|
|
94
|
+
distDir: string;
|
|
95
|
+
harness?: HarnessName;
|
|
96
|
+
sourceDirs?: string[];
|
|
97
|
+
}): Promise<CollectionBuildResult[]> {
|
|
98
|
+
const { collectionsDir, distDir, harness } = options;
|
|
99
|
+
const sourceDirs = options.sourceDirs ?? [...SOURCE_DIRS];
|
|
100
|
+
const results: CollectionBuildResult[] = [];
|
|
101
|
+
|
|
102
|
+
// Load collection manifests
|
|
103
|
+
const collections = await loadCollections(collectionsDir);
|
|
104
|
+
if (collections.length === 0) {
|
|
105
|
+
console.error(
|
|
106
|
+
chalk.yellow("No collection manifests found in collections/"),
|
|
107
|
+
);
|
|
108
|
+
return results;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Load catalog to get collection membership from artifact frontmatter
|
|
112
|
+
const catalogEntries = await generateCatalog(sourceDirs);
|
|
113
|
+
|
|
114
|
+
// Warn on unregistered collection references
|
|
115
|
+
const refWarnings = validateArtifactCollectionRefs(
|
|
116
|
+
catalogEntries,
|
|
117
|
+
collections,
|
|
118
|
+
);
|
|
119
|
+
for (const w of refWarnings) {
|
|
120
|
+
console.error(chalk.yellow(` ⚠ ${w.field}: ${w.message}`));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Build membership map: collection name → artifact names
|
|
124
|
+
const membership = buildCollectionMembership(catalogEntries);
|
|
125
|
+
|
|
126
|
+
const targetHarnesses: HarnessName[] = harness
|
|
127
|
+
? [harness]
|
|
128
|
+
: [...SUPPORTED_HARNESSES];
|
|
129
|
+
|
|
130
|
+
for (const collection of collections) {
|
|
131
|
+
const members = membership.get(collection.name) ?? [];
|
|
132
|
+
|
|
133
|
+
if (members.length === 0) {
|
|
134
|
+
console.error(
|
|
135
|
+
chalk.yellow(
|
|
136
|
+
` ⚠ Collection "${collection.name}" has no member artifacts`,
|
|
137
|
+
),
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
for (const h of targetHarnesses) {
|
|
142
|
+
const { artifactsIncluded, filesWritten } =
|
|
143
|
+
await buildCollectionForHarness(collection, members, h, distDir);
|
|
144
|
+
results.push({
|
|
145
|
+
collection: collection.name,
|
|
146
|
+
harness: h,
|
|
147
|
+
artifactsIncluded,
|
|
148
|
+
filesWritten,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return results;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Default `kanon collection` action — shows a status overview of all collection
|
|
158
|
+
* manifests, their member counts, trust lanes, and any validation warnings.
|
|
159
|
+
*/
|
|
160
|
+
export async function collectionStatusCommand(): Promise<void> {
|
|
161
|
+
// When called as `kanon collection help`, show help instead of status
|
|
162
|
+
const collectionArgIdx = process.argv.indexOf("collection");
|
|
163
|
+
const nextArg =
|
|
164
|
+
collectionArgIdx >= 0 ? process.argv[collectionArgIdx + 1] : undefined;
|
|
165
|
+
if (nextArg === "help") {
|
|
166
|
+
process.argv.splice(collectionArgIdx + 1, 1, "--help");
|
|
167
|
+
// Re-parse will be handled by Commander on the next tick; just return to let it flow
|
|
168
|
+
console.log(
|
|
169
|
+
[
|
|
170
|
+
"",
|
|
171
|
+
" Manage knowledge collections",
|
|
172
|
+
"",
|
|
173
|
+
" Usage: forge collection [options] [command]",
|
|
174
|
+
"",
|
|
175
|
+
" Commands:",
|
|
176
|
+
" new Scaffold a new collection manifest",
|
|
177
|
+
" build Build collection bundles from dist artifacts",
|
|
178
|
+
"",
|
|
179
|
+
" Options:",
|
|
180
|
+
" -h, --help display help for command",
|
|
181
|
+
"",
|
|
182
|
+
].join("\n"),
|
|
183
|
+
);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const collectionsDir = "collections";
|
|
188
|
+
const collections = await loadCollections(collectionsDir);
|
|
189
|
+
const catalogEntries = await generateCatalog([...SOURCE_DIRS]);
|
|
190
|
+
const membership = buildCollectionMembership(catalogEntries);
|
|
191
|
+
const warnings = validateArtifactCollectionRefs(catalogEntries, collections);
|
|
192
|
+
|
|
193
|
+
if (collections.length === 0 && warnings.length === 0) {
|
|
194
|
+
console.log("");
|
|
195
|
+
console.log(chalk.bold(" No collections yet."));
|
|
196
|
+
console.log("");
|
|
197
|
+
console.log(
|
|
198
|
+
chalk.dim(
|
|
199
|
+
' Collections are curated bundles of artifacts (e.g. "aws", "security", "onboarding").',
|
|
200
|
+
),
|
|
201
|
+
);
|
|
202
|
+
console.log(
|
|
203
|
+
chalk.dim(" Artifacts declare membership in their frontmatter: ") +
|
|
204
|
+
chalk.cyan("collections: [aws]"),
|
|
205
|
+
);
|
|
206
|
+
console.log("");
|
|
207
|
+
|
|
208
|
+
const create = await p.confirm({
|
|
209
|
+
message: "Create your first collection now?",
|
|
210
|
+
initialValue: true,
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
if (p.isCancel(create) || !create) {
|
|
214
|
+
console.log("");
|
|
215
|
+
console.log(
|
|
216
|
+
chalk.dim(" Run ") +
|
|
217
|
+
chalk.cyan("forge collection new <name>") +
|
|
218
|
+
chalk.dim(" when you're ready."),
|
|
219
|
+
);
|
|
220
|
+
console.log("");
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const name = await p.text({
|
|
225
|
+
message: "Collection name (kebab-case)",
|
|
226
|
+
placeholder: "my-collection",
|
|
227
|
+
validate: (v) => {
|
|
228
|
+
if (!v || v.trim().length === 0) return "Name is required";
|
|
229
|
+
if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(v.trim()))
|
|
230
|
+
return "Must be kebab-case (e.g. my-collection)";
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
if (p.isCancel(name)) return;
|
|
234
|
+
|
|
235
|
+
await collectionNewCommand(name as string);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const total = collections.length;
|
|
240
|
+
const totalMembers = [...membership.values()].reduce(
|
|
241
|
+
(s, m) => s + m.length,
|
|
242
|
+
0,
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
console.log(
|
|
246
|
+
chalk.bold(`\n Collections`) +
|
|
247
|
+
chalk.dim(
|
|
248
|
+
` (${total} manifest${total !== 1 ? "s" : ""}, ${totalMembers} total memberships)\n`,
|
|
249
|
+
),
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
if (collections.length > 0) {
|
|
253
|
+
// Column widths
|
|
254
|
+
const nameWidth =
|
|
255
|
+
Math.max(...collections.map((c) => c.displayName.length), 4) + 2;
|
|
256
|
+
const trustWidth = 12;
|
|
257
|
+
|
|
258
|
+
for (const c of collections) {
|
|
259
|
+
const members = membership.get(c.name) ?? [];
|
|
260
|
+
const memberCount = members.length;
|
|
261
|
+
const trustBadge = c.trust
|
|
262
|
+
? chalk.dim(`[${c.trust}]`).padEnd(trustWidth + 7) // pad accounts for dim codes
|
|
263
|
+
: "".padEnd(trustWidth);
|
|
264
|
+
const nameStr = chalk.cyan(c.displayName.padEnd(nameWidth));
|
|
265
|
+
const countStr =
|
|
266
|
+
memberCount === 0
|
|
267
|
+
? chalk.yellow(`${memberCount} members`)
|
|
268
|
+
: chalk.green(`${memberCount} member${memberCount !== 1 ? "s" : ""}`);
|
|
269
|
+
const desc = c.description ? chalk.dim(` ${c.description}`) : "";
|
|
270
|
+
|
|
271
|
+
console.log(` ${nameStr}${trustBadge}${countStr}${desc}`);
|
|
272
|
+
|
|
273
|
+
if (members.length > 0 && members.length <= 5) {
|
|
274
|
+
console.log(chalk.dim(` ${members.join(", ")}`));
|
|
275
|
+
} else if (members.length > 5) {
|
|
276
|
+
console.log(
|
|
277
|
+
chalk.dim(
|
|
278
|
+
` ${members.slice(0, 4).join(", ")}, +${members.length - 4} more`,
|
|
279
|
+
),
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Unknown collection references (artifacts pointing to non-existent manifests)
|
|
286
|
+
if (warnings.length > 0) {
|
|
287
|
+
const uniqueUnknown = [
|
|
288
|
+
...new Set(warnings.map((w) => w.message.match(/"([^"]+)"/)?.[1] ?? "?")),
|
|
289
|
+
];
|
|
290
|
+
console.log("");
|
|
291
|
+
for (const name of uniqueUnknown) {
|
|
292
|
+
console.log(
|
|
293
|
+
chalk.yellow(
|
|
294
|
+
` ⚠ Artifacts declare collection "${name}" — no manifest found in collections/`,
|
|
295
|
+
),
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Artifacts with no collection membership
|
|
301
|
+
const unaffiliated = catalogEntries.filter(
|
|
302
|
+
(e) => e.collections.length === 0,
|
|
303
|
+
).length;
|
|
304
|
+
if (unaffiliated > 0) {
|
|
305
|
+
console.log(
|
|
306
|
+
chalk.dim(
|
|
307
|
+
`\n ${unaffiliated} artifact${unaffiliated !== 1 ? "s" : ""} not in any collection`,
|
|
308
|
+
),
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
console.log("");
|
|
313
|
+
console.log(
|
|
314
|
+
chalk.dim(" forge collection build — generate distributable bundles"),
|
|
315
|
+
);
|
|
316
|
+
console.log("");
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Scaffold a new collection manifest interactively.
|
|
321
|
+
* If `name` is provided, pre-populates the name prompt.
|
|
322
|
+
*/
|
|
323
|
+
export async function collectionNewCommand(name?: string): Promise<void> {
|
|
324
|
+
p.intro(chalk.bold("New collection"));
|
|
325
|
+
|
|
326
|
+
const collectionsDir = "collections";
|
|
327
|
+
await mkdir(collectionsDir, { recursive: true });
|
|
328
|
+
|
|
329
|
+
const collectionName =
|
|
330
|
+
name ??
|
|
331
|
+
((await p.text({
|
|
332
|
+
message: "Collection name (kebab-case)",
|
|
333
|
+
placeholder: "my-collection",
|
|
334
|
+
validate: (v) => {
|
|
335
|
+
if (!v || v.trim().length === 0) return "Name is required";
|
|
336
|
+
if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(v.trim()))
|
|
337
|
+
return "Must be kebab-case (e.g. my-collection)";
|
|
338
|
+
},
|
|
339
|
+
})) as string);
|
|
340
|
+
if (p.isCancel(collectionName)) {
|
|
341
|
+
p.cancel("Cancelled.");
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const filePath = join(collectionsDir, `${collectionName}.yaml`);
|
|
346
|
+
if (await exists(filePath)) {
|
|
347
|
+
console.error(
|
|
348
|
+
chalk.red(
|
|
349
|
+
` Collection "${collectionName}" already exists at ${filePath}`,
|
|
350
|
+
),
|
|
351
|
+
);
|
|
352
|
+
process.exit(1);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const displayName = (await p.text({
|
|
356
|
+
message: "Display name",
|
|
357
|
+
placeholder: collectionName
|
|
358
|
+
.split("-")
|
|
359
|
+
.map((w) => w[0].toUpperCase() + w.slice(1))
|
|
360
|
+
.join(" "),
|
|
361
|
+
})) as string;
|
|
362
|
+
if (p.isCancel(displayName)) {
|
|
363
|
+
p.cancel("Cancelled.");
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const description = (await p.text({
|
|
368
|
+
message: "Description (one sentence)",
|
|
369
|
+
placeholder: `Artifacts for ${displayName || collectionName}`,
|
|
370
|
+
})) as string;
|
|
371
|
+
if (p.isCancel(description)) {
|
|
372
|
+
p.cancel("Cancelled.");
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const trust = await p.select({
|
|
377
|
+
message: "Trust lane",
|
|
378
|
+
options: TrustLaneSchema.options.map((t) => ({
|
|
379
|
+
value: t,
|
|
380
|
+
label: t,
|
|
381
|
+
hint: {
|
|
382
|
+
official: "maintained by core team",
|
|
383
|
+
partner: "maintained by trusted partner",
|
|
384
|
+
community: "community contributed",
|
|
385
|
+
experimental: "experimental / untested",
|
|
386
|
+
}[t],
|
|
387
|
+
})),
|
|
388
|
+
initialValue: "community" as const,
|
|
389
|
+
});
|
|
390
|
+
if (p.isCancel(trust)) {
|
|
391
|
+
p.cancel("Cancelled.");
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const resolvedDisplayName =
|
|
396
|
+
(displayName as string).trim() ||
|
|
397
|
+
collectionName
|
|
398
|
+
.split("-")
|
|
399
|
+
.map((w) => w[0].toUpperCase() + w.slice(1))
|
|
400
|
+
.join(" ");
|
|
401
|
+
|
|
402
|
+
const yaml = `${[
|
|
403
|
+
`name: ${collectionName}`,
|
|
404
|
+
`displayName: "${resolvedDisplayName}"`,
|
|
405
|
+
`description: "${(description as string).trim()}"`,
|
|
406
|
+
`trust: ${trust}`,
|
|
407
|
+
`tags: []`,
|
|
408
|
+
].join("\n")}\n`;
|
|
409
|
+
|
|
410
|
+
await writeFile(filePath, yaml, "utf-8");
|
|
411
|
+
|
|
412
|
+
p.outro(
|
|
413
|
+
chalk.green(`✓ Created ${filePath}\n\n`) +
|
|
414
|
+
chalk.dim(` Add artifacts to this collection by setting:\n`) +
|
|
415
|
+
chalk.cyan(` collections: [${collectionName}]\n`) +
|
|
416
|
+
chalk.dim(` in their knowledge.md frontmatter.`),
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
export async function collectionBuildCommand(options: {
|
|
421
|
+
harness?: string;
|
|
422
|
+
}): Promise<void> {
|
|
423
|
+
const distDir = "dist";
|
|
424
|
+
const collectionsDir = "collections";
|
|
425
|
+
|
|
426
|
+
if (!(await exists(distDir))) {
|
|
427
|
+
console.error(
|
|
428
|
+
chalk.red("No dist/ directory found. Run `kanon build` first."),
|
|
429
|
+
);
|
|
430
|
+
process.exit(1);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (
|
|
434
|
+
options.harness &&
|
|
435
|
+
!(SUPPORTED_HARNESSES as readonly string[]).includes(options.harness)
|
|
436
|
+
) {
|
|
437
|
+
console.error(
|
|
438
|
+
chalk.red(
|
|
439
|
+
`Error: Unknown harness "${options.harness}". Valid harnesses: ${SUPPORTED_HARNESSES.join(", ")}`,
|
|
440
|
+
),
|
|
441
|
+
);
|
|
442
|
+
process.exit(1);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const results = await buildCollections({
|
|
446
|
+
collectionsDir,
|
|
447
|
+
distDir,
|
|
448
|
+
harness: options.harness as HarnessName | undefined,
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
const totalArtifacts = results.reduce((s, r) => s + r.artifactsIncluded, 0);
|
|
452
|
+
const totalFiles = results.reduce((s, r) => s + r.filesWritten, 0);
|
|
453
|
+
const uniqueCollections = new Set(results.map((r) => r.collection)).size;
|
|
454
|
+
|
|
455
|
+
console.error(
|
|
456
|
+
chalk.green(
|
|
457
|
+
`\n✓ Collection build complete: ${uniqueCollections} collection(s), ${totalArtifacts} artifact inclusions, ${totalFiles} files written`,
|
|
458
|
+
),
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
if (options.harness) {
|
|
462
|
+
console.error(` Harness: ${options.harness}`);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { exists, readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import { basename, join } from "node:path";
|
|
3
|
+
import * as yaml from "js-yaml";
|
|
4
|
+
import {
|
|
5
|
+
type CatalogEntry,
|
|
6
|
+
type Collection,
|
|
7
|
+
CollectionSchema,
|
|
8
|
+
type ValidationWarning,
|
|
9
|
+
} from "./schemas";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Load all collection manifests from a directory.
|
|
13
|
+
* Each `<name>.yaml` file is parsed against CollectionSchema.
|
|
14
|
+
* Files that fail validation are skipped with a console warning.
|
|
15
|
+
*/
|
|
16
|
+
export async function loadCollections(
|
|
17
|
+
collectionsDir: string,
|
|
18
|
+
): Promise<Collection[]> {
|
|
19
|
+
if (!(await exists(collectionsDir))) return [];
|
|
20
|
+
|
|
21
|
+
const entries = await readdir(collectionsDir);
|
|
22
|
+
const yamlFiles = entries
|
|
23
|
+
.filter((f) => f.endsWith(".yaml") || f.endsWith(".yml"))
|
|
24
|
+
.filter((f) => f !== ".gitkeep")
|
|
25
|
+
.sort();
|
|
26
|
+
|
|
27
|
+
const collections: Collection[] = [];
|
|
28
|
+
|
|
29
|
+
for (const file of yamlFiles) {
|
|
30
|
+
const filePath = join(collectionsDir, file);
|
|
31
|
+
let raw: string;
|
|
32
|
+
try {
|
|
33
|
+
raw = await readFile(filePath, "utf-8");
|
|
34
|
+
} catch {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let parsed: unknown;
|
|
39
|
+
try {
|
|
40
|
+
parsed = yaml.load(raw);
|
|
41
|
+
} catch {
|
|
42
|
+
console.error(`Warning: Skipping ${file} — invalid YAML`);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Infer name from filename if not set in the manifest
|
|
47
|
+
if (parsed && typeof parsed === "object" && !("name" in parsed)) {
|
|
48
|
+
(parsed as Record<string, unknown>).name = basename(
|
|
49
|
+
file,
|
|
50
|
+
".yaml",
|
|
51
|
+
).replace(/\.yml$/, "");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const result = CollectionSchema.safeParse(parsed);
|
|
55
|
+
if (!result.success) {
|
|
56
|
+
console.error(
|
|
57
|
+
`Warning: Skipping ${file} — schema validation failed: ${result.error.issues.map((i) => i.message).join(", ")}`,
|
|
58
|
+
);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
collections.push(result.data);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return collections;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Returns warnings for artifact catalog entries that declare a collection name
|
|
70
|
+
* not backed by any manifest in collectionsDir.
|
|
71
|
+
*
|
|
72
|
+
* This is a soft check — artifacts may be authored before a collection manifest
|
|
73
|
+
* exists, so this is a warning rather than an error.
|
|
74
|
+
*/
|
|
75
|
+
export function validateArtifactCollectionRefs(
|
|
76
|
+
entries: CatalogEntry[],
|
|
77
|
+
collections: Collection[],
|
|
78
|
+
): ValidationWarning[] {
|
|
79
|
+
const knownCollectionNames = new Set(collections.map((c) => c.name));
|
|
80
|
+
const warnings: ValidationWarning[] = [];
|
|
81
|
+
|
|
82
|
+
for (const entry of entries) {
|
|
83
|
+
for (const collectionRef of entry.collections) {
|
|
84
|
+
if (!knownCollectionNames.has(collectionRef)) {
|
|
85
|
+
warnings.push({
|
|
86
|
+
field: "collections",
|
|
87
|
+
message: `Artifact declares collection "${collectionRef}" but no matching manifest found in collections/`,
|
|
88
|
+
filePath: `${entry.path}/knowledge.md`,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return warnings;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Summarize collection membership across all catalog entries.
|
|
99
|
+
* Returns a map from collection name → array of artifact names that belong to it.
|
|
100
|
+
*/
|
|
101
|
+
export function buildCollectionMembership(
|
|
102
|
+
entries: CatalogEntry[],
|
|
103
|
+
): Map<string, string[]> {
|
|
104
|
+
const membership = new Map<string, string[]>();
|
|
105
|
+
|
|
106
|
+
for (const entry of entries) {
|
|
107
|
+
for (const collectionName of entry.collections) {
|
|
108
|
+
if (!membership.has(collectionName)) {
|
|
109
|
+
membership.set(collectionName, []);
|
|
110
|
+
}
|
|
111
|
+
membership.get(collectionName)?.push(entry.name);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return membership;
|
|
116
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { AssetType, HarnessName } from "./schemas";
|
|
2
|
+
|
|
3
|
+
export type CompatibilityLevel = "full" | "partial" | "none";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Declares how well each asset type is supported by each harness.
|
|
7
|
+
*
|
|
8
|
+
* - "full" — the harness has a native concept for this asset type
|
|
9
|
+
* - "partial" — the harness can represent this type but with degraded fidelity
|
|
10
|
+
* - "none" — the harness has no meaningful output for this type
|
|
11
|
+
*
|
|
12
|
+
* Omitting a harness from a type's entry implies "full" (all standard harnesses
|
|
13
|
+
* support skills/powers/rules fully by default).
|
|
14
|
+
*/
|
|
15
|
+
export const ASSET_HARNESS_COMPATIBILITY: Record<
|
|
16
|
+
AssetType,
|
|
17
|
+
Partial<Record<HarnessName, CompatibilityLevel>>
|
|
18
|
+
> = {
|
|
19
|
+
// Core types — all harnesses support these fully
|
|
20
|
+
skill: {},
|
|
21
|
+
// "power" is a deprecated alias for "skill" (see ADR-0051) — its asset-type
|
|
22
|
+
// compatibility should match skill's exactly, since taxonomy no longer
|
|
23
|
+
// carries different meaning. (Kiro's actual "power" *format* — as opposed
|
|
24
|
+
// to this deprecated *type* value — has no separate row here; format-level
|
|
25
|
+
// concerns live in format-registry.ts / adapters, not this asset-type table.)
|
|
26
|
+
power: {},
|
|
27
|
+
rule: {},
|
|
28
|
+
|
|
29
|
+
// Extended types
|
|
30
|
+
workflow: {
|
|
31
|
+
// Kiro, Copilot, and Q Developer have native workflow/agent file support
|
|
32
|
+
kiro: "full",
|
|
33
|
+
copilot: "full",
|
|
34
|
+
qdeveloper: "full",
|
|
35
|
+
"claude-code": "partial", // emitted as CLAUDE.md sections
|
|
36
|
+
cursor: "partial", // emitted as rule file
|
|
37
|
+
windsurf: "partial",
|
|
38
|
+
cline: "partial",
|
|
39
|
+
},
|
|
40
|
+
// This table answers a build-level question ("does the harness produce
|
|
41
|
+
// meaningful output for this asset type, or should getCompatibility's
|
|
42
|
+
// "none" tell build.ts to skip it entirely?") which is coarser than
|
|
43
|
+
// CAPABILITY_MATRIX[<harness>].agents in adapters/capabilities.ts (a
|
|
44
|
+
// feature-level question consumed by degradation/temper: "can this
|
|
45
|
+
// harness represent a declarative sub-agent file?"). The two should never
|
|
46
|
+
// *contradict* — a "full" here must not pair with "none"/"partial" there —
|
|
47
|
+
// but they may legitimately differ where a harness has no native agent
|
|
48
|
+
// format yet still emits generic, meaningful output (kiro, claude-code,
|
|
49
|
+
// codex all fall into this bucket: no dedicated agent surface, but the
|
|
50
|
+
// adapter still writes real output, hence "partial" rather than "none").
|
|
51
|
+
// See ADR-0050.
|
|
52
|
+
agent: {
|
|
53
|
+
kiro: "partial", // no dedicated agent format; rendered as steering/power prose
|
|
54
|
+
"claude-code": "partial", // no agent surface; rendered as generic CLAUDE.md prose
|
|
55
|
+
codex: "partial", // sub-agents exist via profiles, not declarative files
|
|
56
|
+
copilot: "full",
|
|
57
|
+
cursor: "none",
|
|
58
|
+
windsurf: "none",
|
|
59
|
+
cline: "none",
|
|
60
|
+
qdeveloper: "full",
|
|
61
|
+
},
|
|
62
|
+
prompt: {
|
|
63
|
+
// Prompts are universally representable as steering/rule content
|
|
64
|
+
kiro: "full",
|
|
65
|
+
"claude-code": "full",
|
|
66
|
+
codex: "full",
|
|
67
|
+
copilot: "full",
|
|
68
|
+
cursor: "full",
|
|
69
|
+
windsurf: "full",
|
|
70
|
+
cline: "full",
|
|
71
|
+
qdeveloper: "full",
|
|
72
|
+
},
|
|
73
|
+
template: {
|
|
74
|
+
// Templates are reference material — includable in any harness context
|
|
75
|
+
kiro: "full",
|
|
76
|
+
"claude-code": "full",
|
|
77
|
+
copilot: "partial",
|
|
78
|
+
cursor: "partial",
|
|
79
|
+
windsurf: "partial",
|
|
80
|
+
cline: "partial",
|
|
81
|
+
qdeveloper: "partial",
|
|
82
|
+
},
|
|
83
|
+
"reference-pack": {
|
|
84
|
+
// Reference packs are manual-inclusion only; all harnesses can host them
|
|
85
|
+
kiro: "full",
|
|
86
|
+
"claude-code": "full",
|
|
87
|
+
codex: "full",
|
|
88
|
+
copilot: "full",
|
|
89
|
+
cursor: "full",
|
|
90
|
+
windsurf: "full",
|
|
91
|
+
cline: "full",
|
|
92
|
+
qdeveloper: "full",
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Returns the compatibility level for a given asset type and harness.
|
|
98
|
+
* Defaults to "full" if the harness is not explicitly listed for that type.
|
|
99
|
+
*/
|
|
100
|
+
export function getCompatibility(
|
|
101
|
+
type: AssetType,
|
|
102
|
+
harness: HarnessName,
|
|
103
|
+
): CompatibilityLevel {
|
|
104
|
+
return ASSET_HARNESS_COMPATIBILITY[type]?.[harness] ?? "full";
|
|
105
|
+
}
|