Conversation
…0.0) (#118) P1.3.2 Phase 2. runGateAction refactored to consume klappy://odd/gate/transitions and klappy://odd/gate/prerequisites at runtime via two new helpers (fetchGateTransitions, fetchGatePrerequisites). Transition detection via BM25 stemmed matching (ranking problem); prereq evaluation via stemmed set intersection (independent gap-or-not, avoids BM25 IDF-negative pathology on small shared-vocabulary corpora). Envelope declares governance_source + governance_uris (plural array of 2) + debug.knowledge_base_url echo. Preview smoke 158/158 × 3 consecutive clean. Canon-first satisfied: klappy/klappy.dev#120 + #122 merged before this PR.
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
oddkit | 260492c | Commit Preview URL Branch Preview URL |
Apr 20 2026, 03:16 AM |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 4 potential issues.
Bugbot Autofix prepared fixes for all 4 issues found in the latest run.
- ✅ Fixed: Transition detection now includes context, changing behavior
- Changed BM25 transition detection to use only
input(notfullInput), preserving the old behavior of scoping transition keywords to the user's proposed transition phrase while still allowingfullInputfor prereq stem matching.
- Changed BM25 transition detection to use only
- ✅ Fixed: Unknown prereqs silently pass gate with inconsistent count
- Gate now fails closed on governance errors: gateStatus is NOT_READY when either unmet or unknown prereqs exist, so unresolved prereq IDs no longer let the gate advance state with an inconsistent required_met/required_total count.
- ✅ Fixed: Old
detectTransitionfunction is now dead code- Removed the unused
detectTransitionfunction fromworkers/src/orchestrate.ts; BM25-based detection inrunGateActionis now the sole transition path.
- Removed the unused
- ✅ Fixed: Prereq IDs not backtick-stripped unlike transition keys
- Both the transitions-table prereq-IDs column and the prerequisites-table id column now strip backticks before splitting/trimming, so backtick-wrapped canon identifiers match rather than silently routing to
unknown.
- Both the transitions-table prereq-IDs column and the prerequisites-table id column now strip backticks before splitting/trimming, so backtick-wrapped canon identifiers match rather than silently routing to
Preview (260492c6a2)
diff --git a/workers/src/orchestrate.ts b/workers/src/orchestrate.ts
--- a/workers/src/orchestrate.ts
+++ b/workers/src/orchestrate.ts
@@ -348,26 +348,6 @@
return { mode: sorted[0][0], confidence };
}
-function detectTransition(input: string): { from: string; to: string } {
- if (/\b(ready to build|ready to implement|start building|let's code|start coding)\b/i.test(input))
- return { from: "planning", to: "execution" };
- if (
- /\b(ready to plan|start planning|let's plan|time to plan|move to planning|moving to planning)\b/i.test(
- input,
- )
- )
- return { from: "exploration", to: "planning" };
- if (/\b(moving to execution|moving to build)\b/i.test(input))
- return { from: "planning", to: "execution" };
- if (/\b(back to exploration|need to rethink|step back|reconsider)\b/i.test(input))
- return { from: "execution", to: "exploration" };
- if (/\b(ship|deploy|release|go live|push to prod)\b/i.test(input))
- return { from: "execution", to: "completion" };
- if (/\b(ready|let's go|proceed|move forward|next step)\b/i.test(input))
- return { from: "exploration", to: "planning" };
- return { from: "unknown", to: "unknown" };
-}
-
// Discover encoding types from canon governance docs.
//
// Governance resolution per canon/constraints/core-governance-baseline:
@@ -733,7 +713,7 @@
const key = cols[0].replace(/`/g, "").trim();
const from = cols[1].trim();
const to = cols[2].trim();
- const prereqIdsRaw = cols[3].trim();
+ const prereqIdsRaw = cols[3].replace(/`/g, "").trim();
const detectionText = cols[4].trim();
if (key.length === 0) continue;
const prereqIds = prereqIdsRaw.length > 0
@@ -787,7 +767,7 @@
for (const row of section[1].split("\n").filter((r: string) => r.includes("|"))) {
const cols = parseTableRow(row);
if (cols.length >= 3) {
- const id = cols[0].trim();
+ const id = cols[0].replace(/`/g, "").trim();
const check = cols[1].trim();
const gapMessage = cols[2].replace(/^"|"$/g, "").trim();
if (id.length === 0) continue;
@@ -2368,7 +2348,7 @@
// deterministically when two transitions score identically.
const bm25Docs = transitions.map((t) => ({ id: t.key, text: t.detectionText }));
const transitionIndex = buildBM25Index(bm25Docs);
- const hits = searchBM25(transitionIndex, fullInput, transitions.length);
+ const hits = searchBM25(transitionIndex, input, transitions.length);
let matchedTransition: TransitionDef | null = null;
if (hits.length > 0 && hits[0].score > 0) {
@@ -2414,7 +2394,7 @@
}
}
- const gateStatus = unmet.length > 0 ? "NOT_READY" : "PASS";
+ const gateStatus = unmet.length > 0 || unknown.length > 0 ? "NOT_READY" : "PASS";
const requiredTotal = matchedTransition ? matchedTransition.prereqIds.length : 0;
// Update stateYou can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit 260492c. Configure here.
| // deterministically when two transitions score identically. | ||
| const bm25Docs = transitions.map((t) => ({ id: t.key, text: t.detectionText })); | ||
| const transitionIndex = buildBM25Index(bm25Docs); | ||
| const hits = searchBM25(transitionIndex, fullInput, transitions.length); |
There was a problem hiding this comment.
Transition detection now includes context, changing behavior
Medium Severity
BM25 transition detection uses fullInput (input + context), but the old detectTransition used only input. This means optional context (described as "what's been decided so far") can now influence which transition is detected. If a user provides vague input like "what next?" with context containing phrases like "we shipped the feature", the gate could detect execution-to-completion from context alone — the old code would have returned unknown/unknown. The PR claims "strictly additive" matching, but widening the detection input surface is a semantic change, not just a stemming addition.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 260492c. Configure here.
| } | ||
|
|
||
| const gateStatus = unmet.length > 0 ? "NOT_READY" : "PASS"; | ||
| const requiredTotal = matchedTransition ? matchedTransition.prereqIds.length : 0; |
There was a problem hiding this comment.
Unknown prereqs silently pass gate with inconsistent count
Low Severity
When a transition references a prereq ID not found in the prerequisites map (governance configuration error), the prereq goes into unknown rather than unmet. Since gateStatus only checks unmet.length > 0, unknown prereqs don't block the gate. This creates a contradictory response: status is PASS but required_met < required_total (e.g., "2/3 required met" alongside "PASS"), and the gate advances state despite unresolved prerequisites.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 260492c. Configure here.
| // deterministically when two transitions score identically. | ||
| const bm25Docs = transitions.map((t) => ({ id: t.key, text: t.detectionText })); | ||
| const transitionIndex = buildBM25Index(bm25Docs); | ||
| const hits = searchBM25(transitionIndex, fullInput, transitions.length); |
There was a problem hiding this comment.
Old detectTransition function is now dead code
Low Severity
The detectTransition function (line 351 in orchestrate.ts) is no longer called anywhere in the workers codebase. The only call site was in runGateAction, which this PR replaced with BM25-based detection. The function is now dead code that could confuse future readers about the intended transition detection path.
Reviewed by Cursor Bugbot for commit 260492c. Configure here.
| const detectionText = cols[4].trim(); | ||
| if (key.length === 0) continue; | ||
| const prereqIds = prereqIdsRaw.length > 0 | ||
| ? prereqIdsRaw.split(",").map((s: string) => s.trim()).filter((s: string) => s.length > 0) |
There was a problem hiding this comment.
Prereq IDs not backtick-stripped unlike transition keys
Medium Severity
The transition key column strips backticks via .replace(//g, ""), but prereqIdsRawat the prereq-ID column does not. If the canon markdown table wraps prereq IDs in backticks (a natural formatting choice, consistent with how keys are formatted), the parsed IDs would retain backticks (e.g., ``decisions_locked``) and fail to match against the backtick-free IDs parsed from the prerequisites table. This would silently route every prereq tounknown`, and — per the separate gate-pass-on-unknown issue — the gate would pass with zero prereqs evaluated.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 260492c. Configure here.


Promote 0.20.0 from main to prod.
What's shipping
PR #118 (P1.3.2 Phase 2) merged at
260492c.oddkit_gatenow reads governance from canon at runtime:klappy://odd/gate/transitions— transition keys, detection terms, prereq id mappingsklappy://odd/gate/prerequisites— prereq definitions, check vocabularies, gap messagesEnvelope declares
governance_source+governance_uris(plural array of 2) +debug.knowledge_base_urlecho. Transition detection uses BM25 stemmed matching (ranking problem); prereq evaluation uses stemmed set intersection (independent gap-or-not; avoids BM25 IDF-negative pathology on small shared-vocabulary corpora).Strictly additive — every input that matched 0.19.0's hardcoded word-boundary regex still matches, plus stemmed variations (
deploying,started building,reconsideringetc.) now match too.Verification
https://gate-governance-source-envelope-oddkit.klappy.workers.devhttps://main-oddkit.klappy.workers.dev(just ran)Canon-first satisfied
86a7194937eb52oddkit_getbefore PR feat(gate): governance-driven BM25 + set intersection + envelope (0.20.0) #118 openedRefs
klappy://odd/handoffs/2026-04-20-p1-3-2-phase-2-gate-code-refactorPost-merge prod-smoke expected 158/158 × 3 consecutive after ~40s warmup.
Note
Medium Risk
Updates
oddkit_gate’s core transition/prerequisite logic and response schema (new governance fields and changed prereq outputs), which may break consumers relying on prior matching behavior or string formats, though it includes explicit minimal fallbacks and added tests.Overview
Promotes v0.20.0 (bumps root and worker versions) and documents the release in
CHANGELOG.md, centered on a canon-driven refactor ofoddkit_gate.oddkit_gatenow loads transition and prerequisite vocabulary from canon at runtime with a hardcoded minimal fallback, switches transition detection from regex cascades to BM25 + stemming, and changes prerequisite checks to stemmed set-intersection. The tool’s envelope now includesgovernance_source,governance_uris(array of 2), and echoesdebug.knowledge_base_url.The gate result contract changes:
prerequisites.metnow returns prereq ids (not descriptions) andprerequisites.unmetreturns canon gap messages; the smoke test suite is extended to cover the new envelope fields, override behavior, BM25 priority resolution, and stemmed matching cases.Reviewed by Cursor Bugbot for commit 260492c. Bugbot is set up for automated code reviews on this repo. Configure here.