Skip to content

fix(telemetry_policy): canary completeness + knowledge_base rename + live smoke#108

Merged
klappy merged 4 commits intomainfrom
fix/telemetry-policy-envelope-and-canon-url
Apr 19, 2026
Merged

fix(telemetry_policy): canary completeness + knowledge_base rename + live smoke#108
klappy merged 4 commits intomainfrom
fix/telemetry-policy-envelope-and-canon-url

Conversation

@klappy
Copy link
Copy Markdown
Owner

@klappy klappy commented Apr 19, 2026

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

  1. Envelope convention divergencetelemetry_policy returned {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.
  2. canon_url silently ignored / fallback bug — Bugbot caught that ZipBaselineFetcher.getFile() always appended the default klappy/klappy.dev repo as a search fallback. When canon_url pointed elsewhere and the override lacked the file, the file was still found in the fallback, so governance_source was set to "canon" instead of "minimal". Cursor Agent pushed a fix introducing skipBaselineFallback on the fetcher; the telemetry_policy handler now sets it whenever a user knowledge_base_url is provided, giving "strict mode is automatic when overridden" semantics.
  3. No live-smokeworkers/test/canon-tool-envelope.smoke.mjs now exists. Curls the MCP endpoint, asserts the envelope shape, asserts governance_source is one of knowledge_base | bundled | minimal, and verifies the strict-override falls through to minimal when the override repo lacks the file.

Terminology rename

  • Parameter: canon_urlknowledge_base_url (function-named, plain English)
  • Response envelope tier strings: "canon""knowledge_base", "baseline""bundled", "minimal" unchanged
  • Class/variable internal rename (ZipBaselineFetcher, canonUrl, BASELINE_URL) deferred to a follow-up PR to keep this PR focused on user-visible contract changes

Changes

  • workers/src/index.ts — every tool's Zod schema renamed; telemetry_policy handler updated for new naming and strict semantics
  • workers/test/canon-tool-envelope.smoke.mjs — full rewrite of assertions to match new API
  • Companion canon update: klappy/klappy.dev#101 (contract)
  • Companion doc sweep: klappy/klappy.dev#106 (forward-facing docs)

Verification

  • npm run typecheck clean
  • Wait for Cloudflare preview deploy to attach to this PR
  • Run ODDKIT_URL=<preview-url> node workers/test/canon-tool-envelope.smoke.mjs
  • If smoke green: merge order is klappy.dev#101 → klappy.dev#106 → this PR → promotion PR to prod

Lessons

  • Parser tests are necessary, not sufficient. End-to-end smoke against the MCP endpoint catches contract gaps that parser tests cannot see.
  • Bugbot / Cursor Agent collaboration is real. Bugbot caught the silent-fallback bug I missed during live validation; Cursor Agent pushed the strict-override fix while I was focused on the rename.
  • Naming is a contract. Renaming before external users arrive is free; renaming after is expensive. The decision to rename 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_urlknowledge_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 oddkit tool and all individual tools from canon_url to knowledge_base_url, and updates telemetry labeling/docs to match.

Fixes telemetry_policy contract gaps by returning the full standard envelope (server_time, assistant_text, debug) and by treating governance tiers as knowledge_base|bundled|minimal, plus adding a strict-override mode where a provided knowledge_base_url suppresses baseline fallback so missing files fall through to minimal.

Extends repo fetch semantics via ZipBaselineFetcher.getFile(..., options) with skipBaselineFallback to support the strict override behavior, and updates telemetry parsing to accept knowledge_base_url while still supporting legacy canon_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.

…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.
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented Apr 19, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

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.
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread workers/src/index.ts Outdated
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.
@klappy klappy changed the title fix(telemetry_policy): complete envelope + canon_url support + live smoke fix(telemetry_policy): canary completeness + knowledge_base rename + live smoke Apr 19, 2026
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

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.

Comment thread workers/src/index.ts
@klappy klappy merged commit 36514bd into main Apr 19, 2026
5 checks passed
@klappy klappy deleted the fix/telemetry-policy-envelope-and-canon-url branch April 19, 2026 06:37
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).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants