@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,306 @@
|
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import * as yaml from "js-yaml";
|
|
3
|
+
import type { Manifest, ManifestEntry } from "./guild/manifest";
|
|
4
|
+
import {
|
|
5
|
+
ArtifactManifestEntrySchema,
|
|
6
|
+
CollectionManifestEntrySchema,
|
|
7
|
+
parseManifest,
|
|
8
|
+
printManifest,
|
|
9
|
+
} from "./guild/manifest";
|
|
10
|
+
import type { SyncLock } from "./guild/sync";
|
|
11
|
+
|
|
12
|
+
/** Input shape for adding a manifest entry */
|
|
13
|
+
export interface ManifestEntryInput {
|
|
14
|
+
name?: string; // for artifact refs
|
|
15
|
+
collection?: string; // for collection refs
|
|
16
|
+
version: string;
|
|
17
|
+
mode: "required" | "optional";
|
|
18
|
+
harnesses?: string[];
|
|
19
|
+
backend?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Sync status for a single manifest entry */
|
|
23
|
+
export interface EntryStatus {
|
|
24
|
+
identifier: string; // name or collection
|
|
25
|
+
type: "artifact" | "collection";
|
|
26
|
+
version: string;
|
|
27
|
+
mode: string;
|
|
28
|
+
syncedVersion: string | null;
|
|
29
|
+
status: "synced" | "outdated" | "missing";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Full sync status response */
|
|
33
|
+
export interface SyncStatusResponse {
|
|
34
|
+
entries: EntryStatus[];
|
|
35
|
+
syncedAt: string | null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Reads and parses the manifest file, returns empty manifest if file doesn't exist */
|
|
39
|
+
export async function readManifest(
|
|
40
|
+
manifestPath: string,
|
|
41
|
+
): Promise<{ manifest: Manifest; raw: Record<string, unknown> }> {
|
|
42
|
+
let content: string;
|
|
43
|
+
try {
|
|
44
|
+
content = await readFile(manifestPath, "utf-8");
|
|
45
|
+
} catch (err: unknown) {
|
|
46
|
+
if (
|
|
47
|
+
err &&
|
|
48
|
+
typeof err === "object" &&
|
|
49
|
+
"code" in err &&
|
|
50
|
+
(err as { code: string }).code === "ENOENT"
|
|
51
|
+
) {
|
|
52
|
+
return { manifest: { artifacts: [] }, raw: {} };
|
|
53
|
+
}
|
|
54
|
+
throw err;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const manifest = parseManifest(content);
|
|
58
|
+
const raw = (yaml.load(content) as Record<string, unknown>) ?? {};
|
|
59
|
+
|
|
60
|
+
return { manifest, raw };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Reads and parses the sync-lock file, returns null if file doesn't exist */
|
|
64
|
+
export async function readSyncLock(
|
|
65
|
+
syncLockPath: string,
|
|
66
|
+
): Promise<SyncLock | null> {
|
|
67
|
+
let content: string;
|
|
68
|
+
try {
|
|
69
|
+
content = await readFile(syncLockPath, "utf-8");
|
|
70
|
+
} catch (err: unknown) {
|
|
71
|
+
if (
|
|
72
|
+
err &&
|
|
73
|
+
typeof err === "object" &&
|
|
74
|
+
"code" in err &&
|
|
75
|
+
(err as { code: string }).code === "ENOENT"
|
|
76
|
+
) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
throw err;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return JSON.parse(content) as SyncLock;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Computes sync status by comparing manifest entries against sync-lock */
|
|
86
|
+
export function computeSyncStatus(
|
|
87
|
+
manifest: Manifest,
|
|
88
|
+
syncLock: SyncLock | null,
|
|
89
|
+
): SyncStatusResponse {
|
|
90
|
+
const entries: EntryStatus[] = manifest.artifacts.map((entry) => {
|
|
91
|
+
const identifier =
|
|
92
|
+
"name" in entry
|
|
93
|
+
? entry.name
|
|
94
|
+
: (entry as { collection: string }).collection;
|
|
95
|
+
const type: "artifact" | "collection" =
|
|
96
|
+
"name" in entry ? "artifact" : "collection";
|
|
97
|
+
|
|
98
|
+
const lockEntry =
|
|
99
|
+
syncLock?.entries.find((e) => e.name === identifier) ?? null;
|
|
100
|
+
|
|
101
|
+
let status: "synced" | "outdated" | "missing";
|
|
102
|
+
if (!lockEntry) {
|
|
103
|
+
status = "missing";
|
|
104
|
+
} else if (lockEntry.version === entry.version) {
|
|
105
|
+
status = "synced";
|
|
106
|
+
} else {
|
|
107
|
+
status = "outdated";
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
identifier,
|
|
112
|
+
type,
|
|
113
|
+
version: entry.version,
|
|
114
|
+
mode: entry.mode,
|
|
115
|
+
syncedVersion: lockEntry?.version ?? null,
|
|
116
|
+
status,
|
|
117
|
+
};
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
entries,
|
|
122
|
+
syncedAt: syncLock?.syncedAt ?? null,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Error type for manifest admin operations */
|
|
127
|
+
export class ManifestAdminError extends Error {
|
|
128
|
+
constructor(
|
|
129
|
+
message: string,
|
|
130
|
+
public readonly type: "conflict" | "not-found" | "validation",
|
|
131
|
+
) {
|
|
132
|
+
super(message);
|
|
133
|
+
this.name = "ManifestAdminError";
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Validates a ManifestEntryInput against the appropriate schema */
|
|
138
|
+
export function validateManifestEntry(
|
|
139
|
+
input: ManifestEntryInput,
|
|
140
|
+
):
|
|
141
|
+
| { success: true; data: ManifestEntry }
|
|
142
|
+
| { success: false; errors: Array<{ field: string; message: string }> } {
|
|
143
|
+
const hasName = input.name != null && input.name !== "";
|
|
144
|
+
const hasCollection = input.collection != null && input.collection !== "";
|
|
145
|
+
|
|
146
|
+
if (hasName && hasCollection) {
|
|
147
|
+
return {
|
|
148
|
+
success: false,
|
|
149
|
+
errors: [
|
|
150
|
+
{
|
|
151
|
+
field: "name",
|
|
152
|
+
message:
|
|
153
|
+
"Cannot set both 'name' and 'collection' — each entry must have exactly one",
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (!hasName && !hasCollection) {
|
|
160
|
+
return {
|
|
161
|
+
success: false,
|
|
162
|
+
errors: [
|
|
163
|
+
{
|
|
164
|
+
field: "name",
|
|
165
|
+
message:
|
|
166
|
+
"Must set either 'name' (for artifact ref) or 'collection' (for collection ref)",
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const schema = hasName
|
|
173
|
+
? ArtifactManifestEntrySchema
|
|
174
|
+
: CollectionManifestEntrySchema;
|
|
175
|
+
const result = schema.safeParse(input);
|
|
176
|
+
|
|
177
|
+
if (!result.success) {
|
|
178
|
+
const errors = result.error.issues.map((issue) => ({
|
|
179
|
+
field: issue.path.join(".") || "unknown",
|
|
180
|
+
message: issue.message,
|
|
181
|
+
}));
|
|
182
|
+
return { success: false, errors };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return { success: true, data: result.data as ManifestEntry };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Helper to get the identifier from a manifest entry */
|
|
189
|
+
function getEntryIdentifier(entry: ManifestEntry): string {
|
|
190
|
+
return "name" in entry
|
|
191
|
+
? entry.name
|
|
192
|
+
: (entry as { collection: string }).collection;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Writes the manifest back to disk, preserving unknown top-level keys from the raw object */
|
|
196
|
+
async function writeManifestToDisk(
|
|
197
|
+
manifestPath: string,
|
|
198
|
+
manifest: Manifest,
|
|
199
|
+
raw: Record<string, unknown>,
|
|
200
|
+
): Promise<void> {
|
|
201
|
+
// Merge validated manifest back into raw to preserve unknown top-level keys
|
|
202
|
+
const merged: Record<string, unknown> = { ...raw };
|
|
203
|
+
if (manifest.backend != null) {
|
|
204
|
+
merged.backend = manifest.backend;
|
|
205
|
+
} else {
|
|
206
|
+
// Only delete if the manifest explicitly doesn't have it
|
|
207
|
+
// but keep it if raw had it (passthrough)
|
|
208
|
+
if (!("backend" in manifest)) {
|
|
209
|
+
delete merged.backend;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
merged.artifacts = manifest.artifacts;
|
|
213
|
+
|
|
214
|
+
const yamlContent = printManifest(merged as Manifest);
|
|
215
|
+
await writeFile(manifestPath, yamlContent, "utf-8");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Adds a new entry to the manifest, writes to disk */
|
|
219
|
+
export async function addManifestEntry(
|
|
220
|
+
manifestPath: string,
|
|
221
|
+
input: ManifestEntryInput,
|
|
222
|
+
): Promise<Manifest> {
|
|
223
|
+
const validation = validateManifestEntry(input);
|
|
224
|
+
if (!validation.success) {
|
|
225
|
+
throw new ManifestAdminError(
|
|
226
|
+
`Validation failed: ${validation.errors.map((e) => e.message).join("; ")}`,
|
|
227
|
+
"validation",
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const { manifest, raw } = await readManifest(manifestPath);
|
|
232
|
+
const newEntry = validation.data;
|
|
233
|
+
const newIdentifier = getEntryIdentifier(newEntry);
|
|
234
|
+
|
|
235
|
+
// Check for duplicate
|
|
236
|
+
const duplicate = manifest.artifacts.find(
|
|
237
|
+
(entry) => getEntryIdentifier(entry) === newIdentifier,
|
|
238
|
+
);
|
|
239
|
+
if (duplicate) {
|
|
240
|
+
throw new ManifestAdminError(
|
|
241
|
+
`Manifest entry '${newIdentifier}' already exists`,
|
|
242
|
+
"conflict",
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
manifest.artifacts.push(newEntry);
|
|
247
|
+
await writeManifestToDisk(manifestPath, manifest, raw);
|
|
248
|
+
|
|
249
|
+
return manifest;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** Updates an existing entry in the manifest, writes to disk */
|
|
253
|
+
export async function editManifestEntry(
|
|
254
|
+
manifestPath: string,
|
|
255
|
+
identifier: string,
|
|
256
|
+
input: Partial<ManifestEntryInput>,
|
|
257
|
+
): Promise<Manifest> {
|
|
258
|
+
const { manifest, raw } = await readManifest(manifestPath);
|
|
259
|
+
|
|
260
|
+
const entryIndex = manifest.artifacts.findIndex(
|
|
261
|
+
(entry) => getEntryIdentifier(entry) === identifier,
|
|
262
|
+
);
|
|
263
|
+
if (entryIndex === -1) {
|
|
264
|
+
throw new ManifestAdminError(
|
|
265
|
+
`Manifest entry '${identifier}' not found`,
|
|
266
|
+
"not-found",
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const existing = manifest.artifacts[entryIndex];
|
|
271
|
+
|
|
272
|
+
// Update mutable fields from the partial input
|
|
273
|
+
const updated = { ...existing } as Record<string, unknown>;
|
|
274
|
+
if (input.version !== undefined) updated.version = input.version;
|
|
275
|
+
if (input.mode !== undefined) updated.mode = input.mode;
|
|
276
|
+
if (input.harnesses !== undefined) updated.harnesses = input.harnesses;
|
|
277
|
+
if (input.backend !== undefined) updated.backend = input.backend;
|
|
278
|
+
|
|
279
|
+
manifest.artifacts[entryIndex] = updated as ManifestEntry;
|
|
280
|
+
await writeManifestToDisk(manifestPath, manifest, raw);
|
|
281
|
+
|
|
282
|
+
return manifest;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Removes an entry from the manifest, writes to disk */
|
|
286
|
+
export async function removeManifestEntry(
|
|
287
|
+
manifestPath: string,
|
|
288
|
+
identifier: string,
|
|
289
|
+
): Promise<Manifest> {
|
|
290
|
+
const { manifest, raw } = await readManifest(manifestPath);
|
|
291
|
+
|
|
292
|
+
const entryIndex = manifest.artifacts.findIndex(
|
|
293
|
+
(entry) => getEntryIdentifier(entry) === identifier,
|
|
294
|
+
);
|
|
295
|
+
if (entryIndex === -1) {
|
|
296
|
+
throw new ManifestAdminError(
|
|
297
|
+
`Manifest entry '${identifier}' not found`,
|
|
298
|
+
"not-found",
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
manifest.artifacts.splice(entryIndex, 1);
|
|
303
|
+
await writeManifestToDisk(manifestPath, manifest, raw);
|
|
304
|
+
|
|
305
|
+
return manifest;
|
|
306
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* context-bazaar MCP bridge
|
|
5
|
+
*
|
|
6
|
+
* Exposes the kanon catalog and artifacts as MCP tools so Claude Code
|
|
7
|
+
* users can browse and install knowledge artifacts directly from the assistant.
|
|
8
|
+
*
|
|
9
|
+
* Tools:
|
|
10
|
+
* catalog_list — list all artifacts (optionally filtered by collection)
|
|
11
|
+
* artifact_content — read a specific artifact's knowledge.md
|
|
12
|
+
* collection_list — list collections with member counts
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { access, readFile } from "node:fs/promises";
|
|
16
|
+
import { dirname, join, resolve } from "node:path";
|
|
17
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
18
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
19
|
+
import {
|
|
20
|
+
CallToolRequestSchema,
|
|
21
|
+
ListToolsRequestSchema,
|
|
22
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
23
|
+
|
|
24
|
+
// Resolve the Kanon data root — works when installed as a Claude or Codex
|
|
25
|
+
// plugin, and also when run from the checked-out source tree.
|
|
26
|
+
const ENTRYPOINT_PATH = process.argv[1];
|
|
27
|
+
const RUNTIME_DIR = ENTRYPOINT_PATH
|
|
28
|
+
? dirname(resolve(ENTRYPOINT_PATH))
|
|
29
|
+
: process.cwd();
|
|
30
|
+
const ENV_PLUGIN_ROOT =
|
|
31
|
+
process.env.CODEX_PLUGIN_ROOT ?? process.env.CLAUDE_PLUGIN_ROOT;
|
|
32
|
+
const PLUGIN_ROOT_CANDIDATES = [
|
|
33
|
+
...(ENV_PLUGIN_ROOT ? [join(ENV_PLUGIN_ROOT, "kanon"), ENV_PLUGIN_ROOT] : []),
|
|
34
|
+
resolve(RUNTIME_DIR, ".."),
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
async function resolvePluginRoot() {
|
|
38
|
+
for (const root of PLUGIN_ROOT_CANDIDATES) {
|
|
39
|
+
try {
|
|
40
|
+
await access(join(root, "catalog.json"));
|
|
41
|
+
return root;
|
|
42
|
+
} catch {
|
|
43
|
+
// Try the next root shape.
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return PLUGIN_ROOT_CANDIDATES[PLUGIN_ROOT_CANDIDATES.length - 1];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function loadCatalog() {
|
|
50
|
+
const pluginRoot = await resolvePluginRoot();
|
|
51
|
+
const catalogPath = join(pluginRoot, "catalog.json");
|
|
52
|
+
const raw = await readFile(catalogPath, "utf-8");
|
|
53
|
+
return {
|
|
54
|
+
entries: JSON.parse(raw) as Array<Record<string, unknown>>,
|
|
55
|
+
pluginRoot,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const server = new Server(
|
|
60
|
+
{ name: "context-bazaar", version: "0.2.0" },
|
|
61
|
+
{ capabilities: { tools: {} } },
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
65
|
+
tools: [
|
|
66
|
+
{
|
|
67
|
+
name: "catalog_list",
|
|
68
|
+
description:
|
|
69
|
+
"List knowledge artifacts in the context-bazaar catalog. " +
|
|
70
|
+
"Optionally filter by collection name (e.g. 'neon-caravan', 'byron-powers') " +
|
|
71
|
+
"or by type (skill, power, workflow, prompt, agent, template, reference-pack).",
|
|
72
|
+
inputSchema: {
|
|
73
|
+
type: "object" as const,
|
|
74
|
+
properties: {
|
|
75
|
+
collection: {
|
|
76
|
+
type: "string",
|
|
77
|
+
description: "Filter to a specific collection name",
|
|
78
|
+
},
|
|
79
|
+
type: {
|
|
80
|
+
type: "string",
|
|
81
|
+
description: "Filter by artifact type",
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: "artifact_content",
|
|
88
|
+
description:
|
|
89
|
+
"Return the full knowledge.md content of a specific artifact. " +
|
|
90
|
+
"Use catalog_list first to find the artifact name.",
|
|
91
|
+
inputSchema: {
|
|
92
|
+
type: "object" as const,
|
|
93
|
+
required: ["name"],
|
|
94
|
+
properties: {
|
|
95
|
+
name: {
|
|
96
|
+
type: "string",
|
|
97
|
+
description: "Artifact name (kebab-case, e.g. 'commit-craft')",
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: "collection_list",
|
|
104
|
+
description:
|
|
105
|
+
"List all available collections with their member artifact names.",
|
|
106
|
+
inputSchema: {
|
|
107
|
+
type: "object" as const,
|
|
108
|
+
properties: {},
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
}));
|
|
113
|
+
|
|
114
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
115
|
+
const { entries: catalog, pluginRoot } = await loadCatalog();
|
|
116
|
+
const args = (request.params.arguments ?? {}) as Record<string, string>;
|
|
117
|
+
|
|
118
|
+
switch (request.params.name) {
|
|
119
|
+
case "catalog_list": {
|
|
120
|
+
let entries = catalog;
|
|
121
|
+
if (args.collection) {
|
|
122
|
+
entries = entries.filter(
|
|
123
|
+
(e) =>
|
|
124
|
+
Array.isArray(e.collections) &&
|
|
125
|
+
e.collections.includes(args.collection),
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
if (args.type) {
|
|
129
|
+
entries = entries.filter((e) => e.type === args.type);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const summary = entries.map((e) => ({
|
|
133
|
+
name: e.name,
|
|
134
|
+
displayName: e.displayName,
|
|
135
|
+
type: e.type,
|
|
136
|
+
description: e.description,
|
|
137
|
+
maturity: e.maturity,
|
|
138
|
+
collections: e.collections,
|
|
139
|
+
harnesses: e.harnesses,
|
|
140
|
+
}));
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
content: [
|
|
144
|
+
{
|
|
145
|
+
type: "text" as const,
|
|
146
|
+
text: `Found ${summary.length} artifact(s):\n\n${JSON.stringify(summary, null, 2)}`,
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
case "artifact_content": {
|
|
153
|
+
const entry = catalog.find((e) => e.name === args.name);
|
|
154
|
+
if (!entry) {
|
|
155
|
+
return {
|
|
156
|
+
content: [
|
|
157
|
+
{
|
|
158
|
+
type: "text" as const,
|
|
159
|
+
text: `Artifact '${args.name}' not found in catalog.`,
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
isError: true,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const contentPath = join(pluginRoot, String(entry.path), "knowledge.md");
|
|
167
|
+
let content: string;
|
|
168
|
+
try {
|
|
169
|
+
content = await readFile(contentPath, "utf-8");
|
|
170
|
+
} catch {
|
|
171
|
+
return {
|
|
172
|
+
content: [
|
|
173
|
+
{
|
|
174
|
+
type: "text" as const,
|
|
175
|
+
text: `Artifact found in catalog but source file not available at ${contentPath}.`,
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
isError: true,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
content: [
|
|
184
|
+
{
|
|
185
|
+
type: "text" as const,
|
|
186
|
+
text: `# ${entry.displayName}\n**Type:** ${entry.type} **Maturity:** ${entry.maturity}\n\n${content}`,
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
case "collection_list": {
|
|
193
|
+
const collectionMap = new Map<string, string[]>();
|
|
194
|
+
for (const entry of catalog) {
|
|
195
|
+
for (const col of (Array.isArray(entry.collections)
|
|
196
|
+
? entry.collections
|
|
197
|
+
: []) as string[]) {
|
|
198
|
+
if (!collectionMap.has(col)) collectionMap.set(col, []);
|
|
199
|
+
collectionMap.get(col)?.push(String(entry.name));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const result = [...collectionMap.entries()].map(([name, members]) => ({
|
|
204
|
+
name,
|
|
205
|
+
memberCount: members.length,
|
|
206
|
+
members,
|
|
207
|
+
}));
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
content: [
|
|
211
|
+
{
|
|
212
|
+
type: "text" as const,
|
|
213
|
+
text:
|
|
214
|
+
result.length === 0
|
|
215
|
+
? "No collections found."
|
|
216
|
+
: JSON.stringify(result, null, 2),
|
|
217
|
+
},
|
|
218
|
+
],
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
default:
|
|
223
|
+
return {
|
|
224
|
+
content: [
|
|
225
|
+
{
|
|
226
|
+
type: "text" as const,
|
|
227
|
+
text: `Unknown tool: ${request.params.name}`,
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
isError: true,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
async function main() {
|
|
236
|
+
const transport = new StdioServerTransport();
|
|
237
|
+
await server.connect(transport);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mutation testing — delta strategy (Req 5.6).
|
|
3
|
+
*
|
|
4
|
+
* `selectDeltaTargets` is a pure intersection function: given the set of adapter
|
|
5
|
+
* files eligible for mutation and the set of files changed since the last
|
|
6
|
+
* recorded run (from `git diff`), return only the adapter files that appear in
|
|
7
|
+
* both sets.
|
|
8
|
+
*
|
|
9
|
+
* Path normalization is applied so that trailing slashes, redundant separators,
|
|
10
|
+
* and leading `./` prefixes do not prevent a match.
|
|
11
|
+
*
|
|
12
|
+
* The `git diff` invocation and reading the last SHA from history are I/O
|
|
13
|
+
* concerns handled by the runner; this module is pure.
|
|
14
|
+
*
|
|
15
|
+
* See ADR-0042 for the architectural rationale.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { normalize } from "node:path";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Normalize a file path for comparison: strip leading `./`, collapse redundant
|
|
22
|
+
* separators, and remove trailing slashes.
|
|
23
|
+
*/
|
|
24
|
+
function normalizePath(p: string): string {
|
|
25
|
+
// Use node:path normalize to collapse separators and resolve `.` segments.
|
|
26
|
+
let normalized = normalize(p);
|
|
27
|
+
// Strip trailing slash (normalize keeps it for root `/`).
|
|
28
|
+
if (normalized.length > 1 && normalized.endsWith("/")) {
|
|
29
|
+
normalized = normalized.slice(0, -1);
|
|
30
|
+
}
|
|
31
|
+
return normalized;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Given the set of adapter files and the set of changed files (from git diff
|
|
36
|
+
* since the last run's SHA), return the subset of adapter files that also appear
|
|
37
|
+
* in the changed files set — i.e., the files to mutate for a delta run.
|
|
38
|
+
*
|
|
39
|
+
* Pure. Preserves the original path strings from `adapterFiles` in the output.
|
|
40
|
+
* If either input is empty, returns `[]`.
|
|
41
|
+
*/
|
|
42
|
+
export function selectDeltaTargets(
|
|
43
|
+
adapterFiles: string[],
|
|
44
|
+
changedFiles: string[],
|
|
45
|
+
): string[] {
|
|
46
|
+
if (adapterFiles.length === 0 || changedFiles.length === 0) return [];
|
|
47
|
+
|
|
48
|
+
const changedSet = new Set(changedFiles.map(normalizePath));
|
|
49
|
+
return adapterFiles.filter((f) => changedSet.has(normalizePath(f)));
|
|
50
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mutation testing — history persistence (Req 5.8).
|
|
3
|
+
*
|
|
4
|
+
* Pure core: `serializeRecord` and `parseHistory` are deterministic, I/O-free
|
|
5
|
+
* functions that convert between `MutationRunRecord` objects and JSONL text.
|
|
6
|
+
*
|
|
7
|
+
* Thin I/O shell: `appendRecord` writes one JSONL line to the history file,
|
|
8
|
+
* creating parent directories if needed.
|
|
9
|
+
*
|
|
10
|
+
* See ADR-0042 for the architectural rationale.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { appendFile, mkdir } from "node:fs/promises";
|
|
14
|
+
import { dirname } from "node:path";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A single mutation testing run result, persisted as one JSONL line.
|
|
18
|
+
*
|
|
19
|
+
* Fields (per Req 5.8):
|
|
20
|
+
* - ts: ISO 8601 timestamp of the run
|
|
21
|
+
* - sha: short git commit hash at time of run
|
|
22
|
+
* - killRate: decimal kill rate (killed / total)
|
|
23
|
+
* - totalMutants: total mutants generated
|
|
24
|
+
* - killed: mutants detected by the test suite
|
|
25
|
+
* - survived: mutants not detected
|
|
26
|
+
* - operators: array of operator names used in the run
|
|
27
|
+
*/
|
|
28
|
+
export interface MutationRunRecord {
|
|
29
|
+
ts: string;
|
|
30
|
+
sha: string;
|
|
31
|
+
killRate: number;
|
|
32
|
+
totalMutants: number;
|
|
33
|
+
killed: number;
|
|
34
|
+
survived: number;
|
|
35
|
+
operators: string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Serialize one record to a single JSONL line (trailing newline included). Pure.
|
|
40
|
+
*/
|
|
41
|
+
export function serializeRecord(r: MutationRunRecord): string {
|
|
42
|
+
return `${JSON.stringify(r)}\n`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Parse JSONL content into an array of records. Pure.
|
|
47
|
+
* Empty lines are silently skipped.
|
|
48
|
+
*/
|
|
49
|
+
export function parseHistory(content: string): MutationRunRecord[] {
|
|
50
|
+
return content
|
|
51
|
+
.split("\n")
|
|
52
|
+
.filter((line) => line.trim().length > 0)
|
|
53
|
+
.map((line) => JSON.parse(line) as MutationRunRecord);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Append a single record to the mutation history file (I/O).
|
|
58
|
+
* Creates parent directories if they do not already exist.
|
|
59
|
+
*/
|
|
60
|
+
export async function appendRecord(
|
|
61
|
+
filePath: string,
|
|
62
|
+
record: MutationRunRecord,
|
|
63
|
+
): Promise<void> {
|
|
64
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
65
|
+
await appendFile(filePath, serializeRecord(record), "utf-8");
|
|
66
|
+
}
|