Conversation
…moke Addresses three gaps found in live validation of canary PR #106: 1. Response envelope was missing server_time, assistant_text, debug. Every other oddkit tool returns {action, result, server_time, assistant_text, debug}; telemetry_policy returned only {action, result}. This breaks the time-discipline contract — project instructions require every oddkit response to carry server_time so models have a clock reading on every call. 2. canon_url parameter was silently ignored. The Zod schema was {}, so MCP stripped canon_url before the handler saw it, and the handler hardcoded the default baseline. The three-tier resolution contract in canon/constraints/core-governance-baseline assumes every canon-driven tool accepts canon_url for overrides — this is load-bearing for TruthKit / custom-canon consumers. 3. No live-smoke test for the envelope shape. Parser tests in governance-parser.test.mjs exercised parser logic only. The canary shipped with partial contract conformance because no test invoked the MCP tool end-to-end and asserted the envelope shape. Changes: - Add canon_url to the tool's Zod schema; thread through to fetcher.getFile(path, canon_url). - Expand response envelope to match convention: server_time, assistant_text (human-readable summary naming the tier), debug with duration_ms and canon_url echo. - New workers/test/canon-tool-envelope.smoke.mjs — live smoke script that curls the MCP endpoint and verifies envelope shape for oddkit_time (convention baseline), telemetry_policy default (canon tier), and telemetry_policy with canon_url override (minimal fallback). Verified: - npm run typecheck: clean - Smoke script structure matches PR #100's governance-parser test style and exits non-zero on any envelope violation. Lesson for the sweep: every canon-driven refactor must verify both the new governance_source signal AND full envelope conformance. The canary's partial completion was caught by live validation but should have been caught by pre-merge smoke. Follow-up to update the refactor template in docs/oddkit/audit/... separately.
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
oddkit | c8f53ae | Commit Preview URL Branch Preview URL |
Apr 19 2026, 01:51 AM |
klappy
added a commit
that referenced
this pull request
Apr 19, 2026
…e smoke
Lessons from canary validation: internal parser tests aren't enough to
catch tool-contract regressions. The telemetry_policy canary shipped
with a broken response envelope (missing server_time/assistant_text/
debug) and a Zod schema that silently stripped the canon_url override
parameter. Parser tests passed; the contract was broken.
New constraints added to the audit doc:
- Response envelope is load-bearing: every canon-driven tool must
return {action, result, server_time, assistant_text, debug}.
- canon_url parameter is required in the Zod schema for every
canon-driven tool; hardcoding the baseline URL defeats the
override contract that core-governance-baseline depends on.
- Live-smoke is mandatory pre-merge, not post-merge validation.
Template: workers/test/canon-tool-envelope.smoke.mjs.
New 7-point refactor template (definition of done) section. Each
subsequent tool refactor in this sweep must hit all 7 before the
audit row gets stamped.
Follow-up to canary: #108 closes these gaps on
telemetry_policy. The smoke script added there becomes the template
for every subsequent refactor in the sweep.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: canon_url override silently falls back to baseline repo
- Added a skipBaselineFallback option to ZipBaselineFetcher.getFile and pass it from the telemetry_policy handler when canon_url is provided, so a missing file in the override canon returns null and governance_source correctly resolves to 'minimal' instead of being served by the baseline.
Preview (620e6a9976)
diff --git a/workers/src/index.ts b/workers/src/index.ts
--- a/workers/src/index.ts
+++ b/workers/src/index.ts
@@ -496,15 +496,17 @@
server.tool(
"telemetry_policy",
- "Return oddkit telemetry and sharing policy guidance. What is tracked, what is excluded, and why. Fetched from canonical governance document at runtime. Response envelope declares governance_source (canon|baseline|minimal) per canon/constraints/core-governance-baseline.",
- {},
+ "Return oddkit telemetry and sharing policy guidance. What is tracked, what is excluded, and why. Fetched from canonical governance document at runtime. Response envelope declares governance_source (canon|baseline|minimal) per canon/constraints/core-governance-baseline. Accepts canon_url to read from an alternate canon repo.",
{
+ canon_url: z.string().optional().describe("Optional GitHub repo URL for canon override. When provided, fetches canon/constraints/telemetry-governance.md from this repo instead of the oddkit-hosted default. Falls back to the minimal baseline if the file is missing."),
+ },
+ {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true,
},
- async () => {
+ async ({ canon_url }) => {
// Governance resolution per canon/constraints/core-governance-baseline:
// 1. Live canon fetch (preferred) → governance_source: "canon"
// 2. Minimal baseline (shipped in code) → governance_source: "minimal"
@@ -512,13 +514,21 @@
// This canary refactor implements tiers 1 and 3 only. The bundled
// baseline tier (2) and the build-time schema check arrive in follow-up
// work; the manifest + baseline directory are not yet in place.
+ const startTime = Date.now();
const fetcher = new ZipBaselineFetcher(env);
let policyContent: string | null = null;
let selfReportHeaders: Record<string, string> | null = null;
let governanceSource: "canon" | "baseline" | "minimal" = "minimal";
try {
- const content = await fetcher.getFile("canon/constraints/telemetry-governance.md");
+ // When a canon_url override is provided, suppress the baseline fallback
+ // so a missing file in the override canon surfaces as "minimal" rather
+ // than silently serving the klappy.dev baseline.
+ const content = await fetcher.getFile(
+ "canon/constraints/telemetry-governance.md",
+ canon_url,
+ canon_url ? { skipBaselineFallback: true } : undefined,
+ );
if (content) {
policyContent = content;
const parsed = parseSelfReportHeadersTable(content);
@@ -551,6 +561,9 @@
}
}
+ const headerCount = selfReportHeaders ? Object.keys(selfReportHeaders).length : 0;
+ const assistantText = `Telemetry policy loaded from ${governanceSource}. ${headerCount} self-report headers available.${canon_url ? ` (canon_url override: ${canon_url})` : ""}`;
+
return {
content: [{
type: "text" as const,
@@ -563,6 +576,9 @@
self_report_headers: selfReportHeaders,
generated_at: new Date().toISOString(),
},
+ server_time: new Date().toISOString(),
+ assistant_text: assistantText,
+ debug: { duration_ms: Date.now() - startTime, canon_url: canon_url ?? null },
}, null, 2),
}],
};
diff --git a/workers/src/zip-baseline-fetcher.ts b/workers/src/zip-baseline-fetcher.ts
--- a/workers/src/zip-baseline-fetcher.ts
+++ b/workers/src/zip-baseline-fetcher.ts
@@ -978,12 +978,25 @@
* Get a specific file from the baseline or canon.
* Content-addressed: file cache is keyed to each repo's own commit SHA.
* Three-tier: module memory → R2 → ZIP extraction.
+ *
+ * When `options.skipBaselineFallback` is true, the baseline repo is not
+ * appended to the search sources. Callers that need to distinguish between
+ * "file found in the canon_url override" and "file found in the baseline
+ * fallback" can pass this flag so a null return unambiguously means the
+ * override canon lacks the file.
*/
- async getFile(path: string, canonUrl?: string): Promise<string | null> {
+ async getFile(
+ path: string,
+ canonUrl?: string,
+ options?: { skipBaselineFallback?: boolean },
+ ): Promise<string | null> {
const baselineRepoUrl = "https://github.com/klappy/klappy.dev";
+ const skipBaselineFallback = options?.skipBaselineFallback === true;
- // Resolve SHA for each repo independently
- const baselineSha = await this.getLatestCommitSha(baselineRepoUrl);
+ // Resolve SHA for the baseline only when it will actually be searched.
+ const baselineSha = skipBaselineFallback && canonUrl
+ ? null
+ : await this.getLatestCommitSha(baselineRepoUrl);
// Build the list of repos to search, each with its own SHA
const sources: Array<{ url: string; repoKey: string; sha: string }> = [];
@@ -998,13 +1011,15 @@
});
}
- sources.push({
- url: this.env.BASELINE_URL.includes("raw.githubusercontent.com")
- ? this.env.BASELINE_URL.replace("/main", "").replace("raw.githubusercontent.com", "github.com")
- : baselineRepoUrl,
- repoKey: getCacheKey("baseline"),
- sha: baselineSha || "unknown",
- });
+ if (!(skipBaselineFallback && canonUrl)) {
+ sources.push({
+ url: this.env.BASELINE_URL.includes("raw.githubusercontent.com")
+ ? this.env.BASELINE_URL.replace("/main", "").replace("raw.githubusercontent.com", "github.com")
+ : baselineRepoUrl,
+ repoKey: getCacheKey("baseline"),
+ sha: baselineSha || "unknown",
+ });
+ }
for (const source of sources) {
// Content-addressed cache key: repo identity + repo SHA + file path
diff --git a/workers/test/canon-tool-envelope.smoke.mjs b/workers/test/canon-tool-envelope.smoke.mjs
new file mode 100644
--- /dev/null
+++ b/workers/test/canon-tool-envelope.smoke.mjs
@@ -1,0 +1,134 @@
+#!/usr/bin/env node
+/**
+ * Live smoke test for canon-driven MCP tool envelope contracts.
+ *
+ * Exercises the actual MCP endpoint (preview or prod) and verifies that
+ * every canon-driven tool returns the full envelope shape:
+ *
+ * { action, result, server_time, assistant_text, debug, ... }
+ *
+ * AND that canon-driven tools surface `governance_source` inside `result`.
+ *
+ * Why this exists: parser tests (workers/test/governance-parser.test.mjs)
+ * exercise parser logic in isolation. They passed for the telemetry_policy
+ * canary, but the canary shipped with a broken envelope (missing server_time,
+ * assistant_text, debug) and silently ignored the canon_url parameter because
+ * the Zod schema was {}. Parser tests cannot catch the tool's response
+ * contract — only live smoke against the MCP endpoint can.
+ *
+ * Usage:
+ * node workers/test/canon-tool-envelope.smoke.mjs
+ * ODDKIT_URL=https://preview-xxx.oddkit.klappy.dev/mcp node ...
+ *
+ * Exit 0 on all pass, 1 on any failure.
+ */
+
+const ODDKIT_URL = process.env.ODDKIT_URL || "https://oddkit.klappy.dev/mcp";
+
+let passed = 0;
+let failed = 0;
+
+function ok(label, cond, hint = "") {
+ if (cond) {
+ console.log(` ✓ ${label}`);
+ passed++;
+ } else {
+ console.log(` ✗ ${label}${hint ? ` — ${hint}` : ""}`);
+ failed++;
+ }
+}
+
+async function callTool(name, args = {}) {
+ const body = JSON.stringify({
+ jsonrpc: "2.0",
+ id: 1,
+ method: "tools/call",
+ params: { name, arguments: args },
+ });
+ const res = await fetch(ODDKIT_URL, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "Accept": "application/json, text/event-stream",
+ "x-oddkit-client": "envelope-smoke-test",
+ },
+ body,
+ });
+ const text = await res.text();
+ // SSE format: `event: message\ndata: {...}\n\n`
+ const match = text.match(/data: (\{[\s\S]*\})/);
+ if (!match) throw new Error(`No data payload from ${name}: ${text.slice(0, 300)}`);
+ const envelope = JSON.parse(match[1]);
+ const inner = JSON.parse(envelope.result.content[0].text);
+ return inner;
+}
+
+function expectFullEnvelope(toolName, inner) {
+ console.log(`\n─── Envelope shape: ${toolName} ───`);
+ ok(`${toolName}: has 'action'`, typeof inner.action === "string");
+ ok(`${toolName}: has 'result'`, typeof inner.result === "object" && inner.result !== null);
+ ok(`${toolName}: has 'server_time' (ISO 8601)`,
+ typeof inner.server_time === "string" && /^\d{4}-\d{2}-\d{2}T/.test(inner.server_time),
+ `got: ${inner.server_time}`);
+ ok(`${toolName}: has 'assistant_text'`, typeof inner.assistant_text === "string" && inner.assistant_text.length > 0);
+ ok(`${toolName}: has 'debug'`, typeof inner.debug === "object" && inner.debug !== null);
+ ok(`${toolName}: debug.duration_ms is a number`, typeof inner.debug?.duration_ms === "number");
+}
+
+function expectGovernanceSource(toolName, inner, expectedTier) {
+ console.log(`\n─── Governance source: ${toolName} ───`);
+ const source = inner.result?.governance_source;
+ ok(`${toolName}: result.governance_source present`, typeof source === "string", `got: ${source}`);
+ ok(`${toolName}: result.governance_source is one of canon|baseline|minimal`,
+ ["canon", "baseline", "minimal"].includes(source),
+ `got: ${source}`);
+ if (expectedTier) {
+ ok(`${toolName}: result.governance_source == "${expectedTier}"`,
+ source === expectedTier,
+ `got: ${source}`);
+ }
+}
+
+async function run() {
+ console.log(`Target: ${ODDKIT_URL}\n`);
+
+ // Tool 1: oddkit_time — non-canon-driven baseline for envelope convention
+ const timeResult = await callTool("oddkit_time");
+ expectFullEnvelope("oddkit_time", timeResult);
+
+ // Tool 2: telemetry_policy — canon-driven, should have full envelope + governance_source
+ const policyDefault = await callTool("telemetry_policy");
+ expectFullEnvelope("telemetry_policy (default canon)", policyDefault);
+ expectGovernanceSource("telemetry_policy (default canon)", policyDefault, "canon");
+
+ // Tool 3: telemetry_policy with canon_url override pointing at a repo that
+ // doesn't have the governance file — should fall back to minimal
+ console.log(`\n─── canon_url override: telemetry_policy ───`);
+ const policyOverride = await callTool("telemetry_policy", {
+ canon_url: "https://github.com/torvalds/linux",
+ });
+ expectFullEnvelope("telemetry_policy (canon_url override)", policyOverride);
+ ok(
+ "telemetry_policy: canon_url override falls back to minimal when file missing",
+ policyOverride.result?.governance_source === "minimal",
+ `got: ${policyOverride.result?.governance_source}`,
+ );
+ ok(
+ "telemetry_policy: minimal fallback still returns 8 headers",
+ Object.keys(policyOverride.result?.self_report_headers ?? {}).length === 8,
+ `got: ${Object.keys(policyOverride.result?.self_report_headers ?? {}).length}`,
+ );
+ ok(
+ "telemetry_policy: debug.canon_url echoes the override",
+ policyOverride.debug?.canon_url === "https://github.com/torvalds/linux",
+ `got: ${policyOverride.debug?.canon_url}`,
+ );
+
+ console.log(`\n${passed} passed, ${failed} failed`);
+ process.exit(failed === 0 ? 0 : 1);
+}
+
+run().catch((e) => {
+ console.error(e);
+ process.exit(1);
+});You can send follow-ups to the cloud agent here.
klappy
added a commit
to klappy/klappy.dev
that referenced
this pull request
Apr 19, 2026
Live validation of telemetry_policy canary (klappy/oddkit#106) against prod surfaced three gaps the original contract didn't name explicitly enough: 1. Response envelope shape is part of the contract. A tool that returns {action, result} but omits server_time/assistant_text/debug breaks the time-discipline system even if governance_source is present. Added as Runtime Invariant #3. 2. canon_url parameter must be in the Zod schema, not just documented as a concept. MCP silently strips unknown parameters. The canary shipped with schema={} and canon_url was unreachable. Added as Runtime Invariant #4. 3. Live-smoke against the MCP endpoint is a ship-blocker, not a nice-to-have. Internal parser tests passed while the tool shipped with broken envelope and silent param stripping. Added as Runtime Invariant #7, template referenced. Refactor Implications section expanded to a 7-point checklist and acknowledges the canary's partial completion + follow-up PR as the first documented test of the contract. Follow-up PR that closes the canary gaps: klappy/oddkit#108.
When canon_url is provided, getFile() previously still appended the klappy.dev baseline as a search source. A missing governance file in the override canon would be silently satisfied by the baseline, causing governance_source to report 'canon' instead of 'minimal'. Add an optional skipBaselineFallback flag to ZipBaselineFetcher.getFile and pass it from the telemetry_policy handler when canon_url is set, so a missing file in the override canon correctly falls back to the minimal tier.
klappy
added a commit
to klappy/klappy.dev
that referenced
this pull request
Apr 19, 2026
…canon→knowledge_base, baseline→bundled External-facing rename: terminology that users and future maintainers will actually read. Decision to rename made after canary validation revealed that 'canon' and 'baseline' are ODD-specific jargon that leaks into every caller's mental model. Zero reported external users today, so the migration cost is zero. Naming principles applied: - Name by function, not form. 'knowledge_base_url' names what the URL refers to (a knowledge base); 'canon_url' presumed a specific governance framework. - Use plain English that external users already know. 'Knowledge base' is universal vocabulary; 'canon' is a Klappy/ODD term. Tier renames in response envelope: - canon → knowledge_base (served from user's KB) - baseline → bundled (served from Worker's bundled snapshot) - minimal → minimal (unchanged; already plain English) 'Canon' as a content genre/concept stays throughout the prose — it's still what the docs ARE (stable, curated, human-governed truth). 'Knowledge base' is where they LIVE (a URL you point at). Different concepts, different words. Companion changes landing separately: - klappy/oddkit#108 (telemetry_policy canary completeness + same rename) - #105 (no changes needed — doesn't reference canon_url) - canon/principles/consistency-same-pattern-every-time.md — one stray reference to update in a follow-up sweep commit TruthKit alignment: this rename makes 'bring your own knowledge base' the supported story. A TruthKit consumer sets knowledge_base_url and the response envelope's governance_source tells them whether they're reading their canon, the bundled fallback, or the minimal last resort.
klappy
added a commit
to klappy/klappy.dev
that referenced
this pull request
Apr 19, 2026
Forward-facing documentation sweep aligning tool reference docs, consistency principle, and template with the rename landing in klappy.dev#101 and klappy/oddkit#108. Files updated: - canon/principles/consistency-same-pattern-every-time.md — one reference to `canon_url` values being server-invariant - docs/oddkit/tools/oddkit_{search,get,encode,orient,catalog,challenge, version,gate,cleanup_storage}.md — tool schema parameter references - docs/oddkit/tools/telemetry_public.md — telemetry schema field name and SQL query alias (blob6 was documented as canon_url; now documented as knowledge_base_url with the same underlying blob) - docs/examples/project-instructions-template.md — step 3 reference - docs/oddkit/IMPL-catalog-recent.md — temporal-discovery docs Intentionally NOT updated (historical records): - docs/planning/e0007-implementation-plan.md - docs/oddkit/proactive/handoff-to-new-conversation.md - docs/oddkit/proactive/e0007-validation.md These are archaeological records of epoch-7 planning and validation. Rewriting them to use current terminology would falsify the historical record. The terminology shift is documented in the PR landing this rename; readers of historical planning docs can infer the mapping. Companion PRs: - #101 (contract with new terminology) - klappy/oddkit#108 (tool implementation + smoke test)
User-facing terminology rename. Companion to klappy/klappy.dev#101 (contract update) and klappy/klappy.dev#118 (forward-facing doc sweep). Public API changes: - Every tool's Zod schema parameter: canon_url → knowledge_base_url. Affects oddkit router tool (action-routed) and all per-action tools (orient, search, get, challenge, gate, preflight, validate, encode, catalog, version, cleanup_storage) via the unified args passthrough. - telemetry_policy schema also renamed, with updated description naming strict-mode semantics: 'When set, strict mode is automatic: missing files fall through to the bundled governance tier rather than silently substituting from the default knowledge base.' Response envelope tier strings: - governance_source: "canon" → "knowledge_base" - governance_source: "baseline" → "bundled" - governance_source: "minimal" → "minimal" (unchanged) Smoke test (workers/test/canon-tool-envelope.smoke.mjs): - All parameter references renamed - Tier-string assertions updated - Strict-override test now explicitly verifies the 'knowledge_base_url set → bundled fallback suppressed' contract - Error message assertions check debug.knowledge_base_url echo Intentionally NOT renamed in this commit (internal, deferred): - ZipBaselineFetcher class → would become KnowledgeBaseFetcher - Internal variable canonUrl in orchestrate.ts (111 refs) - Internal variable canonUrl in telemetry.ts - BASELINE_URL environment variable Deferring internal renames keeps this PR focused on the user-visible contract. A dedicated internal-rename PR will clean up the class, variable names, and env var in one coordinated pass so reviewers can see the full internal shift at once rather than spread across the user-facing change. The router passthrough (orchestrate.ts interface) still names its input field canon_url for now — the rename there happens in the internal PR. Users never see that field; they pass knowledge_base_url at the MCP level and it's mapped to the orchestrator's canon_url parameter internally. Typecheck clean. Smoke test will run against Cloudflare preview once this commit deploys.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Telemetry extraction misses renamed parameter in request body
- Updated parseToolCall in workers/src/telemetry.ts to read a.knowledge_base_url (with legacy a.canon_url fallback) so blob6 captures the per-request override sent under the renamed schema.
Preview (c8f53ae1a4)
diff --git a/workers/src/index.ts b/workers/src/index.ts
--- a/workers/src/index.ts
+++ b/workers/src/index.ts
@@ -217,7 +217,7 @@
"voice-dump", "drafting", "peer-review-ready",
"canon-tier-2", "canon-tier-1", "published-essay",
]).optional().describe("Optional mode hint. Epistemic modes (exploration/planning/execution) or writing-lifecycle modes (voice-dump/drafting/peer-review-ready/canon-tier-2/canon-tier-1/published-essay). Sourced from odd/challenge/stakes-calibration."),
- canon_url: z.string().optional().describe("Optional GitHub repo URL for canon override."),
+ knowledge_base_url: z.string().optional().describe("Optional GitHub repo URL for your knowledge base. When set, strict mode is automatic: missing files fall through to the bundled governance tier rather than silently substituting from the default knowledge base."),
include_metadata: z.boolean().optional().describe("When true, search/get responses include a metadata object with full parsed frontmatter. Default: false."),
section: z.string().optional().describe("For action='get': extract only the named ## section from the document. Returns section content or available sections if not found."),
sort_by: z.enum(["date", "path"]).optional().describe("For action='catalog': sort articles. 'date' returns newest first (requires frontmatter). 'path' returns all docs alphabetically, including undated."),
@@ -238,7 +238,7 @@
input: args.input,
context: args.context,
mode: args.mode,
- canon_url: args.canon_url,
+ canon_url: args.knowledge_base_url,
include_metadata: args.include_metadata,
section: args.section,
sort_by: args.sort_by,
@@ -271,7 +271,7 @@
action: "orient",
schema: {
input: z.string().describe("A goal, idea, or situation description to orient against."),
- canon_url: z.string().optional().describe("Optional: GitHub repo URL for canon override."),
+ knowledge_base_url: z.string().optional().describe("Optional: GitHub repo URL for your knowledge base. When set, strict mode is automatic: missing files fall through to the bundled governance tier."),
},
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
},
@@ -286,7 +286,7 @@
"voice-dump", "drafting", "peer-review-ready",
"canon-tier-2", "canon-tier-1", "published-essay",
]).optional().describe("Mode for proportional challenge. Epistemic (exploration/planning/execution) or writing-lifecycle (voice-dump/drafting/peer-review-ready/canon-tier-2/canon-tier-1/published-essay). voice-dump suppresses all challenge output."),
- canon_url: z.string().optional().describe("Optional: GitHub repo URL for canon override."),
+ knowledge_base_url: z.string().optional().describe("Optional: GitHub repo URL for your knowledge base. When set, strict mode is automatic: missing files fall through to the bundled governance tier."),
},
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
},
@@ -297,7 +297,7 @@
schema: {
input: z.string().describe("The proposed transition (e.g., 'ready to build', 'moving to planning')."),
context: z.string().optional().describe("Optional context about what's been decided so far."),
- canon_url: z.string().optional().describe("Optional: GitHub repo URL for canon override."),
+ knowledge_base_url: z.string().optional().describe("Optional: GitHub repo URL for your knowledge base. When set, strict mode is automatic: missing files fall through to the bundled governance tier."),
},
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
},
@@ -308,7 +308,7 @@
schema: {
input: z.string().describe("A decision, insight, or boundary to capture."),
context: z.string().optional().describe("Optional supporting context."),
- canon_url: z.string().optional().describe("Optional: GitHub repo URL for canon override."),
+ knowledge_base_url: z.string().optional().describe("Optional: GitHub repo URL for your knowledge base. When set, strict mode is automatic: missing files fall through to the bundled governance tier."),
},
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
},
@@ -318,7 +318,7 @@
action: "search",
schema: {
input: z.string().describe("Natural language query or tags to search for."),
- canon_url: z.string().optional().describe("Optional: GitHub repo URL for canon override."),
+ knowledge_base_url: z.string().optional().describe("Optional: GitHub repo URL for your knowledge base. When set, strict mode is automatic: missing files fall through to the bundled governance tier."),
include_metadata: z.boolean().optional().describe("When true, each hit includes a metadata object with full parsed frontmatter. Default: false."),
},
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
@@ -329,7 +329,7 @@
action: "get",
schema: {
input: z.string().describe("Canonical URI (e.g., klappy://canon/values/orientation)."),
- canon_url: z.string().optional().describe("Optional: GitHub repo URL for canon override."),
+ knowledge_base_url: z.string().optional().describe("Optional: GitHub repo URL for your knowledge base. When set, strict mode is automatic: missing files fall through to the bundled governance tier."),
include_metadata: z.boolean().optional().describe("When true, response includes a metadata object with full parsed frontmatter. Default: false."),
section: z.string().optional().describe("Extract only the named ## section from the document. Returns available sections if not found."),
},
@@ -340,7 +340,7 @@
description: "Lists available documentation with categories, counts, and start-here suggestions. Supports temporal discovery: use sort_by='date' to get recent articles with full frontmatter metadata.",
action: "catalog",
schema: {
- canon_url: z.string().optional().describe("Optional: GitHub repo URL for canon override."),
+ knowledge_base_url: z.string().optional().describe("Optional: GitHub repo URL for your knowledge base. When set, strict mode is automatic: missing files fall through to the bundled governance tier."),
sort_by: z.enum(["date", "path"]).optional().describe("Sort articles. 'date' returns newest first (requires frontmatter). 'path' returns all docs alphabetically, including undated."),
limit: z.number().min(1).max(500).optional().describe("Max articles to return when sort_by is provided. Default: 10, max: 500."),
offset: z.number().min(0).optional().describe("Skip this many articles before returning results. Use with limit for pagination. Default: 0."),
@@ -363,7 +363,7 @@
action: "preflight",
schema: {
input: z.string().describe("Description of what you're about to implement."),
- canon_url: z.string().optional().describe("Optional: GitHub repo URL for canon override."),
+ knowledge_base_url: z.string().optional().describe("Optional: GitHub repo URL for your knowledge base. When set, strict mode is automatic: missing files fall through to the bundled governance tier."),
},
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
},
@@ -372,7 +372,7 @@
description: "Returns oddkit version and the authoritative canon target (commit/mode).",
action: "version",
schema: {
- canon_url: z.string().optional().describe("Optional: GitHub repo URL for canon override."),
+ knowledge_base_url: z.string().optional().describe("Optional: GitHub repo URL for your knowledge base. When set, strict mode is automatic: missing files fall through to the bundled governance tier."),
},
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
},
@@ -381,7 +381,7 @@
description: "Storage hygiene: clears orphaned cached data. NOT required for correctness — content-addressed caching ensures fresh content is served automatically when the baseline changes.",
action: "cleanup_storage",
schema: {
- canon_url: z.string().optional().describe("Optional: GitHub repo URL for canon override."),
+ knowledge_base_url: z.string().optional().describe("Optional: GitHub repo URL for your knowledge base. When set, strict mode is automatic: missing files fall through to the bundled governance tier."),
},
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
},
@@ -399,7 +399,7 @@
input: (args.input as string) || "",
context: args.context as string | undefined,
mode: args.mode as string | undefined,
- canon_url: args.canon_url as string | undefined,
+ canon_url: args.knowledge_base_url as string | undefined,
include_metadata: args.include_metadata as boolean | undefined,
section: args.section as string | undefined,
sort_by: args.sort_by as string | undefined,
@@ -430,7 +430,7 @@
blob3 — tool_name oddkit action (e.g. "orient", "search")
blob4 — consumer_label best-effort caller identity
blob5 — consumer_source how label was resolved (e.g. "user-agent")
- blob6 — canon_url which repo baseline is being served
+ blob6 — knowledge_base_url which knowledge base is being served
blob7 — document_uri for get calls, the klappy:// URI requested
blob8 — worker_version oddkit version string
double1 — count always 1
@@ -496,35 +496,46 @@
server.tool(
"telemetry_policy",
- "Return oddkit telemetry and sharing policy guidance. What is tracked, what is excluded, and why. Fetched from canonical governance document at runtime. Response envelope declares governance_source (canon|baseline|minimal) per canon/constraints/core-governance-baseline.",
- {},
+ "Return oddkit telemetry and sharing policy guidance. What is tracked, what is excluded, and why. Fetched from canonical governance document at runtime. Response envelope declares governance_source (knowledge_base|bundled|minimal) per canon/constraints/core-governance-baseline. Accepts knowledge_base_url to read from an alternate knowledge base.",
{
+ knowledge_base_url: z.string().optional().describe("Optional GitHub repo URL for your knowledge base. When set, strict mode is automatic: missing files fall through to the bundled governance tier rather than silently substituting from the default knowledge base. When provided, fetches canon/constraints/telemetry-governance.md from this repo instead of the oddkit-hosted default. Falls back to the minimal baseline if the file is missing."),
+ },
+ {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true,
},
- async () => {
+ async ({ knowledge_base_url }) => {
// Governance resolution per canon/constraints/core-governance-baseline:
- // 1. Live canon fetch (preferred) → governance_source: "canon"
- // 2. Minimal baseline (shipped in code) → governance_source: "minimal"
+ // 1. Live knowledge base fetch (preferred) → governance_source: "knowledge_base"
+ // 2. Bundled governance (oddkit Worker snapshot) → governance_source: "bundled"
+ // 3. Minimal hardcoded fallback → governance_source: "minimal"
//
// This canary refactor implements tiers 1 and 3 only. The bundled
// baseline tier (2) and the build-time schema check arrive in follow-up
// work; the manifest + baseline directory are not yet in place.
+ const startTime = Date.now();
const fetcher = new ZipBaselineFetcher(env);
let policyContent: string | null = null;
let selfReportHeaders: Record<string, string> | null = null;
- let governanceSource: "canon" | "baseline" | "minimal" = "minimal";
+ let governanceSource: "knowledge_base" | "bundled" | "minimal" = "minimal";
try {
- const content = await fetcher.getFile("canon/constraints/telemetry-governance.md");
+ // When knowledge_base_url is set, strict mode is automatic: suppress the bundled-governance fallback
+ // so a missing file in the override knowledge base surfaces as "minimal" rather
+ // than silently serving content from the default knowledge base.
+ const content = await fetcher.getFile(
+ "canon/constraints/telemetry-governance.md",
+ knowledge_base_url,
+ knowledge_base_url ? { skipBaselineFallback: true } : undefined,
+ );
if (content) {
policyContent = content;
const parsed = parseSelfReportHeadersTable(content);
if (parsed && Object.keys(parsed).length > 0) {
selfReportHeaders = parsed;
- governanceSource = "canon";
+ governanceSource = "knowledge_base";
}
}
} catch {
@@ -551,6 +562,9 @@
}
}
+ const headerCount = selfReportHeaders ? Object.keys(selfReportHeaders).length : 0;
+ const assistantText = `Telemetry policy loaded from ${governanceSource}. ${headerCount} self-report headers available.${knowledge_base_url ? ` (knowledge_base_url override: ${knowledge_base_url})` : ""}`;
+
return {
content: [{
type: "text" as const,
@@ -563,6 +577,9 @@
self_report_headers: selfReportHeaders,
generated_at: new Date().toISOString(),
},
+ server_time: new Date().toISOString(),
+ assistant_text: assistantText,
+ debug: { duration_ms: Date.now() - startTime, knowledge_base_url: knowledge_base_url ?? null },
}, null, 2),
}],
};
diff --git a/workers/src/telemetry.ts b/workers/src/telemetry.ts
--- a/workers/src/telemetry.ts
+++ b/workers/src/telemetry.ts
@@ -160,8 +160,10 @@
if (typeof a.input === "string" && a.input.includes("://")) {
documentUri = a.input;
}
- // Extract canon_url from tool arguments
- if (typeof a.canon_url === "string" && a.canon_url) {
+ // Extract knowledge base URL from tool arguments (accept legacy canon_url alias)
+ if (typeof a.knowledge_base_url === "string" && a.knowledge_base_url) {
+ canonUrl = a.knowledge_base_url;
+ } else if (typeof a.canon_url === "string" && a.canon_url) {
canonUrl = a.canon_url;
}
}
diff --git a/workers/src/zip-baseline-fetcher.ts b/workers/src/zip-baseline-fetcher.ts
--- a/workers/src/zip-baseline-fetcher.ts
+++ b/workers/src/zip-baseline-fetcher.ts
@@ -978,12 +978,25 @@
* Get a specific file from the baseline or canon.
* Content-addressed: file cache is keyed to each repo's own commit SHA.
* Three-tier: module memory → R2 → ZIP extraction.
+ *
+ * When `options.skipBaselineFallback` is true, the baseline repo is not
+ * appended to the search sources. Callers that need to distinguish between
+ * "file found in the canon_url override" and "file found in the baseline
+ * fallback" can pass this flag so a null return unambiguously means the
+ * override canon lacks the file.
*/
- async getFile(path: string, canonUrl?: string): Promise<string | null> {
+ async getFile(
+ path: string,
+ canonUrl?: string,
+ options?: { skipBaselineFallback?: boolean },
+ ): Promise<string | null> {
const baselineRepoUrl = "https://github.com/klappy/klappy.dev";
+ const skipBaselineFallback = options?.skipBaselineFallback === true;
- // Resolve SHA for each repo independently
- const baselineSha = await this.getLatestCommitSha(baselineRepoUrl);
+ // Resolve SHA for the baseline only when it will actually be searched.
+ const baselineSha = skipBaselineFallback && canonUrl
+ ? null
+ : await this.getLatestCommitSha(baselineRepoUrl);
// Build the list of repos to search, each with its own SHA
const sources: Array<{ url: string; repoKey: string; sha: string }> = [];
@@ -998,13 +1011,15 @@
});
}
- sources.push({
- url: this.env.BASELINE_URL.includes("raw.githubusercontent.com")
- ? this.env.BASELINE_URL.replace("/main", "").replace("raw.githubusercontent.com", "github.com")
- : baselineRepoUrl,
- repoKey: getCacheKey("baseline"),
- sha: baselineSha || "unknown",
- });
+ if (!(skipBaselineFallback && canonUrl)) {
+ sources.push({
+ url: this.env.BASELINE_URL.includes("raw.githubusercontent.com")
+ ? this.env.BASELINE_URL.replace("/main", "").replace("raw.githubusercontent.com", "github.com")
+ : baselineRepoUrl,
+ repoKey: getCacheKey("baseline"),
+ sha: baselineSha || "unknown",
+ });
+ }
for (const source of sources) {
// Content-addressed cache key: repo identity + repo SHA + file path
diff --git a/workers/test/canon-tool-envelope.smoke.mjs b/workers/test/canon-tool-envelope.smoke.mjs
new file mode 100644
--- /dev/null
+++ b/workers/test/canon-tool-envelope.smoke.mjs
@@ -1,0 +1,139 @@
+#!/usr/bin/env node
+/**
+ * Live smoke test for knowledge-base-driven MCP tool envelope contracts.
+ *
+ * Exercises the actual MCP endpoint (preview or prod) and verifies that
+ * every canon-driven tool returns the full envelope shape:
+ *
+ * { action, result, server_time, assistant_text, debug, ... }
+ *
+ * AND that knowledge-base-driven tools surface `governance_source` inside `result` with one of: knowledge_base | bundled | minimal.
+ *
+ * Why this exists: parser tests (workers/test/governance-parser.test.mjs)
+ * exercise parser logic in isolation. They passed for the telemetry_policy
+ * canary, but the canary shipped with a broken envelope and silent
+ * knowledge_base_url fallback because no test invoked the MCP tool end-to-end.
+ * Parser tests cannot catch the tool's response contract — only live smoke
+ * against the MCP endpoint can. This test also verifies the strict-override
+ * contract: when knowledge_base_url points at a repo lacking the file, the
+ * response must surface governance_source: 'minimal', not silently substitute
+ * from the default knowledge base.
+ *
+ * Usage:
+ * node workers/test/canon-tool-envelope.smoke.mjs
+ * ODDKIT_URL=https://preview-xxx.oddkit.klappy.dev/mcp node ...
+ *
+ * Exit 0 on all pass, 1 on any failure.
+ */
+
+const ODDKIT_URL = process.env.ODDKIT_URL || "https://oddkit.klappy.dev/mcp";
+
+let passed = 0;
+let failed = 0;
+
+function ok(label, cond, hint = "") {
+ if (cond) {
+ console.log(` ✓ ${label}`);
+ passed++;
+ } else {
+ console.log(` ✗ ${label}${hint ? ` — ${hint}` : ""}`);
+ failed++;
+ }
+}
+
+async function callTool(name, args = {}) {
+ const body = JSON.stringify({
+ jsonrpc: "2.0",
+ id: 1,
+ method: "tools/call",
+ params: { name, arguments: args },
+ });
+ const res = await fetch(ODDKIT_URL, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "Accept": "application/json, text/event-stream",
+ "x-oddkit-client": "envelope-smoke-test",
+ },
+ body,
+ });
+ const text = await res.text();
+ // SSE format: `event: message\ndata: {...}\n\n`
+ const match = text.match(/data: (\{[\s\S]*\})/);
+ if (!match) throw new Error(`No data payload from ${name}: ${text.slice(0, 300)}`);
+ const envelope = JSON.parse(match[1]);
+ const inner = JSON.parse(envelope.result.content[0].text);
+ return inner;
+}
+
+function expectFullEnvelope(toolName, inner) {
+ console.log(`\n─── Envelope shape: ${toolName} ───`);
+ ok(`${toolName}: has 'action'`, typeof inner.action === "string");
+ ok(`${toolName}: has 'result'`, typeof inner.result === "object" && inner.result !== null);
+ ok(`${toolName}: has 'server_time' (ISO 8601)`,
+ typeof inner.server_time === "string" && /^\d{4}-\d{2}-\d{2}T/.test(inner.server_time),
+ `got: ${inner.server_time}`);
+ ok(`${toolName}: has 'assistant_text'`, typeof inner.assistant_text === "string" && inner.assistant_text.length > 0);
+ ok(`${toolName}: has 'debug'`, typeof inner.debug === "object" && inner.debug !== null);
+ ok(`${toolName}: debug.duration_ms is a number`, typeof inner.debug?.duration_ms === "number");
+}
+
+function expectGovernanceSource(toolName, inner, expectedTier) {
+ console.log(`\n─── Governance source: ${toolName} ───`);
+ const source = inner.result?.governance_source;
+ ok(`${toolName}: result.governance_source present`, typeof source === "string", `got: ${source}`);
+ ok(`${toolName}: result.governance_source is one of knowledge_base|bundled|minimal`,
+ ["knowledge_base", "bundled", "minimal"].includes(source),
+ `got: ${source}`);
+ if (expectedTier) {
+ ok(`${toolName}: result.governance_source == "${expectedTier}"`,
+ source === expectedTier,
+ `got: ${source}`);
+ }
+}
+
+async function run() {
+ console.log(`Target: ${ODDKIT_URL}\n`);
+
+ // Tool 1: oddkit_time — non-canon-driven baseline for envelope convention
+ const timeResult = await callTool("oddkit_time");
+ expectFullEnvelope("oddkit_time", timeResult);
+
+ // Tool 2: telemetry_policy — canon-driven, should have full envelope + governance_source
+ const policyDefault = await callTool("telemetry_policy");
+ expectFullEnvelope("telemetry_policy (default knowledge_base)", policyDefault);
+ expectGovernanceSource("telemetry_policy (default knowledge_base)", policyDefault, "knowledge_base");
+
+ // Tool 3: telemetry_policy with knowledge_base_url override pointing at a repo
+ // that doesn't have the governance file — should fall back to minimal.
+ // This verifies the strict-override contract: when knowledge_base_url is set,
+ // the bundled fallback is suppressed so a missing file surfaces as "minimal".
+ console.log(`\n─── knowledge_base_url override: telemetry_policy ───`);
+ const policyOverride = await callTool("telemetry_policy", {
+ knowledge_base_url: "https://github.com/torvalds/linux",
+ });
+ expectFullEnvelope("telemetry_policy (knowledge_base_url override)", policyOverride);
+ ok(
+ "telemetry_policy: knowledge_base_url override falls back to minimal when file missing (strict mode)",
+ policyOverride.result?.governance_source === "minimal",
+ `got: ${policyOverride.result?.governance_source}`,
+ );
+ ok(
+ "telemetry_policy: minimal fallback still returns 8 headers",
+ Object.keys(policyOverride.result?.self_report_headers ?? {}).length === 8,
+ `got: ${Object.keys(policyOverride.result?.self_report_headers ?? {}).length}`,
+ );
+ ok(
+ "telemetry_policy: debug.knowledge_base_url echoes the override",
+ policyOverride.debug?.knowledge_base_url === "https://github.com/torvalds/linux",
+ `got: ${policyOverride.debug?.knowledge_base_url}`,
+ );
+
+ console.log(`\n${passed} passed, ${failed} failed`);
+ process.exit(failed === 0 ? 0 : 1);
+}
+
+run().catch((e) => {
+ console.error(e);
+ process.exit(1);
+});You can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit 5ae00f2. Configure here.
This was referenced Apr 19, 2026
klappy
pushed a commit
that referenced
this pull request
Apr 20, 2026
…convention
telemetry_public was the sole tool returning a bare {action, result} envelope.
Every other oddkit tool (including telemetry_policy per PR #108) returns the
full {action, result, server_time, assistant_text, debug} shape.
Canon reference: klappy://docs/appendices/epoch-8-2 — server_time in every response.
Precedent: PR #108 — telemetry_policy envelope conformance.
Changes:
- workers/src/index.ts: telemetry_public handler emits full envelope on both success and not-configured error paths
- workers/test/canon-tool-envelope.smoke.mjs: add telemetry_public assertion via expectFullEnvelope
Caught by v0.21.0 regression test sweep (2026-04-20).
This was referenced Apr 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Canary completeness fix + knowledge_base terminology rename
Live validation of canary PR #106 against prod surfaced three contract-conformance gaps, plus a naming audit surfaced that
canon_url/governance_source: "canon"are ODD-specific jargon leaking into every caller's mental model. This PR addresses both in one coordinated landing.What was broken (and now fixed)
Canary gaps
telemetry_policyreturned{action, result}while every other tool returns the full{action, result, server_time, assistant_text, debug}. Fixed: full envelope now matches convention. Time-discipline contract preserved.canon_urlsilently ignored / fallback bug — Bugbot caught thatZipBaselineFetcher.getFile()always appended the defaultklappy/klappy.devrepo as a search fallback. Whencanon_urlpointed elsewhere and the override lacked the file, the file was still found in the fallback, sogovernance_sourcewas set to"canon"instead of"minimal". Cursor Agent pushed a fix introducingskipBaselineFallbackon the fetcher; thetelemetry_policyhandler now sets it whenever a user knowledge_base_url is provided, giving "strict mode is automatic when overridden" semantics.workers/test/canon-tool-envelope.smoke.mjsnow exists. Curls the MCP endpoint, asserts the envelope shape, assertsgovernance_sourceis one ofknowledge_base | bundled | minimal, and verifies the strict-override falls through tominimalwhen the override repo lacks the file.Terminology rename
canon_url→knowledge_base_url(function-named, plain English)"canon"→"knowledge_base","baseline"→"bundled","minimal"unchangedZipBaselineFetcher,canonUrl,BASELINE_URL) deferred to a follow-up PR to keep this PR focused on user-visible contract changesChanges
workers/src/index.ts— every tool's Zod schema renamed; telemetry_policy handler updated for new naming and strict semanticsworkers/test/canon-tool-envelope.smoke.mjs— full rewrite of assertions to match new APIklappy/klappy.dev#101(contract)klappy/klappy.dev#106(forward-facing docs)Verification
npm run typecheckcleanODDKIT_URL=<preview-url> node workers/test/canon-tool-envelope.smoke.mjsLessons
canon_url/ tier strings was made in one turn with zero migration cost.Related
klappy/oddkit#106— original canary (completed by this PR)klappy/klappy.dev#101— core-governance-baseline contract (canary-learned invariants + rename)klappy/klappy.dev#102— canon Description column (merged)klappy/klappy.dev#105— validation as fourth epistemic mode (open, separate concern)klappy/klappy.dev#106— forward-facing doc sweep (rename companion)Note
Medium Risk
Medium risk because it changes the public MCP tool contract (
canon_url→knowledge_base_url) and alters fallback behavior for governance file resolution, which could affect existing callers and what content is served.Overview
Renames the public knowledge-base override parameter across the unified
oddkittool and all individual tools fromcanon_urltoknowledge_base_url, and updates telemetry labeling/docs to match.Fixes
telemetry_policycontract gaps by returning the full standard envelope (server_time,assistant_text,debug) and by treating governance tiers asknowledge_base|bundled|minimal, plus adding a strict-override mode where a providedknowledge_base_urlsuppresses baseline fallback so missing files fall through tominimal.Extends repo fetch semantics via
ZipBaselineFetcher.getFile(..., options)withskipBaselineFallbackto support the strict override behavior, and updates telemetry parsing to acceptknowledge_base_urlwhile still supporting legacycanon_url.Adds a live end-to-end smoke test (
workers/test/canon-tool-envelope.smoke.mjs) that calls the deployed MCP endpoint to assert envelope shape and strict override behavior.Reviewed by Cursor Bugbot for commit c8f53ae. Bugbot is set up for automated code reviews on this repo. Configure here.