diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9a4f5217..1d507ce3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -89,7 +89,10 @@ jobs: version: 2.84.2 - name: Start local Supabase - run: supabase start -x realtime,storage-api,imgproxy,mailpit,postgres-meta,studio,edge-runtime,logflare,vector,supavisor + # storage-api + edge-runtime are included (not in -x): the peer-notebook + # tests need Storage and the intelligence-pipeline/branch-workers tests + # invoke edge functions. + run: supabase start -x realtime,imgproxy,mailpit,postgres-meta,studio,logflare,vector,supavisor - name: Run test suite run: pnpm test diff --git a/.github/workflows/validate-pr.yml b/.github/workflows/validate-pr.yml deleted file mode 100644 index 6462bb4c..00000000 --- a/.github/workflows/validate-pr.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: Validate PR - -# Deterministic validation of the PR description (no LLM). Checks that the -# PR's description artifact under prs/ conforms to the expected schema. - -on: - pull_request: - branches: [main] - types: [opened, synchronize, reopened] - -permissions: - contents: read - -jobs: - validate-pr-description: - name: Validate PR Description - runs-on: ubuntu-latest - timeout-minutes: 5 - steps: - - name: Checkout - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - with: - persist-credentials: false - - - name: Setup pnpm - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 - with: - version: 9.15.4 - - - name: Setup Node.js - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - with: - node-version: "22" - cache: "pnpm" - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Validate PR description - # head_ref is attacker-controllable (fork branch names); pass via env, - # never interpolate into the script, to avoid template injection. - env: - HEAD_REF: ${{ github.head_ref }} - run: pnpm validate:pr --branch "$HEAD_REF" diff --git a/package.json b/package.json index ec0db08c..543c2040 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,6 @@ "improvement-loop:dry": "tsx scripts/agents/run-improvement-loop.ts --dry-run --verbose", "improvement-loop:verbose": "tsx scripts/agents/run-improvement-loop.ts --verbose", "check:event-types": "tsx scripts/check-event-type-parity.ts", - "validate:pr": "tsx scripts/validate-pr-description.ts", "test:behavioral": "tsx scripts/agents/test-behavioral-contracts.ts", "test:behavioral:variance": "tsx scripts/agents/test-behavioral-contracts.ts --variance-only", "check:lint": "oxlint -c oxlint.json src/", diff --git a/scripts/__tests__/validate-pr-description.test.ts b/scripts/__tests__/validate-pr-description.test.ts deleted file mode 100644 index 24bed381..00000000 --- a/scripts/__tests__/validate-pr-description.test.ts +++ /dev/null @@ -1,64 +0,0 @@ -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import { describe, expect, it } from "vitest"; -import { validatePrDescription } from "../validate-pr-description.js"; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const REPO_ROOT = path.resolve(__dirname, "../.."); - -describe("validatePrDescription (spec claims)", () => { - it("passes for a valid spec_claim_id reference", async () => { - const { failures } = await validatePrDescription( - "test/spec-fixture-valid", - REPO_ROOT - ); - expect(failures).toEqual([]); - }); - - it("fails when spec_claim_id does not exist in spec frontmatter", async () => { - const { failures } = await validatePrDescription( - "test/spec-fixture-missing-claim", - REPO_ROOT - ); - expect( - failures.some((f) => f.code === "unresolved-spec-claim-ref") - ).toBe(true); - }); - - it("fails when behavioral spec claim uses insufficient evidence_type", async () => { - const { failures } = await validatePrDescription( - "test/spec-fixture-behavioral-evidence", - REPO_ROOT - ); - expect( - failures.some((f) => f.code === "behavioral-claim-insufficient-evidence") - ).toBe(true); - }); - - it("fails when a claim keeps both migrated and legacy claim refs", async () => { - const { failures } = await validatePrDescription( - "test/spec-fixture-dual-claim-ref", - REPO_ROOT - ); - expect(failures.some((f) => f.code === "schema-violation")).toBe(true); - expect(failures.map((f) => f.message).join("\n")).toContain( - "only one claim reference field" - ); - }); - - it("fails when spec_claim_id references a spec omitted from top-level specs", async () => { - const { failures } = await validatePrDescription( - "test/spec-fixture-missing-spec-list", - REPO_ROOT - ); - expect(failures.some((f) => f.code === "spec-claim-not-listed")).toBe(true); - }); - - it("fails when a top-level specs entry does not exist in .specs frontmatter", async () => { - const { failures } = await validatePrDescription( - "test/spec-fixture-unknown-spec", - REPO_ROOT - ); - expect(failures.some((f) => f.code === "unknown-spec")).toBe(true); - }); -}); diff --git a/scripts/validate-pr-description.ts b/scripts/validate-pr-description.ts deleted file mode 100644 index 4a6d75f9..00000000 --- a/scripts/validate-pr-description.ts +++ /dev/null @@ -1,410 +0,0 @@ -#!/usr/bin/env tsx -/** - * Deterministic validator for PR description JSON files. - * - * Checks (in order): - * 1. prs/.json exists - * 2. Valid JSON conforming to PR description schema (via Zod) - * 3. Referenced spec claims resolve against .specs/ markdown frontmatter - * 4. Legacy ADR claim references resolve during migration (deprecated) - * 5. Behavioral claims require agentic_test or human_attestation evidence - * 6. human_attestation claims require attestation block - * 7. agentic_test claims require evidence_path - * - * Usage: - * pnpm validate:pr --branch feat/my-feature - */ - -import { promises as fsp } from "node:fs"; -import path from "node:path"; -import process from "node:process"; -import { fileURLToPath } from "node:url"; -import { z } from "zod"; -import { loadSpecClaimIndex, parseSpecClaimRef } from "./lib/spec-claims.js"; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const REPO_ROOT = path.resolve(__dirname, ".."); - -const ClaimSchema = z - .object({ - id: z.string().min(1), - spec_claim_id: z.string().min(1).optional(), - adr_claim_id: z.string().min(1).optional(), - statement: z.string().min(1), - evidence_type: z.enum([ - "implementation", - "unit_test", - "integration_test", - "agentic_test", - "human_attestation", - "deterministic_check", - ]), - evidence_path: z.string().nullable(), - evidence_description: z.string().nullable().optional(), - }) - .superRefine((claim, ctx) => { - if (!claim.spec_claim_id && !claim.adr_claim_id) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "Each claim must include spec_claim_id or adr_claim_id (legacy)", - path: ["spec_claim_id"], - }); - } - if (claim.spec_claim_id && claim.adr_claim_id) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: - "Each claim must include only one claim reference field: spec_claim_id or adr_claim_id", - path: ["spec_claim_id"], - }); - } - }); - -const AttestationSchema = z.object({ - attested_by: z.string().min(1), - timestamp: z.string().min(1), - note: z.string().optional(), -}); - -const PRDescriptionSchema = z.object({ - branch: z.string().min(1), - specs: z.array(z.string()).optional().default([]), - adrs: z.array(z.string()).optional().default([]), - summary: z.string().min(1), - claims: z.array(ClaimSchema).min(1), - attestation: AttestationSchema.nullable().optional(), -}); - -const AdrClaimSchema = z.object({ - id: z.string().min(1), - statement: z.string().min(1), - type: z.enum(["implementation", "behavioral", "governance", "performance"]), - behavioral: z.boolean(), - required_evidence: z.string().optional(), -}); - -const AdrSchema = z.object({ - id: z.string().min(1), - title: z.string().min(1), - status: z.enum(["proposed", "accepted", "rejected", "superseded"]), - date: z.string().min(1), - claims: z.array(AdrClaimSchema).min(1), -}); - -function branchToFilename(branch: string): string { - return branch.replace(/\//g, "-") + ".json"; -} - -function argValue(flag: string): string | null { - const i = process.argv.indexOf(flag); - if (i < 0 || i + 1 >= process.argv.length) return null; - return process.argv[i + 1]; -} - -interface Failure { - code: string; - message: string; -} - -interface Warning { - code: string; - message: string; -} - -async function readJson(filePath: string): Promise { - const content = await fsp.readFile(filePath, "utf8"); - return JSON.parse(content); -} - -async function findAdrFile(adrId: string): Promise { - const bases = [ - path.join(REPO_ROOT, "docs/decisions/archive/adr"), - path.join(REPO_ROOT, "docs/decisions/archive"), - path.join(REPO_ROOT, ".adr"), - ]; - const dirs = ["staging", "accepted", "rejected", "superseded", "retired"]; - - for (const base of bases) { - for (const dir of dirs) { - const candidate = path.join(base, dir, `${adrId}.json`); - try { - await fsp.access(candidate); - return candidate; - } catch { - // not in this dir - } - } - const flatCandidate = path.join(base, `${adrId}.json`); - try { - await fsp.access(flatCandidate); - return flatCandidate; - } catch { - // not found - } - } - return null; -} - -function claimRef(claim: z.infer): string { - return claim.spec_claim_id ?? claim.adr_claim_id ?? "__none__"; -} - -function isBehavioralClaim( - claim: z.infer, - specIndex: Map, - adrClaims: Map -): boolean { - if (claim.spec_claim_id && claim.spec_claim_id !== "__none__") { - return specIndex.get(claim.spec_claim_id)?.behavioral ?? false; - } - if (claim.adr_claim_id && claim.adr_claim_id !== "__none__") { - for (const claims of adrClaims.values()) { - const adrClaim = claims.find((c) => c.id === claim.adr_claim_id); - if (adrClaim?.behavioral) return true; - } - } - return false; -} - -export async function validatePrDescription( - branch: string, - repoRoot: string = REPO_ROOT -): Promise<{ failures: Failure[]; warnings: Warning[] }> { - const failures: Failure[] = []; - const warnings: Warning[] = []; - const filename = branchToFilename(branch); - const prPath = path.join(repoRoot, "prs", filename); - - try { - await fsp.access(prPath); - } catch { - failures.push({ - code: "missing-pr-description", - message: `PR description not found: prs/${filename}. Every PR targeting main must include a machine-readable PR description.`, - }); - return { failures, warnings }; - } - - let raw: unknown; - try { - raw = await readJson(prPath); - } catch (err) { - failures.push({ - code: "invalid-json", - message: `prs/${filename} is not valid JSON: ${String(err)}`, - }); - return { failures, warnings }; - } - - const parsed = PRDescriptionSchema.safeParse(raw); - if (!parsed.success) { - for (const issue of parsed.error.issues) { - failures.push({ - code: "schema-violation", - message: `prs/${filename}: ${issue.path.join(".")} — ${issue.message}`, - }); - } - return { failures, warnings }; - } - - const pr = parsed.data; - - if (pr.branch !== branch) { - failures.push({ - code: "branch-mismatch", - message: `prs/${filename}: branch field "${pr.branch}" does not match actual branch "${branch}"`, - }); - } - - if (pr.adrs.length > 0) { - warnings.push({ - code: "deprecated-adrs-field", - message: `prs/${filename} uses deprecated "adrs" field. Migrate to "specs" and spec_claim_id references.`, - }); - } - - const specClaimIndex = await loadSpecClaimIndex(path.join(repoRoot, ".specs")); - const specBehavioralIndex = new Map(); - const knownSpecIds = new Set(); - for (const [ref, entry] of specClaimIndex) { - specBehavioralIndex.set(ref, { behavioral: entry.behavioral }); - knownSpecIds.add(entry.specId); - } - - // Validate top-level specs entries exist, independent of per-claim references. - // Mirrors the legacy ADR existence check so a listed-but-unreferenced spec - // (e.g. a typo alongside a "__none__" cleanup claim) cannot pass validation - // and then surface in the rendered PR body. - for (const specId of pr.specs) { - if (!knownSpecIds.has(specId)) { - failures.push({ - code: "unknown-spec", - message: `Top-level specs entry "${specId}" does not match any spec_id with claims in .specs/ markdown frontmatter. Remove it or correct the spec_id.`, - }); - } - } - - const adrClaims = new Map(); - for (const adrId of pr.adrs) { - const adrPath = await findAdrFile(adrId); - if (!adrPath) { - failures.push({ - code: "missing-adr", - message: `Referenced ADR "${adrId}" has no corresponding JSON file in docs/decisions/archive/ or legacy .adr/. Migrate to spec_claim_id references.`, - }); - continue; - } - - let adrRaw: unknown; - try { - adrRaw = await readJson(adrPath); - } catch (err) { - failures.push({ - code: "invalid-adr-json", - message: `${adrPath} is not valid JSON: ${String(err)}`, - }); - continue; - } - - const adrParsed = AdrSchema.safeParse(adrRaw); - if (!adrParsed.success) { - failures.push({ - code: "invalid-adr-schema", - message: `${adrPath} does not conform to adr-v1 schema: ${adrParsed.error.issues.map((i) => i.message).join("; ")}`, - }); - continue; - } - - adrClaims.set( - adrId, - adrParsed.data.claims.map((c) => ({ id: c.id, behavioral: c.behavioral })) - ); - } - - for (const claim of pr.claims) { - const ref = claimRef(claim); - - if (claim.adr_claim_id && !claim.spec_claim_id) { - warnings.push({ - code: "deprecated-adr-claim-id", - message: `Claim "${claim.id}" uses deprecated adr_claim_id "${claim.adr_claim_id}". Migrate to spec_claim_id (spec_id:claim_id).`, - }); - } - - if (ref === "__none__") continue; - - if (claim.spec_claim_id) { - if (claim.spec_claim_id !== "__none__") { - const parsedRef = parseSpecClaimRef(claim.spec_claim_id); - if (!parsedRef) { - failures.push({ - code: "invalid-spec-claim-ref", - message: `Claim "${claim.id}" spec_claim_id "${claim.spec_claim_id}" must be spec_id:claim_id (e.g. SPEC-CONTROL-PLANE:c1) or "__none__".`, - }); - continue; - } - - if (!specClaimIndex.has(claim.spec_claim_id)) { - failures.push({ - code: "unresolved-spec-claim-ref", - message: `Claim "${claim.id}" references spec_claim_id "${claim.spec_claim_id}" which does not exist in any .specs/ markdown frontmatter.`, - }); - } - if (!pr.specs.includes(parsedRef.specId)) { - failures.push({ - code: "spec-claim-not-listed", - message: `Claim "${claim.id}" references spec "${parsedRef.specId}" but that spec is not listed in the top-level specs array.`, - }); - } - } - } else if (claim.adr_claim_id) { - let found = false; - for (const claims of adrClaims.values()) { - if (claims.some((c) => c.id === claim.adr_claim_id)) { - found = true; - break; - } - } - if (!found) { - failures.push({ - code: "unresolved-claim-ref", - message: `Claim "${claim.id}" references adr_claim_id "${claim.adr_claim_id}" which does not exist in any referenced ADR. Migrate to spec_claim_id or use "__none__".`, - }); - } - } - - if (isBehavioralClaim(claim, specBehavioralIndex, adrClaims)) { - if ( - claim.evidence_type !== "agentic_test" && - claim.evidence_type !== "human_attestation" - ) { - failures.push({ - code: "behavioral-claim-insufficient-evidence", - message: `Claim "${claim.id}" implements behavioral spec/ADR claim "${ref}" but evidence_type is "${claim.evidence_type}". Behavioral claims require "agentic_test" or "human_attestation".`, - }); - } - } - } - - const hasAttestationClaim = pr.claims.some( - (c) => c.evidence_type === "human_attestation" - ); - if (hasAttestationClaim && !pr.attestation) { - failures.push({ - code: "missing-attestation-block", - message: `One or more claims use evidence_type "human_attestation" but the PR description has no attestation block`, - }); - } - - for (const claim of pr.claims) { - if (claim.evidence_type === "agentic_test" && !claim.evidence_path) { - failures.push({ - code: "missing-evidence-path", - message: `Claim "${claim.id}" has evidence_type "agentic_test" but no evidence_path`, - }); - } - } - - return { failures, warnings }; -} - -function report(failures: Failure[], warnings: Warning[]): void { - for (const w of warnings) { - console.log(` [warn:${w.code}] ${w.message}`); - } - - if (failures.length === 0) { - console.log("validate:pr passed"); - return; - } - - console.log("validate:pr failed"); - for (const f of failures) { - console.log(` [${f.code}] ${f.message}`); - } - process.exitCode = 1; -} - -async function main(): Promise { - const branch = argValue("--branch"); - if (!branch) { - console.error("Usage: pnpm validate:pr --branch "); - process.exitCode = 1; - return; - } - - const { failures, warnings } = await validatePrDescription(branch); - report(failures, warnings); -} - -const isMain = - process.argv[1] && - (process.argv[1].endsWith("validate-pr-description.ts") || - process.argv[1].endsWith("validate-pr-description.js")); - -if (isMain) { - void main().catch((err) => { - console.error("validate:pr error:", err); - process.exitCode = 1; - }); -} diff --git a/src/__tests__/architecture.test.ts b/src/__tests__/architecture.test.ts index 315b5b53..bbb9caea 100644 --- a/src/__tests__/architecture.test.ts +++ b/src/__tests__/architecture.test.ts @@ -34,6 +34,7 @@ describe('architectural constraints', () => { 'protocol/handler.ts', // thoughtbox-kcv: needs ProtocolStorage interface 'auth/api-key.ts', // uses SupabaseClient for API key validation 'auth/static-workspace.ts', // upserts workspace rows for static key auth + 'branch/handlers.ts', // tb-branch handlers: need a BranchStorage interface ]); const allowed = (file: string): boolean => { diff --git a/src/__tests__/intelligence-pipeline.test.ts b/src/__tests__/intelligence-pipeline.test.ts index a4ae978a..47cc04ae 100644 --- a/src/__tests__/intelligence-pipeline.test.ts +++ b/src/__tests__/intelligence-pipeline.test.ts @@ -105,6 +105,21 @@ describe('Intelligence Pipeline', () => { .single(); expect(error).toBeNull(); testSessionId = data!.id; + + // The thought_processing queue is shared and persists across test files. + // Drain stale messages left by earlier files so these assertions observe + // only the message each test enqueues. + const { data: stale } = await client.rpc('pgmq_read_queue', { + queue_name: 'thought_processing', + vt: 0, + qty: 1000, + }); + for (const msg of (stale as Array<{ msg_id: number }> | null) ?? []) { + await client.rpc('pgmq_archive_queue_message', { + queue_name: 'thought_processing', + msg_id: msg.msg_id, + }); + } }); it('thought insert enqueues to thought_processing', async () => { diff --git a/src/otel/__tests__/otel-storage.test.ts b/src/otel/__tests__/otel-storage.test.ts index 138dce7a..627f1195 100644 --- a/src/otel/__tests__/otel-storage.test.ts +++ b/src/otel/__tests__/otel-storage.test.ts @@ -16,6 +16,31 @@ import { ensureTestWorkspace, TEST_WORKSPACE_ID, } from '../../__tests__/supabase-test-helpers.js'; +import { randomUUID } from 'node:crypto'; + +// otel queries resolve a thoughtbox session UUID to its otel session id(s) via +// the runs binding table. Seed a session + run so the query path can resolve a +// UUID to the otel session id carried on ingested events. +async function seedSessionWithOtel(otelSessionId: string): Promise { + const client = createServiceClient(); + const { data: session, error: sessionError } = await client + .from('sessions') + .insert({ workspace_id: TEST_WORKSPACE_ID, title: `otel-test ${otelSessionId}` }) + .select('id') + .single(); + if (sessionError || !session) { + throw new Error(`Failed to seed session: ${sessionError?.message}`); + } + const { error: runError } = await client.from('runs').insert({ + workspace_id: TEST_WORKSPACE_ID, + session_id: session.id, + otel_session_id: otelSessionId, + }); + if (runError) { + throw new Error(`Failed to seed run: ${runError.message}`); + } + return session.id as string; +} describe('OtelEventStorage', () => { let storage: OtelEventStorage; @@ -169,12 +194,13 @@ describe('OtelEventStorage', () => { await storage.ingest(parseLogsPayload(logsPayload, TEST_WORKSPACE_ID)); + const sessionId = await seedSessionWithOtel('timeline-session'); const timeline = await storage.querySessionTimeline( TEST_WORKSPACE_ID, - 'timeline-session', + sessionId, ); - expect(timeline.session_id).toBe('timeline-session'); + expect(timeline.session_id).toBe(sessionId); expect(timeline.count).toBe(2); expect(timeline.events[0].event_name).toBe('claude_code.user_prompt'); expect(timeline.events[1].event_name).toBe('claude_code.tool_result'); @@ -202,9 +228,10 @@ describe('OtelEventStorage', () => { await storage.ingest(parseLogsPayload(logsPayload, TEST_WORKSPACE_ID)); + const sessionId = await seedSessionWithOtel('limit-session'); const timeline = await storage.querySessionTimeline( TEST_WORKSPACE_ID, - 'limit-session', + sessionId, { limit: 2 }, ); @@ -242,8 +269,9 @@ describe('OtelEventStorage', () => { await storage.ingest(parseMetricsPayload(payload, TEST_WORKSPACE_ID)); - const cost = await storage.querySessionCost(TEST_WORKSPACE_ID, 'cost-session'); - expect(cost.session_id).toBe('cost-session'); + const sessionId = await seedSessionWithOtel('cost-session'); + const cost = await storage.querySessionCost(TEST_WORKSPACE_ID, sessionId); + expect(cost.session_id).toBe(sessionId); expect(cost.total).toBeCloseTo(0.035); expect(cost.costs).toHaveLength(2); @@ -260,7 +288,7 @@ describe('OtelEventStorage', () => { it('returns zero cost for session with no cost events', async ({ skip }) => { if (!available) skip(); - const cost = await storage.querySessionCost(TEST_WORKSPACE_ID, 'nonexistent-session'); + const cost = await storage.querySessionCost(TEST_WORKSPACE_ID, randomUUID()); expect(cost.total).toBe(0); expect(cost.costs).toHaveLength(0); }); diff --git a/src/persistence/__tests__/supabase-storage.test.ts b/src/persistence/__tests__/supabase-storage.test.ts index f8a23404..a5261b4c 100644 --- a/src/persistence/__tests__/supabase-storage.test.ts +++ b/src/persistence/__tests__/supabase-storage.test.ts @@ -17,6 +17,8 @@ import { isSupabaseAvailable, getTestSupabaseConfig, truncateAllTables, + ensureTestWorkspace, + TEST_WORKSPACE_ID, } from '../../__tests__/supabase-test-helpers.js'; import type { ThoughtData } from '../types.js'; @@ -43,9 +45,10 @@ describe('SupabaseStorage (ThoughtboxStorage)', () => { beforeEach(async () => { if (!available) return; await truncateAllTables(); + await ensureTestWorkspace(); storage = new SupabaseStorage({ ...getTestSupabaseConfig(), - workspaceId: '11111111-1111-1111-1111-111111111111', + workspaceId: TEST_WORKSPACE_ID, }); await storage.initialize(); }); diff --git a/supabase/config.toml b/supabase/config.toml index 974d2541..93394f9f 100644 --- a/supabase/config.toml +++ b/supabase/config.toml @@ -277,6 +277,14 @@ verify_jwt = false import_map = "./functions/process-thought-queue/deno.json" entrypoint = "./functions/process-thought-queue/index.ts" +# tb-branch authenticates via its own signed branch token (?token=), so the +# Supabase edge gateway must not require an Authorization JWT header. +[functions.tb-branch] +enabled = true +verify_jwt = false +import_map = "./functions/tb-branch/deno.json" +entrypoint = "./functions/tb-branch/index.ts" + [analytics] enabled = true port = 54327 diff --git a/supabase/migrations/20260408033928_add_hub_tables_vectors_pgmq_realtime.sql b/supabase/migrations/20260408033928_add_hub_tables_vectors_pgmq_realtime.sql index 6cb91f91..2e30e28a 100644 --- a/supabase/migrations/20260408033928_add_hub_tables_vectors_pgmq_realtime.sql +++ b/supabase/migrations/20260408033928_add_hub_tables_vectors_pgmq_realtime.sql @@ -4,7 +4,7 @@ -- 1. Enable extensions required for queue processing and scheduled worker invocation. CREATE EXTENSION IF NOT EXISTS pgmq; CREATE EXTENSION IF NOT EXISTS pg_cron; -CREATE EXTENSION IF NOT EXISTS pg_net; +CREATE EXTENSION IF NOT EXISTS pg_net WITH SCHEMA extensions; -- 2. pgmq queues for intelligence pipeline. SELECT FROM pgmq.create('thought_processing');