diff --git a/.agents/skills/access/SKILL.md b/.agents/skills/access/SKILL.md new file mode 100644 index 000000000..74e603141 --- /dev/null +++ b/.agents/skills/access/SKILL.md @@ -0,0 +1,13 @@ +--- +name: access +description: Verify Database authentication, authorization, ownership, tenancy, grants, RLS, administrative boundaries, and conservative denial behavior. Use for auth changes, private routes, owner scope, cross-tenant risk, or permissions. +--- + +# Access + +1. Map actor, credential, resource owner, tenancy boundary, route guard, database policy, and service-role use. +2. Test unauthenticated, wrong-owner, non-admin, stale-session, missing-record, and cross-tenant paths. +3. Inspect RLS, grants, security-definer functions, public API keys, and server/client trust boundaries. +4. Run focused local access-model, owner-scope, and private-route tests. +5. Keep staging cross-tenant tests and live Supabase checks approval-gated. +6. Report the authorization decision matrix, denial proof, bypass risk, and unverified live boundary. diff --git a/.agents/skills/access/agents/openai.yaml b/.agents/skills/access/agents/openai.yaml new file mode 100644 index 000000000..77086bf78 --- /dev/null +++ b/.agents/skills/access/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Access" + short_description: "Verify Database auth and tenancy boundaries" + default_prompt: "Use $access to verify authentication, authorization, ownership, grants, RLS, and cross-tenant denial behavior." diff --git a/.agents/skills/api/SKILL.md b/.agents/skills/api/SKILL.md new file mode 100644 index 000000000..c9ea7d018 --- /dev/null +++ b/.agents/skills/api/SKILL.md @@ -0,0 +1,13 @@ +--- +name: api +description: Review Database route and function contracts for validation, authentication, authorization, errors, retries, idempotency, compatibility, and safe provider boundaries. Use for API route, edge function, webhook, or public contract work. +--- + +# API + +1. Identify callers, request schema, authentication, authorization, response contract, status codes, and side effects. +2. Test invalid, empty, oversized, duplicate, stale, unauthorized, timeout, retry, and dependency-failure cases. +3. Check stable error envelopes, cache behavior, rate assumptions, idempotency, and backwards compatibility. +4. Use route-unit tests and mocks locally; never send live requests without approval. +5. Verify secrets and privileged clients remain server-only. +6. Report contract changes, compatibility risk, focused proofs, and untested live integrations. diff --git a/.agents/skills/api/agents/openai.yaml b/.agents/skills/api/agents/openai.yaml new file mode 100644 index 000000000..6efeebe86 --- /dev/null +++ b/.agents/skills/api/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "API" + short_description: "Review Database API contracts and failure paths" + default_prompt: "Use $api to verify this Database route contract, validation, auth, errors, retries, and compatibility." diff --git a/.agents/skills/audit/SKILL.md b/.agents/skills/audit/SKILL.md new file mode 100644 index 000000000..c483fb934 --- /dev/null +++ b/.agents/skills/audit/SKILL.md @@ -0,0 +1,13 @@ +--- +name: audit +description: Perform a deep evidence-backed Database repository audit and produce severity-ranked, reproducible findings without mutating code unless fixes are requested. Use for repo-wide audits, risk assessments, or exhaustive review requests. +--- + +# Audit + +1. Define scope, read `docs/codex-review-protocol.md`, and inspect the review ledger. +2. Inventory architecture, trust boundaries, changed areas, tests, and documented controls. +3. Trace realistic correctness, security, privacy, clinical, data-loss, and reliability failures. +4. Prove findings with exact files, lines, triggers, impact, and focused checks. +5. Rank findings by severity and distinguish confirmed defects from residual risk. +6. Remain read-only unless the user explicitly asks for fixes; keep provider checks approval-gated. diff --git a/.agents/skills/audit/agents/openai.yaml b/.agents/skills/audit/agents/openai.yaml new file mode 100644 index 000000000..f37bcca9d --- /dev/null +++ b/.agents/skills/audit/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Audit" + short_description: "Audit the Database repository with concrete evidence" + default_prompt: "Use $audit to perform an evidence-backed Database audit with severity-ranked reproducible findings." diff --git a/.agents/skills/catalog.json b/.agents/skills/catalog.json new file mode 100644 index 000000000..21b4ce308 --- /dev/null +++ b/.agents/skills/catalog.json @@ -0,0 +1,31 @@ +{ + "version": 1, + "categories": [ + { + "name": "Everyday", + "skills": ["skills", "plan", "run", "test", "fix", "review", "task", "handover", "health", "audit", "export"] + }, + { + "name": "Clinical and app", + "skills": ["clinical", "ui", "rag", "sources", "ingest", "reindex", "documents"] + }, + { + "name": "Database, API, and security", + "skills": ["migrate", "drift", "access", "security", "privacy", "api", "data"] + }, + { + "name": "Operations and release", + "skills": ["release", "deploy", "operations", "incident", "recovery", "performance", "dependencies"] + } + ], + "aliases": { + "workflows": "skills", + "database-flightplan": "plan", + "verify-triage-fix": "fix", + "clinical-change-proof": "clinical", + "live-design-sweep": "ui", + "rag-change-lab": "rag", + "operator-closeout": "operations", + "session-lifecycle": "task" + } +} diff --git a/.agents/skills/clinical-change-proof/SKILL.md b/.agents/skills/clinical-change-proof/SKILL.md index d5634bd7b..c823211fc 100644 --- a/.agents/skills/clinical-change-proof/SKILL.md +++ b/.agents/skills/clinical-change-proof/SKILL.md @@ -1,20 +1,10 @@ --- name: clinical-change-proof -description: Build evidence for Database changes affecting clinical output, ingestion, retrieval, ranking, answers, citations, source rendering, privacy, owner scoping, document access, Supabase, or production behavior. Use for implementation, review, readiness, or handoff work that must satisfy clinical governance and conservative failure requirements. +description: Compatibility alias for the clinical skill. Use only when the user explicitly invokes the older clinical-change-proof name; otherwise use clinical for safety and governance evidence. --- -# Clinical Change Proof +# Clinical Change Proof Alias -1. Generate the scoped evidence plan: - `npm run workflow:clinical-proof -- --write-evidence` -2. Read only the relevant sections of `.github/pull_request_template.md`, `docs/clinical-governance.md`, and the domain runbook selected by the change. -3. Trace the full behavior boundary: input, authorization/owner scope, retrieval or write path, output contract, source evidence, logs, and failure fallback. -4. Prove locally that: - - private or service-role data remains server-only and fail-closed; - - source-backed claims and conservative unknown/outdated behavior are preserved; - - demo data remains distinct from clinical sources; - - rollback or feature-disable behavior is documented; - - affected behavior has focused regression coverage. -5. Run the local checks in the plan, narrowest first. The offline RAG contract is mandatory when retrieval or answer behavior is involved. -6. Treat every command in the plan's `approvalRequired` list as an independent confirmation-required action, including retrieval, deployment, release, remote Git, hosted CI, Supabase, OpenAI, and production-readiness commands. Never bundle approvals, infer approval from another action, or execute one indirectly through a local gate or wrapper. -7. Complete the Clinical Governance Preflight and report evidence, gaps, rollback, and any SaMD implication. +1. Read and follow `.agents/skills/clinical/SKILL.md`. +2. Use `clinical` in recommendations and new documentation. +3. Do not maintain a second copy of the clinical proof procedure here. diff --git a/.agents/skills/clinical-change-proof/agents/openai.yaml b/.agents/skills/clinical-change-proof/agents/openai.yaml index b526d3734..b7dca1d30 100644 --- a/.agents/skills/clinical-change-proof/agents/openai.yaml +++ b/.agents/skills/clinical-change-proof/agents/openai.yaml @@ -1,4 +1,6 @@ interface: - display_name: "Clinical Change Proof" - short_description: "Build clinical governance evidence" - default_prompt: "Use $clinical-change-proof to assess this clinical change and produce the required local evidence, governance checks, and provider approval gates." + display_name: "Clinical Alias" + short_description: "Compatibility alias for Database clinical proof" + default_prompt: "Use $clinical-change-proof only as a compatibility alias for $clinical." +policy: + allow_implicit_invocation: false diff --git a/.agents/skills/clinical/SKILL.md b/.agents/skills/clinical/SKILL.md new file mode 100644 index 000000000..5b0601f54 --- /dev/null +++ b/.agents/skills/clinical/SKILL.md @@ -0,0 +1,13 @@ +--- +name: clinical +description: Assemble clinical safety, privacy, source-governance, rollback, and production-readiness evidence for Database changes. Use for answer generation, retrieval, ingestion, clinical content, source rendering, or other patient-safety-sensitive behavior. +--- + +# Clinical + +1. Run `npm run workflow:clinical-proof -- --write-evidence` for the affected paths. +2. Trace the clinical claim path from source and retrieval through generation and rendering. +3. Check conservative failure behavior, ownership, privacy, citations, metadata, and rollback. +4. Add the smallest deterministic local safety proof and run relevant offline domain checks. +5. Complete the governance checklist required by `.github/pull_request_template.md` for handoff. +6. Keep live Supabase, OpenAI, production-readiness, and provider evaluations approval-gated. diff --git a/.agents/skills/clinical/agents/openai.yaml b/.agents/skills/clinical/agents/openai.yaml new file mode 100644 index 000000000..fe23dcdc9 --- /dev/null +++ b/.agents/skills/clinical/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Clinical" + short_description: "Prove clinical safety and source governance" + default_prompt: "Use $clinical to build local clinical safety, privacy, source, rollback, and verification evidence for this change." diff --git a/.agents/skills/data/SKILL.md b/.agents/skills/data/SKILL.md new file mode 100644 index 000000000..2b9e59099 --- /dev/null +++ b/.agents/skills/data/SKILL.md @@ -0,0 +1,13 @@ +--- +name: data +description: Validate Database imports, seeds, transformations, reconciliation, deduplication, integrity, provenance, and rollback controls. Use for dataset changes, bulk operations, registry content, or data migration logic. +--- + +# Data + +1. Identify source format, trust level, schema, identifiers, ownership, transformations, and destination constraints. +2. Test malformed, missing, duplicate, conflicting, oversized, partial, and rerun inputs with local fixtures. +3. Check deterministic normalization, idempotency, transactions, reconciliation, provenance, and rollback. +4. Measure counts and invariants before and after local dry runs without exposing sensitive values. +5. Treat live imports, seeds, backfills, database writes, and production exports as approval-required. +6. Report accepted/rejected records, integrity proof, rollback, and remaining operator action. diff --git a/.agents/skills/data/agents/openai.yaml b/.agents/skills/data/agents/openai.yaml new file mode 100644 index 000000000..5fa004277 --- /dev/null +++ b/.agents/skills/data/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Data" + short_description: "Validate Database imports and data integrity" + default_prompt: "Use $data to verify this Database import, seed, transformation, or reconciliation workflow safely." diff --git a/.agents/skills/database-flightplan/SKILL.md b/.agents/skills/database-flightplan/SKILL.md index 8fd01a935..e3e578fef 100644 --- a/.agents/skills/database-flightplan/SKILL.md +++ b/.agents/skills/database-flightplan/SKILL.md @@ -1,18 +1,10 @@ --- name: database-flightplan -description: Plan safe, risk-scoped work in the Database repository by inspecting the current diff, selecting the smallest local verification ladder, and separating provider-backed checks into explicit approval gates. Use before non-trivial source, config, test, UI, database, retrieval, clinical, dependency, CI, or deployment changes, or when the user asks what checks a change needs. +description: Compatibility alias for the plan skill. Use only when the user explicitly invokes the older database-flightplan name; otherwise use plan for risk-scoped offline verification planning. --- -# Database Flightplan +# Database Flightplan Alias -1. Run the repository task-start preflight if it has not run for the current task. Inspect branch and full Git status; preserve unrelated work. -2. Generate the plan: - `npm run workflow:flightplan -- --write-evidence` - Use `--files pathA,pathB` when planning proposed paths before editing. -3. Confirm the risk classes match the actual behavior, not only filenames. Add a focused check when the change crosses an unclassified boundary. -4. Execute the narrowest relevant check first. Use `--run` only when running the full printed local/offline sequence is proportionate. -5. Never execute anything listed under `approvalRequired` without explicit user confirmation. Treat indirect provider calls the same way. -6. After fixes, run `npm run verify:pr-local` when the change is ready for handoff. Add UI or domain gates only when the plan selects them. -7. Report changed files, checks and results, checks not run, approval gates, branch state, and residual risk. - -Keep the planner authoritative for deterministic selection. Do not reproduce its path tables in the skill. +1. Read and follow `.agents/skills/plan/SKILL.md`. +2. Use `plan` in recommendations and new documentation. +3. Do not maintain a second copy of the planning procedure here. diff --git a/.agents/skills/database-flightplan/agents/openai.yaml b/.agents/skills/database-flightplan/agents/openai.yaml index f2f201b90..8c182904a 100644 --- a/.agents/skills/database-flightplan/agents/openai.yaml +++ b/.agents/skills/database-flightplan/agents/openai.yaml @@ -1,4 +1,6 @@ interface: - display_name: "Database Flightplan" - short_description: "Plan safe repo changes and verification" - default_prompt: "Use $database-flightplan to inspect the current Database repo change and produce a risk-scoped local verification and approval plan." + display_name: "Plan Alias" + short_description: "Compatibility alias for Database planning" + default_prompt: "Use $database-flightplan only as a compatibility alias for $plan." +policy: + allow_implicit_invocation: false diff --git a/.agents/skills/database-skills.md b/.agents/skills/database-skills.md index 2b6af658a..81ae496e2 100644 --- a/.agents/skills/database-skills.md +++ b/.agents/skills/database-skills.md @@ -1,20 +1,6 @@ # Database skills catalog -This repository has the following Database-specific skills. +Run `npm run skills` to render the current categories from `catalog.json` and explanations from each canonical skill's frontmatter. +Run `npm run check:skills` to prove that every canonical skill and compatibility alias has valid local metadata. -## Workflow and repo orchestration skills - -- `database-flightplan` — create a risk-scoped verification plan before non-trivial work. -- `verify-triage-fix` — diagnose and fix local verification failures with minimal repros. -- `clinical-change-proof` — assemble clinical/medical safety and production-readiness evidence. -- `live-design-sweep` — inspect the running app across routes, breakpoints, accessibility modes, and interactions. -- `rag-change-lab` — validate retrieval/ranking/grounding changes with focused offline checks. -- `operator-closeout` — turn operator/provisioning/debt tasks into an ordered, approval-gated execution plan. -- `session-lifecycle` — manage safe task start/handoff/cleanup and merge-proof transitions. -- `workflows` — this new skill: indexed reference describing each Database workflow skill and when to use it. - -## Notes - -- These are the skills currently present in `.agents/skills` for this Database worktree. -- Use these skills as lightweight orchestration and evidence-first planning helpers. -- The first step for most non-trivial tasks remains `start-codex-task.ps1` (as required by AGENTS instructions). +The `skills` skill uses this generated view, explains each unique skill in the chat, and recommends the smallest useful set for the current request. Compatibility aliases remain callable but are not counted or displayed as separate skills. diff --git a/.agents/skills/dependencies/SKILL.md b/.agents/skills/dependencies/SKILL.md new file mode 100644 index 000000000..21c0c48af --- /dev/null +++ b/.agents/skills/dependencies/SKILL.md @@ -0,0 +1,13 @@ +--- +name: dependencies +description: Maintain Database dependencies safely by checking compatible stable versions, release notes, peer and engine constraints, old API usage, lockfile integrity, security, and focused verification. Use for dependency updates or reviews. +--- + +# Dependencies + +1. Inspect branch, status, Node/npm versions, `package.json`, lockfile, npm configuration, and active repo processes. +2. Compare direct dependencies with stable compatible releases and group risky ecosystems coherently. +3. Read migration notes for framework, runtime, build, test, lint, database, and security-sensitive updates. +4. Search for old APIs and make only the smallest compatibility changes. +5. When an update is requested, regenerate the existing npm lockfile and verify install integrity, focused behavior, then broader local gates. Keep review-only requests read-only. +6. Do not use force flags, switch package managers, publish, deploy, or call provider APIs without approval. diff --git a/.agents/skills/dependencies/agents/openai.yaml b/.agents/skills/dependencies/agents/openai.yaml new file mode 100644 index 000000000..2695c35e4 --- /dev/null +++ b/.agents/skills/dependencies/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Dependencies" + short_description: "Maintain Database dependencies safely" + default_prompt: "Use $dependencies to review or update Database dependencies with compatibility, lockfile, and verification discipline." diff --git a/.agents/skills/deploy/SKILL.md b/.agents/skills/deploy/SKILL.md new file mode 100644 index 000000000..c3ab60038 --- /dev/null +++ b/.agents/skills/deploy/SKILL.md @@ -0,0 +1,13 @@ +--- +name: deploy +description: Review Database deployment configuration, environment parity, build and boot behavior, workers, migrations, health checks, and rollback readiness. Use for deployment changes, hosting configuration, runtime failures, or production setup. +--- + +# Deploy + +1. Map web, worker, database, build, start, health, environment, migration, and rollback responsibilities. +2. Inspect deployment manifests and environment contracts without printing secrets. +3. Run static config, offline build/boot, and environment-parity checks selected by risk. +4. Check ordering, zero-downtime compatibility, health timeouts, restart behavior, and rollback. +5. Treat hosted deployments, provider dashboards, production health, secrets, and live databases as approval-required. +6. Report configuration proof, blockers, rollback readiness, and exact gated deployment action. diff --git a/.agents/skills/deploy/agents/openai.yaml b/.agents/skills/deploy/agents/openai.yaml new file mode 100644 index 000000000..7a6190511 --- /dev/null +++ b/.agents/skills/deploy/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Deploy" + short_description: "Review Database deployment and rollback readiness" + default_prompt: "Use $deploy to verify Database deployment configuration, environment parity, boot behavior, and rollback readiness." diff --git a/.agents/skills/documents/SKILL.md b/.agents/skills/documents/SKILL.md new file mode 100644 index 000000000..169107a81 --- /dev/null +++ b/.agents/skills/documents/SKILL.md @@ -0,0 +1,13 @@ +--- +name: documents +description: Verify Database document access, storage, signed images, metadata, extraction, labels, downloads, and lifecycle behavior. Use for private documents, image loading, signed URLs, document routes, or storage defects. +--- + +# Documents + +1. Trace document identity, owner scope, metadata, storage key, route authorization, and rendered state. +2. Check private access, signed URL expiry, missing objects, image/page selection, downloads, deletion, and stale metadata. +3. Reproduce with local fixtures and focused route or component tests. +4. Verify no service-role secret or private object path reaches the client. +5. Treat live Supabase storage, production documents, and signed-URL calls as approval-required. +6. Report access proof, storage assumptions, failure behavior, and remaining live verification. diff --git a/.agents/skills/documents/agents/openai.yaml b/.agents/skills/documents/agents/openai.yaml new file mode 100644 index 000000000..fca3342f6 --- /dev/null +++ b/.agents/skills/documents/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Documents" + short_description: "Verify Database document and storage behavior" + default_prompt: "Use $documents to trace and verify this document access, storage, signed image, or metadata behavior." diff --git a/.agents/skills/drift/SKILL.md b/.agents/skills/drift/SKILL.md new file mode 100644 index 000000000..4546f3362 --- /dev/null +++ b/.agents/skills/drift/SKILL.md @@ -0,0 +1,13 @@ +--- +name: drift +description: Detect Database schema, migration, type, environment, and generated-artifact drift offline, while keeping live Supabase comparison approval-gated. Use when schema sources disagree or deployment drift is suspected. +--- + +# Drift + +1. Compare repository migrations, `supabase/schema.sql`, generated database types, manifests, and migration references. +2. Run static drift and consistency checks that do not contact a provider. +3. Distinguish committed schema drift, stale generated output, missing migration history, and environment-only mismatch. +4. Add a deterministic manifest or test when the mismatch can recur silently. +5. Ask before `npm run check:supabase-project`, live schema inspection, CLI linking, pulling, or diffing. +6. Report exact sources of truth, mismatch, remediation order, and remaining live comparison. diff --git a/.agents/skills/drift/agents/openai.yaml b/.agents/skills/drift/agents/openai.yaml new file mode 100644 index 000000000..72e2b2868 --- /dev/null +++ b/.agents/skills/drift/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Drift" + short_description: "Detect Database schema and migration drift" + default_prompt: "Use $drift to compare local Database schema sources and isolate drift before any approved live comparison." diff --git a/.agents/skills/export/SKILL.md b/.agents/skills/export/SKILL.md new file mode 100644 index 000000000..d16af726a --- /dev/null +++ b/.agents/skills/export/SKILL.md @@ -0,0 +1,13 @@ +--- +name: export +description: Create a review-safe Database codebase archive with a manifest while excluding secrets, dependencies, build output, caches, logs, and machine-local state. Use when the user asks to package or export the repository for review. +--- + +# Export + +1. Inspect tracked, untracked, ignored, sensitive, generated, and oversized paths without printing secrets. +2. Stage the archive outside the repository and include `EXPORT_MANIFEST.md`. +3. Include only review-relevant source, configuration, tests, and documentation. +4. Exclude `.env*`, credentials, dependencies, builds, caches, logs, browser artifacts, and local state. +5. Verify the archive opens, inspect its file list, and confirm its size before handoff. +6. Do not alter the worktree, commit, push, upload, or send the archive without explicit authorization. diff --git a/.agents/skills/export/agents/openai.yaml b/.agents/skills/export/agents/openai.yaml new file mode 100644 index 000000000..336c4097b --- /dev/null +++ b/.agents/skills/export/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Export" + short_description: "Create a safe review archive of the Database repo" + default_prompt: "Use $export to create a verified review-safe Database archive with sensitive and generated files excluded." diff --git a/.agents/skills/fix/SKILL.md b/.agents/skills/fix/SKILL.md new file mode 100644 index 000000000..9f6b1047e --- /dev/null +++ b/.agents/skills/fix/SKILL.md @@ -0,0 +1,13 @@ +--- +name: fix +description: Diagnose and repair a local Database verification failure using the smallest reproducer and safest scoped change. Use when lint, typecheck, tests, builds, browser checks, or offline evaluations fail or hang. +--- + +# Fix + +1. Capture the exact failing command, output, duration, and environment context. +2. Run `npm run workflow:triage -- --log --write-evidence` when a saved failure log exists. +3. Classify the cause as code, test, tooling, resource, race, or provider/configuration. +4. Reproduce with the narrowest deterministic check before editing. +5. Make the smallest safe fix, rerun that check, then widen proportionally. +6. Do not run provider-backed remediation without explicit approval. diff --git a/.agents/skills/fix/agents/openai.yaml b/.agents/skills/fix/agents/openai.yaml new file mode 100644 index 000000000..18d3c758c --- /dev/null +++ b/.agents/skills/fix/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Fix" + short_description: "Triage and repair local Database check failures" + default_prompt: "Use $fix to reproduce, classify, and minimally repair this local Database verification failure." diff --git a/.agents/skills/handover/SKILL.md b/.agents/skills/handover/SKILL.md new file mode 100644 index 000000000..e8ee81cd7 --- /dev/null +++ b/.agents/skills/handover/SKILL.md @@ -0,0 +1,13 @@ +--- +name: handover +description: Prepare a concise evidence-backed Database handover without automatically committing, pushing, opening a PR, or calling providers. Use when work is ready for another person, task, review, or explicit upload workflow. +--- + +# Handover + +1. Inspect branch, upstream, worktrees, status, relevant diffs, and recent commits. +2. Run `npm run workflow:lifecycle -- --phase handoff --write-evidence`. +3. Run the smallest proportionate offline verification; prefer `npm run verify:pr-local` for non-trivial ready work. +4. Separate intended changes from unrelated dirty work and list generated artifacts. +5. Summarize files, checks, failures, skipped gates, risks, and the exact next action. +6. Do not commit, push, open a PR, merge, or call providers unless explicitly requested and authorized. diff --git a/.agents/skills/handover/agents/openai.yaml b/.agents/skills/handover/agents/openai.yaml new file mode 100644 index 000000000..c95392005 --- /dev/null +++ b/.agents/skills/handover/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Handover" + short_description: "Prepare a safe evidence-backed Database handover" + default_prompt: "Use $handover to prepare this Database work for handover with local proof and no automatic publishing." diff --git a/.agents/skills/health/SKILL.md b/.agents/skills/health/SKILL.md new file mode 100644 index 000000000..026c8183e --- /dev/null +++ b/.agents/skills/health/SKILL.md @@ -0,0 +1,13 @@ +--- +name: health +description: Assess local Database repository health quickly with offline checks and clear separation of code failures from environment or tooling blockers. Use for routine health checks, stale-state diagnosis, or a fast confidence snapshot. +--- + +# Health + +1. Inspect branch, status, worktrees, runtime versions, relevant logs, and active repo-owned processes. +2. Run `npm run check:runtime`, `npm run check:skills`, and the smallest check related to current changes. +3. Use `npm run verify:cheap` only when a broader offline snapshot is proportionate. +4. Classify failures as product code, test, tooling, resources, environment, or provider-gated. +5. Do not start permanent watchers or run live health endpoints without approval. +6. Return a short healthy/degraded/blocked assessment with exact evidence. diff --git a/.agents/skills/health/agents/openai.yaml b/.agents/skills/health/agents/openai.yaml new file mode 100644 index 000000000..01bbff1fb --- /dev/null +++ b/.agents/skills/health/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Health" + short_description: "Check local Database repository health quickly" + default_prompt: "Use $health to produce a fast offline health assessment for this Database repository." diff --git a/.agents/skills/incident/SKILL.md b/.agents/skills/incident/SKILL.md new file mode 100644 index 000000000..47585d704 --- /dev/null +++ b/.agents/skills/incident/SKILL.md @@ -0,0 +1,13 @@ +--- +name: incident +description: Triage recurring Database canary, CI, runtime, scheduled-task, and provider failures into reproducible causes, impact, containment, and next actions. Use for regressions, outages, flaky jobs, alerts, or repeated nightly or weekly failures. +--- + +# Incident + +1. Build a timeline from local logs, saved artifacts, workflow definitions, and known-good comparisons. +2. Classify the failure as deterministic code, fixture/data drift, schedule, concurrency, environment, quota, provider, or observability. +3. Reproduce offline with the smallest fixture, parser test, or triage artifact before editing. +4. Implement only safe local containment or correction and verify the exact failure path. +5. Do not query GitHub, hosted CI, Supabase, OpenAI, production logs, or providers without approval. +6. Report impact, cause confidence, fix, proof, remaining uncertainty, and gated live confirmation. diff --git a/.agents/skills/incident/agents/openai.yaml b/.agents/skills/incident/agents/openai.yaml new file mode 100644 index 000000000..97d61bd71 --- /dev/null +++ b/.agents/skills/incident/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Incident" + short_description: "Triage recurring Database operational failures" + default_prompt: "Use $incident to reconstruct and reproduce this Database canary, CI, runtime, or scheduled failure offline first." diff --git a/.agents/skills/ingest/SKILL.md b/.agents/skills/ingest/SKILL.md new file mode 100644 index 000000000..c58362879 --- /dev/null +++ b/.agents/skills/ingest/SKILL.md @@ -0,0 +1,13 @@ +--- +name: ingest +description: Triage and verify Database document ingestion, extraction, queueing, retries, workers, batches, and failure recovery. Use for uploads, ingestion jobs, extraction defects, worker reliability, or queue behavior. +--- + +# Ingest + +1. Map the local path from upload and validation through queue, worker, extraction, persistence, and status reporting. +2. Reproduce with fixtures, unit tests, or mocked/local queue state before considering live services. +3. Check idempotency, retry limits, partial failure, cancellation, owner scope, file safety, and observability. +4. Run the smallest ingestion or edge-function test plus production-readiness planning. +5. Treat workers, imports, live buckets, Supabase functions, and production queues as approval-required. +6. Report the failed stage, recovery safety, data-integrity risk, and exact gated next command. diff --git a/.agents/skills/ingest/agents/openai.yaml b/.agents/skills/ingest/agents/openai.yaml new file mode 100644 index 000000000..cb0827299 --- /dev/null +++ b/.agents/skills/ingest/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Ingest" + short_description: "Triage Database ingestion and worker pipelines" + default_prompt: "Use $ingest to trace and verify this Database ingestion, extraction, queue, or worker issue safely." diff --git a/.agents/skills/live-design-sweep/SKILL.md b/.agents/skills/live-design-sweep/SKILL.md index 40dfe469c..d66e90fc4 100644 --- a/.agents/skills/live-design-sweep/SKILL.md +++ b/.agents/skills/live-design-sweep/SKILL.md @@ -1,17 +1,10 @@ --- name: live-design-sweep -description: Inspect and improve the running Database application across routes, responsive breakpoints, keyboard interaction, accessibility modes, and shared UI surfaces. Use for design reviews, UI polish, browser QA, screenshots, mobile checks, accessibility work, or requests to find and fix visual or interaction defects across the app. +description: Compatibility alias for the ui skill. Use only when the user explicitly invokes the older live-design-sweep name; otherwise use ui for live route and accessibility verification. --- -# Live Design Sweep +# Live Design Sweep Alias -1. Run `npm run workflow:design-sweep` to print the proof contract. -2. Run `npm run ensure` before opening the app. Use only the identity-verified URL it prints. -3. Derive route coverage from `docs/site-map.md` and changed navigation. Inspect representative content-rich, empty, loading, error, dialog, and long-scroll states. -4. Cover 320, 390, 639, 768, 1440, and 1920 px where applicable. Check horizontal overflow, single-scrollport ownership, sticky/fixed collisions, tap targets, wrapping, and safe-area behavior. -5. Check keyboard order, visible focus, dialog focus trapping and restoration, accessible names, contrast, reduced motion, forced colors, zoom, and screen-reader semantics. -6. Record reproducible defects with route, viewport, trigger, expected behavior, and screenshot or test evidence. Fix shared primitives before duplicating route-specific patches. -7. Add the smallest regression proof, then run focused Playwright, accessibility, `verify:ui`, and `verify:cheap` in that order as warranted. -8. Write durable route/breakpoint coverage to the task report or `docs/archive/design-qa.md` when the pass is broad. - -Do not claim external design fidelity unless a target design or screenshot was supplied. +1. Read and follow `.agents/skills/ui/SKILL.md`. +2. Use `ui` in recommendations and new documentation. +3. Do not maintain a second copy of the design verification procedure here. diff --git a/.agents/skills/live-design-sweep/agents/openai.yaml b/.agents/skills/live-design-sweep/agents/openai.yaml index 6202e84bf..6e37b3575 100644 --- a/.agents/skills/live-design-sweep/agents/openai.yaml +++ b/.agents/skills/live-design-sweep/agents/openai.yaml @@ -1,4 +1,6 @@ interface: - display_name: "Live Design Sweep" - short_description: "Inspect and polish every live route" - default_prompt: "Use $live-design-sweep to inspect the running Database app across routes and breakpoints, fix confirmed UI issues, and verify accessibility." + display_name: "UI Alias" + short_description: "Compatibility alias for Database UI verification" + default_prompt: "Use $live-design-sweep only as a compatibility alias for $ui." +policy: + allow_implicit_invocation: false diff --git a/.agents/skills/migrate/SKILL.md b/.agents/skills/migrate/SKILL.md new file mode 100644 index 000000000..1ccca258e --- /dev/null +++ b/.agents/skills/migrate/SKILL.md @@ -0,0 +1,13 @@ +--- +name: migrate +description: Review and prepare Database schema migrations for safety, reversibility, privileges, RLS, locking, data preservation, and schema consistency. Use for new or changed Supabase migrations, functions, grants, policies, or schema changes. +--- + +# Migrate + +1. Inspect migration order, current schema snapshots, generated types, dependent queries, and rollback expectations. +2. Review destructive operations, locks, defaults, nullability, backfills, function security, grants, and RLS. +3. Prefer additive, idempotent, staged changes with explicit validation and rollback. +4. Run static migration, grant, owner-scope, type, and focused unit checks offline. +5. Do not apply, reset, repair, link, or compare against live Supabase without explicit approval. +6. Report forward plan, rollback, data risk, compatibility window, and gated live proof. diff --git a/.agents/skills/migrate/agents/openai.yaml b/.agents/skills/migrate/agents/openai.yaml new file mode 100644 index 000000000..92dd29ec3 --- /dev/null +++ b/.agents/skills/migrate/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Migrate" + short_description: "Review safe Database schema migrations" + default_prompt: "Use $migrate to review this Database migration for data safety, privileges, RLS, compatibility, and rollback." diff --git a/.agents/skills/operations/SKILL.md b/.agents/skills/operations/SKILL.md new file mode 100644 index 000000000..daa50e87c --- /dev/null +++ b/.agents/skills/operations/SKILL.md @@ -0,0 +1,13 @@ +--- +name: operations +description: Turn pending Database operator, provisioning, configuration, and provider work into a deduplicated dependency-ordered batch with explicit approvals and evidence. Use for operator debt, manual runbooks, or deferred external actions. +--- + +# Operations + +1. Run `npm run workflow:operator-closeout -- --write-evidence`. +2. Inventory pending actions from docs, evidence, logs, and current change without executing them. +3. Deduplicate by outcome and order prerequisites, local proof, approval, execution, verification, and rollback. +4. Separate local/offline actions from GitHub, Supabase, OpenAI, hosting, credentials, and production work. +5. Ask for approval only when the batch is precise enough to execute safely. +6. Record owner, command, expected result, evidence, rollback, and unresolved dependency for each item. diff --git a/.agents/skills/operations/agents/openai.yaml b/.agents/skills/operations/agents/openai.yaml new file mode 100644 index 000000000..0c3a05b3d --- /dev/null +++ b/.agents/skills/operations/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Operations" + short_description: "Batch Database operator work safely" + default_prompt: "Use $operations to deduplicate and order pending Database operator work with explicit approval gates and rollback." diff --git a/.agents/skills/operator-closeout/SKILL.md b/.agents/skills/operator-closeout/SKILL.md index 30f81466c..622847994 100644 --- a/.agents/skills/operator-closeout/SKILL.md +++ b/.agents/skills/operator-closeout/SKILL.md @@ -1,23 +1,10 @@ --- name: operator-closeout -description: Convert pending Database operator, deployment, migration, secret, live-service, and governance debt into a current, deduplicated, dependency-ordered, approval-gated execution batch with pre-state, post-state, rollback, and evidence requirements. Use when closing operator backlog items, preparing launch actions, or reconciling confirmation-required follow-ups. +description: Compatibility alias for the operations skill. Use only when the user explicitly invokes the older operator-closeout name; otherwise use operations for approval-gated operator work. --- -# Operator Closeout +# Operator Closeout Alias -1. Inventory candidates with: - `npm run workflow:operator-closeout -- --write-evidence` -2. Treat the output as discovery, not truth. Verify each candidate against its linked runbook and current local state; remove completed, superseded, duplicate, or stale items. -3. Group remaining actions by provider and blast radius. Order prerequisites, read-only probes, reversible writes, irreversible writes, and post-verification. -4. For every action record: - - exact target and environment; - - external effect and credentials required; - - pre-state query or evidence; - - exact command or dashboard action; - - success condition and post-state proof; - - rollback or recovery path; - - expected cost or downtime. -5. Ask for explicit approval for the concrete batch. Approval for one provider or command does not authorize another. -6. Execute approved actions one at a time, stop on unexpected state, and update durable operator documentation only with observed evidence. - -The planner is intentionally plan-only. Never add automatic provider execution to it. +1. Read and follow `.agents/skills/operations/SKILL.md`. +2. Use `operations` in recommendations and new documentation. +3. Do not maintain a second copy of the operator procedure here. diff --git a/.agents/skills/operator-closeout/agents/openai.yaml b/.agents/skills/operator-closeout/agents/openai.yaml index 90a997884..99b443529 100644 --- a/.agents/skills/operator-closeout/agents/openai.yaml +++ b/.agents/skills/operator-closeout/agents/openai.yaml @@ -1,4 +1,6 @@ interface: - display_name: "Operator Closeout" - short_description: "Turn operator debt into safe batches" - default_prompt: "Use $operator-closeout to inventory pending operator actions, deduplicate them, and prepare a safe approval-gated execution batch." + display_name: "Operations Alias" + short_description: "Compatibility alias for Database operator work" + default_prompt: "Use $operator-closeout only as a compatibility alias for $operations." +policy: + allow_implicit_invocation: false diff --git a/.agents/skills/performance/SKILL.md b/.agents/skills/performance/SKILL.md new file mode 100644 index 000000000..9bb348025 --- /dev/null +++ b/.agents/skills/performance/SKILL.md @@ -0,0 +1,13 @@ +--- +name: performance +description: Profile Database query, retrieval, rendering, bundle, worker, queue, memory, and capacity bottlenecks with reproducible local evidence. Use for latency, throughput, resource, scaling, or bundle-budget work. +--- + +# Performance + +1. Define the user-visible metric, baseline, workload, environment, and target before optimizing. +2. Use deterministic local fixtures and existing profiling or budget tools first. +3. Isolate CPU, memory, I/O, query, network, bundle, rendering, concurrency, and cache effects. +4. Make one scoped change at a time and compare before/after distributions, not a single run. +5. Keep live query profiling, provider latency, production traffic, and external load tests approval-gated. +6. Report methodology, variance, improvement, tradeoffs, and remaining capacity risk. diff --git a/.agents/skills/performance/agents/openai.yaml b/.agents/skills/performance/agents/openai.yaml new file mode 100644 index 000000000..0d9238963 --- /dev/null +++ b/.agents/skills/performance/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Performance" + short_description: "Profile Database performance with local evidence" + default_prompt: "Use $performance to baseline, isolate, and verify this Database performance issue with reproducible local evidence." diff --git a/.agents/skills/plan/SKILL.md b/.agents/skills/plan/SKILL.md new file mode 100644 index 000000000..7d369c713 --- /dev/null +++ b/.agents/skills/plan/SKILL.md @@ -0,0 +1,13 @@ +--- +name: plan +description: Plan safe risk-scoped Database work by inspecting the current change, selecting the smallest local verification ladder, and separating provider-backed checks into explicit approval gates. Use before non-trivial changes or when asked what checks are needed. +--- + +# Plan + +1. Complete the task-start preflight and preserve unrelated work. +2. Run `npm run workflow:flightplan -- --write-evidence`; add `--files pathA,pathB` for proposed paths. +3. Confirm the detected risk classes match behavior, not only filenames. +4. Start with the narrowest local check and widen only when warranted. +5. Never execute anything under `approvalRequired` without explicit confirmation. +6. Report the planned checks, approval gates, evidence path, and residual risk. diff --git a/.agents/skills/plan/agents/openai.yaml b/.agents/skills/plan/agents/openai.yaml new file mode 100644 index 000000000..2a4920a92 --- /dev/null +++ b/.agents/skills/plan/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Plan" + short_description: "Plan risk-scoped offline Database verification" + default_prompt: "Use $plan to classify this Database change and choose the smallest safe offline verification ladder." diff --git a/.agents/skills/privacy/SKILL.md b/.agents/skills/privacy/SKILL.md new file mode 100644 index 000000000..a2deaefc1 --- /dev/null +++ b/.agents/skills/privacy/SKILL.md @@ -0,0 +1,13 @@ +--- +name: privacy +description: Review Database sensitive-data collection, ownership, retention, logging, caching, export, deletion, telemetry, and client exposure. Use for privacy changes, clinical queries, user data, logs, analytics, or data-subject operations. +--- + +# Privacy + +1. Inventory sensitive inputs, derived data, identifiers, storage locations, logs, caches, exports, and recipients. +2. Trace collection purpose, minimization, owner scope, retention, deletion, and failure paths. +3. Check client bundles, errors, telemetry, query logs, temporary files, and generated artifacts for exposure. +4. Add focused local proofs for redaction, denial, deletion, or non-persistence behavior. +5. Do not inspect live records, production logs, Supabase, or providers without explicit approval. +6. Report data flows, controls, gaps, retention assumptions, and remaining governance decisions. diff --git a/.agents/skills/privacy/agents/openai.yaml b/.agents/skills/privacy/agents/openai.yaml new file mode 100644 index 000000000..d72f0f498 --- /dev/null +++ b/.agents/skills/privacy/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Privacy" + short_description: "Review Database sensitive-data handling" + default_prompt: "Use $privacy to trace sensitive Database data through collection, storage, logs, export, retention, and deletion." diff --git a/.agents/skills/rag-change-lab/SKILL.md b/.agents/skills/rag-change-lab/SKILL.md index c25003a1e..8a6aab6f8 100644 --- a/.agents/skills/rag-change-lab/SKILL.md +++ b/.agents/skills/rag-change-lab/SKILL.md @@ -1,16 +1,10 @@ --- name: rag-change-lab -description: Validate Database retrieval, ranking, chunking, query classification, source selection, citations, answer synthesis, grounding, and RAG privacy changes with focused tests and provider-free golden evaluation before requesting live evaluation approval. Use for RAG implementation, refactoring, debugging, regression analysis, or merge-readiness work. +description: Compatibility alias for the rag skill. Use only when the user explicitly invokes the older rag-change-lab name; otherwise use rag for offline-first retrieval and answer validation. --- -# RAG Change Lab +# RAG Change Lab Alias -1. Generate the lab plan: - `npm run workflow:rag-lab -- --write-evidence` -2. Read `docs/retrieval-quality-runbook.md` and only the change-relevant RAG architecture or threat-model sections. -3. Define the invariant before editing: retrieval recall/ranking, owner scope, grounding, citation correctness, unsupported-query behavior, latency, cache invalidation, or prompt-injection resistance. -4. Run the closest unit/property tests first, followed by `npm run eval:rag:offline`. Inspect cases and metrics rather than relying only on exit status. -5. Make minimal changes and compare against the accepted baseline. Treat recall, MRR, grounded support, citation failure, numeric grounding, source governance, and strategy mix as separate signals. -6. Run `verify:cheap` after focused proof. Use `verify:pr-local` for handoff. -7. Stop before `eval:retrieval:quality`, `eval:rag`, `eval:quality`, or any live corpus/provider operation. Present the exact command, expected cost/effect, required environment, baseline, and success threshold for explicit approval. -8. Report whether remaining uncertainty is code, corpus, owner configuration, live-provider variance, or environment. +1. Read and follow `.agents/skills/rag/SKILL.md`. +2. Use `rag` in recommendations and new documentation. +3. Do not maintain a second copy of the RAG validation procedure here. diff --git a/.agents/skills/rag-change-lab/agents/openai.yaml b/.agents/skills/rag-change-lab/agents/openai.yaml index dd40ad7a9..30261416f 100644 --- a/.agents/skills/rag-change-lab/agents/openai.yaml +++ b/.agents/skills/rag-change-lab/agents/openai.yaml @@ -1,4 +1,6 @@ interface: - display_name: "RAG Change Lab" - short_description: "Validate retrieval and answer changes" - default_prompt: "Use $rag-change-lab to select and run the safe offline checks for this RAG change and prepare any live evaluation approval request." + display_name: "RAG Alias" + short_description: "Compatibility alias for Database RAG validation" + default_prompt: "Use $rag-change-lab only as a compatibility alias for $rag." +policy: + allow_implicit_invocation: false diff --git a/.agents/skills/rag/SKILL.md b/.agents/skills/rag/SKILL.md new file mode 100644 index 000000000..e12a54df2 --- /dev/null +++ b/.agents/skills/rag/SKILL.md @@ -0,0 +1,13 @@ +--- +name: rag +description: Validate Database retrieval, classification, ranking, grounding, citations, and answer behavior offline before preparing any live evaluation approval gate. Use for RAG changes, golden regressions, search quality, or answer-quality work. +--- + +# RAG + +1. Run `npm run workflow:rag-lab -- --write-evidence` for the affected paths. +2. Compare fixtures, manifests, tests, and prior local artifacts before changing behavior. +3. Reproduce regressions with the smallest deterministic offline case. +4. Run focused retrieval tests, `npm run check:rag:fixtures`, and `npm run eval:rag:offline` as selected. +5. Preserve source governance, owner scope, conservative fallback, and rollback behavior. +6. Ask before live retrieval, answer, Supabase, OpenAI, or provider-backed evaluation. diff --git a/.agents/skills/rag/agents/openai.yaml b/.agents/skills/rag/agents/openai.yaml new file mode 100644 index 000000000..e8340c5f4 --- /dev/null +++ b/.agents/skills/rag/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "RAG" + short_description: "Validate Database RAG behavior offline first" + default_prompt: "Use $rag to reproduce and verify this retrieval or answer change offline before any live evaluation." diff --git a/.agents/skills/recovery/SKILL.md b/.agents/skills/recovery/SKILL.md new file mode 100644 index 000000000..5dcc1a2af --- /dev/null +++ b/.agents/skills/recovery/SKILL.md @@ -0,0 +1,13 @@ +--- +name: recovery +description: Plan Database restore, rollback, queue recovery, reconciliation, backup validation, and disaster-recovery proof without modifying live systems automatically. Use for failed jobs, corrupted state, rollback planning, or continuity exercises. +--- + +# Recovery + +1. Define the affected data, service, owner scope, last known-good point, recovery objective, and acceptable loss window. +2. Inspect local recovery scripts, migrations, queue state models, backups documentation, and reconciliation checks. +3. Prefer dry runs, fixtures, manifests, counts, and idempotent recovery steps. +4. Specify stop conditions, checkpoints, rollback, post-recovery integrity, and audit evidence. +5. Treat live restores, queue mutation, cleanup, Supabase, hosting, and production operations as approval-required. +6. Deliver an ordered recovery runbook and identify every unproven assumption. diff --git a/.agents/skills/recovery/agents/openai.yaml b/.agents/skills/recovery/agents/openai.yaml new file mode 100644 index 000000000..d7b6a3e79 --- /dev/null +++ b/.agents/skills/recovery/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Recovery" + short_description: "Plan safe Database recovery and rollback" + default_prompt: "Use $recovery to build a safe Database restore, rollback, reconciliation, and verification runbook." diff --git a/.agents/skills/reindex/SKILL.md b/.agents/skills/reindex/SKILL.md new file mode 100644 index 000000000..b91c9b49c --- /dev/null +++ b/.agents/skills/reindex/SKILL.md @@ -0,0 +1,13 @@ +--- +name: reindex +description: Plan and verify Database index generations, backfills, promotions, cleanup, health checks, and rollback without touching live data automatically. Use for reindex jobs, generation changes, index migrations, or backfill operations. +--- + +# Reindex + +1. Identify the index generation, affected owners/documents, state transitions, and rollback boundary. +2. Inspect reindex code, migrations, fixtures, health checks, and cleanup behavior offline. +3. Check idempotency, resumability, concurrency, partial promotion, stale generations, and owner isolation. +4. Add or run the smallest deterministic generation-state proof. +5. Treat reindex, backfill, cleanup, promotion, health, and Supabase commands as approval-required unless proven purely local. +6. Produce an ordered runbook with preconditions, checkpoints, rollback, reconciliation, and evidence. diff --git a/.agents/skills/reindex/agents/openai.yaml b/.agents/skills/reindex/agents/openai.yaml new file mode 100644 index 000000000..5576fb00f --- /dev/null +++ b/.agents/skills/reindex/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Reindex" + short_description: "Plan safe Database reindex and backfill work" + default_prompt: "Use $reindex to plan and verify this index generation, backfill, promotion, or cleanup workflow safely." diff --git a/.agents/skills/release/SKILL.md b/.agents/skills/release/SKILL.md new file mode 100644 index 000000000..896cc1716 --- /dev/null +++ b/.agents/skills/release/SKILL.md @@ -0,0 +1,13 @@ +--- +name: release +description: Assemble Database release-readiness evidence, risk-scoped local checks, governance proof, rollback notes, and approval-gated final verification. Use for release, merge readiness, production handoff, or go-live confidence. +--- + +# Release + +1. Inspect intended diff, branch state, review ledger, release notes, migrations, configuration, and rollback. +2. Run the flight plan and complete the smallest focused checks before `npm run verify:pr-local`. +3. Include UI, clinical, RAG, migration, privacy, or deployment proof only when selected by risk. +4. Verify required governance and handoff documentation is complete. +5. Do not run `verify:release`, hosted CI, GitHub, Supabase, OpenAI, or deployment commands without approval. +6. Return a ready/not-ready decision with passed, failed, skipped, and gated checks. diff --git a/.agents/skills/release/agents/openai.yaml b/.agents/skills/release/agents/openai.yaml new file mode 100644 index 000000000..09dbc2388 --- /dev/null +++ b/.agents/skills/release/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Release" + short_description: "Assemble Database release-readiness evidence" + default_prompt: "Use $release to assess Database release readiness with local proof, rollback notes, and explicit approval gates." diff --git a/.agents/skills/review/SKILL.md b/.agents/skills/review/SKILL.md new file mode 100644 index 000000000..4ce0b4c30 --- /dev/null +++ b/.agents/skills/review/SKILL.md @@ -0,0 +1,13 @@ +--- +name: review +description: Review the current Database diff, branch, or explicitly approved PR target for high-confidence correctness, security, privacy, clinical, and reliability defects. Use for code review, diff review, or readiness feedback. +--- + +# Review + +1. Read `docs/codex-review-protocol.md` and `docs/branch-review-ledger.md` when present. +2. Resolve the local target SHA and check whether the same scope was already reviewed. +3. Inspect changed behavior and realistic failure paths; prioritize reproducible P0-P2 findings. +4. Cite exact files and lines, trigger, impact, and the smallest proof or fix. +5. Do not call GitHub or hosted CI without explicit approval. +6. Record the completed local review in the ledger when repository instructions require it. diff --git a/.agents/skills/review/agents/openai.yaml b/.agents/skills/review/agents/openai.yaml new file mode 100644 index 000000000..5122fa971 --- /dev/null +++ b/.agents/skills/review/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Review" + short_description: "Review Database changes for concrete high-risk defects" + default_prompt: "Use $review to inspect this Database change and report high-confidence findings with exact evidence." diff --git a/.agents/skills/run/SKILL.md b/.agents/skills/run/SKILL.md new file mode 100644 index 000000000..11a854b49 --- /dev/null +++ b/.agents/skills/run/SKILL.md @@ -0,0 +1,12 @@ +--- +name: run +description: Start or verify the correct local Database application with repository identity checks and return its actual URL. Use when the user says run, asks for the local app, or needs browser work against this project. +--- + +# Run + +1. Run the task-start preflight if needed and inspect active repo-owned server state. +2. Execute `npm run ensure`; do not assume ports 3000, 3001, or 3002. +3. Trust only a URL whose `/api/local-project-id` identity check matches this project. +4. Do not stop or modify another project's server. +5. Return the verified URL briefly and report any environment blocker. diff --git a/.agents/skills/run/agents/openai.yaml b/.agents/skills/run/agents/openai.yaml new file mode 100644 index 000000000..26bf3ea60 --- /dev/null +++ b/.agents/skills/run/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Run" + short_description: "Start the identity-checked local Database app" + default_prompt: "Use $run to start or verify the local Database app and return its confirmed URL." diff --git a/.agents/skills/security/SKILL.md b/.agents/skills/security/SKILL.md new file mode 100644 index 000000000..7cb2d2328 --- /dev/null +++ b/.agents/skills/security/SKILL.md @@ -0,0 +1,13 @@ +--- +name: security +description: Find and validate concrete Database security risks across secrets, input handling, authentication, authorization, storage, dependencies, and trust boundaries. Use for security review, threat analysis, or confirmed finding remediation. +--- + +# Security + +1. Define the scoped assets, actors, trust boundaries, entry points, and attacker-controlled inputs. +2. Search for realistic exploit paths involving auth, injection, SSRF, file handling, secrets, storage, RLS, and dependencies. +3. Prove each finding with changed code, a safe local test, or deterministic static evidence. +4. Rank by impact and likelihood; avoid speculative style findings. +5. Apply only minimal, testable fixes when requested and keep live/provider validation approval-gated. +6. Report fixed, open, false-positive, and residual risks separately. diff --git a/.agents/skills/security/agents/openai.yaml b/.agents/skills/security/agents/openai.yaml new file mode 100644 index 000000000..8892d4c4b --- /dev/null +++ b/.agents/skills/security/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Security" + short_description: "Find concrete Database security risks" + default_prompt: "Use $security to inspect this Database surface for reproducible security risks and minimal safe fixes." diff --git a/.agents/skills/session-lifecycle/SKILL.md b/.agents/skills/session-lifecycle/SKILL.md index 8d89067ab..efbe19ee6 100644 --- a/.agents/skills/session-lifecycle/SKILL.md +++ b/.agents/skills/session-lifecycle/SKILL.md @@ -1,17 +1,10 @@ --- name: session-lifecycle -description: Manage Database repository work safely from task start through verification, handoff, merge proof, and conservative worktree or branch cleanup. Use when starting a task, checking session state, preparing completed work for handoff, confirming a squash merge landed, or cleaning up only proven-redundant task state. +description: Compatibility alias for the task skill. Use only when the user explicitly invokes the older session-lifecycle name; otherwise use task for safe lifecycle transitions. --- -# Session Lifecycle +# Session Lifecycle Alias -1. Select the phase and generate its plan: - `npm run workflow:lifecycle -- --phase ` -2. At task start, run the mandatory task-start preflight command configured by the active host or root `AGENTS.md`, then read applicable repo instructions and preserve all existing work. On this Windows Codex installation, resolve the installation-managed script from the user profile (for example, `& (Join-Path $env:USERPROFILE '.codex\scripts\start-codex-task.ps1') -TaskSlug `); it is intentionally not repo-local. Do not use the shared stash across worktrees. -3. During work, recheck branch and status before edits and before handoff. Use `database-flightplan` for non-trivial change verification. -4. At handoff, stage explicit coherent paths only, verify locally, and inspect the staged diff. Commit, push, PR creation, hosted CI, and merge remain authorization-bound. -5. After a squash merge, verify the reviewed content against fetched `origin/main`; do not rely on ancestry alone. Check for late orphaned commits before cleanup. -6. Remove a worktree or branch only after exact path/ref resolution, clean-state proof, and content-equivalence evidence. Preserve dirty, active, ambiguous, open-PR, or patch-unique work. -7. Record the outcome in `docs/branch-review-ledger.md` when the repo review protocol requires it. - -The older `.claude/skills/newtask`, `handoff`, and `prlanded` remain compatibility surfaces. Use this skill as the agent-neutral orchestration contract. +1. Read and follow `.agents/skills/task/SKILL.md`. +2. Use `task` in recommendations and new documentation. +3. Do not maintain a second copy of the lifecycle procedure here. diff --git a/.agents/skills/session-lifecycle/agents/openai.yaml b/.agents/skills/session-lifecycle/agents/openai.yaml index ff66a9a4f..e717eaccb 100644 --- a/.agents/skills/session-lifecycle/agents/openai.yaml +++ b/.agents/skills/session-lifecycle/agents/openai.yaml @@ -1,4 +1,6 @@ interface: - display_name: "Session Lifecycle" - short_description: "Manage safe task start through cleanup" - default_prompt: "Use $session-lifecycle to inspect and advance this Database task through start, verification, handoff, merge proof, or safe cleanup." + display_name: "Task Alias" + short_description: "Compatibility alias for Database task lifecycle" + default_prompt: "Use $session-lifecycle only as a compatibility alias for $task." +policy: + allow_implicit_invocation: false diff --git a/.agents/skills/skills/SKILL.md b/.agents/skills/skills/SKILL.md new file mode 100644 index 000000000..3378b50d5 --- /dev/null +++ b/.agents/skills/skills/SKILL.md @@ -0,0 +1,13 @@ +--- +name: skills +description: List every unique Database-specific skill with a clear explanation, hide compatibility aliases, and recommend the smallest useful set for the current request. Use when the user asks what skills exist, what they do, or which workflow to choose. +--- + +# Skills + +1. Run `npm run skills` so the validated catalog, not chat memory, is authoritative. +2. Present every canonical skill exactly once under its catalog category with its plain-language purpose. +3. Do not count compatibility aliases as unique skills. Mention an alias only when it helps the user migrate an older command. +4. Recommend the smallest high-yield set for the current request and explain the order in one sentence. +5. Run `npm run check:skills` when catalog integrity is part of the task. +6. Never execute another skill merely because it was listed; follow the normal scope and approval rules. diff --git a/.agents/skills/skills/agents/openai.yaml b/.agents/skills/skills/agents/openai.yaml new file mode 100644 index 000000000..07e6b6df1 --- /dev/null +++ b/.agents/skills/skills/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Skills" + short_description: "List and recommend every unique Database skill" + default_prompt: "Use $skills to list all unique Database skills, explain each clearly, and recommend the smallest useful set for my request." diff --git a/.agents/skills/sources/SKILL.md b/.agents/skills/sources/SKILL.md new file mode 100644 index 000000000..4d4294bc1 --- /dev/null +++ b/.agents/skills/sources/SKILL.md @@ -0,0 +1,13 @@ +--- +name: sources +description: Audit Database source metadata, citations, approvals, labels, provenance, rendering, and release-governance coverage. Use for citation defects, source-governance changes, document labels, public promotion, or clinical evidence traceability. +--- + +# Sources + +1. Trace source identity from ingestion metadata through retrieval, answer citations, and UI rendering. +2. Check provenance, approval state, labels, dates, ownership, public visibility, and missing-metadata behavior. +3. Use focused local tests and fixture checks before broader governance scripts. +4. Treat scripts that query live Supabase or production-like data as approval-required. +5. Verify conservative behavior when a source is absent, stale, private, rejected, or ambiguous. +6. Report coverage, debt, release impact, rollback, and any live audit that still needs approval. diff --git a/.agents/skills/sources/agents/openai.yaml b/.agents/skills/sources/agents/openai.yaml new file mode 100644 index 000000000..bd10ec390 --- /dev/null +++ b/.agents/skills/sources/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Sources" + short_description: "Audit Database citations and source governance" + default_prompt: "Use $sources to audit source metadata, citations, approvals, provenance, and release-governance coverage." diff --git a/.agents/skills/task/SKILL.md b/.agents/skills/task/SKILL.md new file mode 100644 index 000000000..48f63bbd7 --- /dev/null +++ b/.agents/skills/task/SKILL.md @@ -0,0 +1,13 @@ +--- +name: task +description: Manage Database task lifecycle transitions safely, including start, status, handoff, landed proof, and cleanup planning. Use when beginning work, checking task state, preparing handoff, proving integration, or closing a session. +--- + +# Task + +1. Use the required task-start script before repository changes. +2. Run `npm run workflow:lifecycle -- --phase --write-evidence`. +3. Inspect branch, upstream, worktrees, status, and operation markers for the selected phase. +4. Preserve dirty, ambiguous, active, or unmerged work. +5. Keep fetch, push, PR, merge, and provider checks approval-gated. +6. Report the exact state transition proved and any action still requiring confirmation. diff --git a/.agents/skills/task/agents/openai.yaml b/.agents/skills/task/agents/openai.yaml new file mode 100644 index 000000000..87cabe46d --- /dev/null +++ b/.agents/skills/task/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Task" + short_description: "Manage safe Database task lifecycle transitions" + default_prompt: "Use $task to inspect and manage the requested Database task lifecycle phase safely." diff --git a/.agents/skills/test/SKILL.md b/.agents/skills/test/SKILL.md new file mode 100644 index 000000000..662f763fe --- /dev/null +++ b/.agents/skills/test/SKILL.md @@ -0,0 +1,13 @@ +--- +name: test +description: Choose and run the smallest relevant local Database test, reproduce failures deterministically, and widen verification only when needed. Use for test requests, behavioral proofs, or focused regression coverage. +--- + +# Test + +1. Inspect the touched behavior and existing nearby tests before selecting a command. +2. Prefer a single Vitest or Playwright target; use `npm run test:focused -- --files ` only for safe source-only changes. +3. Add the smallest deterministic regression proof when behavior is unprotected. +4. Run one heavy command at a time and rerun the smallest failing check after each fix. +5. Keep live/provider-backed tests approval-gated. +6. Report exact commands, exit codes, coverage limits, and tests not run. diff --git a/.agents/skills/test/agents/openai.yaml b/.agents/skills/test/agents/openai.yaml new file mode 100644 index 000000000..bd52a5f10 --- /dev/null +++ b/.agents/skills/test/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Test" + short_description: "Run the smallest useful local Database test" + default_prompt: "Use $test to select and run the smallest deterministic local proof for this Database behavior." diff --git a/.agents/skills/ui/SKILL.md b/.agents/skills/ui/SKILL.md new file mode 100644 index 000000000..6ba765344 --- /dev/null +++ b/.agents/skills/ui/SKILL.md @@ -0,0 +1,13 @@ +--- +name: ui +description: Inspect and verify the live Database interface across routes, breakpoints, interactions, accessibility modes, reduced motion, and forced colors. Use for UI, frontend, routing, styling, responsive, browser, or accessibility work. +--- + +# UI + +1. Read the relevant Next.js guide under `node_modules/next/dist/docs/` before code changes. +2. Run `npm run workflow:design-sweep -- --write-evidence` and then `npm run ensure`. +3. Verify project identity before browser work; do not assume a port. +4. Inspect affected routes at phone and desktop widths plus keyboard, focus, reduced-motion, and forced-colors states. +5. Add the smallest focused browser proof, then use `npm run verify:ui` when proportionate. +6. Report routes, viewports, interactions, accessibility evidence, and residual visual risk. diff --git a/.agents/skills/ui/agents/openai.yaml b/.agents/skills/ui/agents/openai.yaml new file mode 100644 index 000000000..06567acee --- /dev/null +++ b/.agents/skills/ui/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "UI" + short_description: "Sweep live Database UI and accessibility states" + default_prompt: "Use $ui to inspect and verify this Database interface across routes, breakpoints, interactions, and accessibility modes." diff --git a/.agents/skills/verify-triage-fix/SKILL.md b/.agents/skills/verify-triage-fix/SKILL.md index 670f26004..fb0509547 100644 --- a/.agents/skills/verify-triage-fix/SKILL.md +++ b/.agents/skills/verify-triage-fix/SKILL.md @@ -1,21 +1,10 @@ --- name: verify-triage-fix -description: Diagnose and fix local Database repository verification failures using the smallest reproducible target, failure classification, known-flake evidence, and narrow reruns before broader gates. Use when lint, typecheck, Vitest, Playwright, build, offline RAG, workflow tooling, or another local check fails or appears hung. +description: Compatibility alias for the fix skill. Use only when the user explicitly invokes the older verify-triage-fix name; otherwise use fix for local verification failure triage. --- -# Verify Triage Fix +# Verify Triage Fix Alias -1. Capture the exact failed command, exit code, complete actionable error, and relevant artifact path. Do not combine more checks until the failure is understood. -2. Classify a saved log with: - `npm run workflow:triage -- --log ` - Without `--log`, the planner uses `.local/workflow-last-failure.json` when present. -3. Verify the classification: - - regression: reproduce the smallest test, typecheck target, or build surface; - - environment: inspect runtime, paths, dependencies, process ownership, and timeouts; - - provider/configuration: stop and request approval or missing configuration; - - known flake: prove the signature matches `tests/flake-ledger.json` before treating it as such. -4. Change only the smallest confirmed cause. Do not hide failures by weakening assertions, increasing timeouts blindly, or broad refactoring. -5. Rerun the smallest failing target after each fix. Widen to `verify:cheap`, `verify:ui`, or `verify:pr-local` only after it passes. -6. Distinguish code defects, pre-existing failures, environment blockers, and provider gates in the final report. - -Never use provider access to diagnose a local failure without explicit confirmation. +1. Read and follow `.agents/skills/fix/SKILL.md`. +2. Use `fix` in recommendations and new documentation. +3. Do not maintain a second copy of the triage procedure here. diff --git a/.agents/skills/verify-triage-fix/agents/openai.yaml b/.agents/skills/verify-triage-fix/agents/openai.yaml index 20ea968cd..111717db8 100644 --- a/.agents/skills/verify-triage-fix/agents/openai.yaml +++ b/.agents/skills/verify-triage-fix/agents/openai.yaml @@ -1,4 +1,6 @@ interface: - display_name: "Verify Triage Fix" - short_description: "Diagnose and fix verification failures" - default_prompt: "Use $verify-triage-fix to diagnose the current local verification failure, fix the smallest confirmed cause, and rerun focused checks." + display_name: "Fix Alias" + short_description: "Compatibility alias for Database failure triage" + default_prompt: "Use $verify-triage-fix only as a compatibility alias for $fix." +policy: + allow_implicit_invocation: false diff --git a/.agents/skills/workflows/SKILL.md b/.agents/skills/workflows/SKILL.md index 3cbe11c14..c059742b0 100644 --- a/.agents/skills/workflows/SKILL.md +++ b/.agents/skills/workflows/SKILL.md @@ -1,30 +1,10 @@ --- name: workflows -description: List all Database workflow skills, summarize their purpose, and recommend the right workflow for the current change. +description: Compatibility alias for the skills skill. Use only when the user explicitly invokes the older workflows name; otherwise use skills for the complete unique Database skill catalog. --- -# Database Workflows +# Workflows Alias -## What this skill covers - -Use this skill when you need a quick index of Database-specific workflow skills and a reminder of what each one validates. - -## Available workflow skills - -- `database-flightplan`: Risk classification and local/offline verification planning for non-trivial changes. -- `verify-triage-fix`: Stepwise local failure triage for lint/typecheck/tests/build/playwright/offline checks. -- `clinical-change-proof`: Clinical, answer-governance, safety, privacy, and source-governance evidence planning. -- `live-design-sweep`: App-wide live route/design/accessibility verification across breakpoints and interaction states. -- `rag-change-lab`: Offline evaluation and migration-proofing for retrieval, classification, ranking, and grounding changes. -- `operator-closeout`: Conversion of pending deployment/operator/provisioning tasks into dependency-ordered execution batches. -- `session-lifecycle`: Start-to-handoff-to-cleanup orchestration with review/merge safety checks. - -## Recommended usage pattern - -1. Start with `database-flightplan` for non-trivial edits. -2. Use `verify-triage-fix` only when checks fail or hang. -3. Add `clinical-change-proof` for clinical retrieval, output, or source-governance risk. -4. Use `live-design-sweep` for UI/routing/interaction defects. -5. Use `rag-change-lab` for RAG behavior work. -6. Use `operator-closeout` for deferred operator or provider-work batches. -7. Use `session-lifecycle` for session-level handoff and cleanup. +1. Read and follow `.agents/skills/skills/SKILL.md`. +2. Use `skills` in recommendations and new documentation. +3. Do not present `workflows` as an additional unique skill. diff --git a/.agents/skills/workflows/agents/openai.yaml b/.agents/skills/workflows/agents/openai.yaml index cb9580560..ca456238a 100644 --- a/.agents/skills/workflows/agents/openai.yaml +++ b/.agents/skills/workflows/agents/openai.yaml @@ -1,4 +1,6 @@ interface: - display_name: "Database Workflows" - short_description: "Index Database workflow skills and choose the right one" - default_prompt: "Use $workflows to list all Database workflow skills with a concise explanation of what each does and when to run it." + display_name: "Workflows Alias" + short_description: "Compatibility alias for the Database skills catalog" + default_prompt: "Use $workflows only as a compatibility alias for $skills." +policy: + allow_implicit_invocation: false diff --git a/.github/workflows/eval-canary.yml b/.github/workflows/eval-canary.yml index 7253081dd..9d651dfe9 100644 --- a/.github/workflows/eval-canary.yml +++ b/.github/workflows/eval-canary.yml @@ -1,4 +1,4 @@ -# Nightly production eval canary. See docs/observability-slos.md §3. +# Weekly production eval canary. See docs/observability-slos.md §3. # # Runs the golden retrieval eval (which PR CI can never run — it needs live # Supabase + OpenAI keys) plus a small answer-quality subset against the live @@ -6,7 +6,7 @@ # scheduled failures. # # The schedule only fires from the default branch. After merging, trigger one -# workflow_dispatch run and confirm it is green before trusting the nightly +# workflow_dispatch run and confirm it is green before trusting the weekly # cadence. name: Eval Canary @@ -18,8 +18,8 @@ on: required: false default: "8" schedule: - # 18:00 UTC = 02:00 Australia/Perth — off-peak for clinicians. - - cron: "0 18 * * *" + # Sunday 18:00 UTC = Monday 02:00 Australia/Perth — off-peak for clinicians. + - cron: "0 18 * * 0" concurrency: group: eval-canary @@ -60,11 +60,13 @@ jobs: steps: - name: Checkout + id: checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Preflight required secrets + id: preflight run: | missing="" [ -z "$SUPABASE_SERVICE_ROLE_KEY" ] && missing="$missing SUPABASE_SERVICE_ROLE_KEY" @@ -75,6 +77,7 @@ jobs: fi - name: Setup Node.js + id: setup_node uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 with: node-version-file: ".nvmrc" @@ -82,15 +85,22 @@ jobs: cache-dependency-path: package-lock.json - name: Install dependencies + id: install run: npm ci - name: Guard Supabase project identity + id: project_guard run: npm run check:supabase-project - name: Golden retrieval eval (live corpus) - run: npm run eval:retrieval:quality -- --fail-on-threshold + id: golden_retrieval + run: | + mkdir -p .local/eval-canary + set -o pipefail + npm run eval:retrieval:quality -- --fail-on-threshold 2>&1 | tee .local/eval-canary/golden-retrieval.log - name: Answer-quality subset (live generation) + id: answer_quality env: ANSWER_CASE_LIMIT: ${{ github.event.inputs.answer_case_limit || '8' }} # Canary-only latency allowance: cross-region runner -> Sydney Supabase @@ -101,20 +111,48 @@ jobs: echo "answer_case_limit must be an integer from 1 to 100" >&2 exit 2 fi - npm run eval:quality -- --rag-only --limit "$ANSWER_CASE_LIMIT" --fail-on-threshold + mkdir -p .local/eval-canary + set -o pipefail + npm run eval:quality -- --rag-only --limit "$ANSWER_CASE_LIMIT" --fail-on-threshold 2>&1 | tee .local/eval-canary/answer-quality.log - name: Open or update canary failure issue if: failure() && github.event_name == 'schedule' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | + const fs = require("node:fs"); + const path = require("node:path"); + const { pathToFileURL } = require("node:url"); + const { analyzeFailureText } = await import(pathToFileURL(`${process.env.GITHUB_WORKSPACE}/scripts/productivity-core.mjs`).href); const label = "eval-canary"; const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + const logPaths = [ + ".local/eval-canary/golden-retrieval.log", + ".local/eval-canary/answer-quality.log", + ].map((item) => path.join(process.env.GITHUB_WORKSPACE, item)); + const failureText = logPaths + .filter((item) => fs.existsSync(item)) + .map((item) => fs.readFileSync(item, "utf8")) + .join("\n"); + const diagnosis = analyzeFailureText(failureText); + const stepOutcomes = { + checkout: "${{ steps.checkout.outcome }}", + preflight: "${{ steps.preflight.outcome }}", + setup_node: "${{ steps.setup_node.outcome }}", + install: "${{ steps.install.outcome }}", + project_guard: "${{ steps.project_guard.outcome }}", + golden_retrieval: "${{ steps.golden_retrieval.outcome }}", + answer_quality: "${{ steps.answer_quality.outcome }}", + }; const body = [ - `Nightly eval canary failed on ${new Date().toISOString()}.`, + `Weekly eval canary failed on ${new Date().toISOString()}.`, "", `Run: ${runUrl}`, "", + `Failure class: ${diagnosis.category} (${diagnosis.confidence})`, + `Reason: ${diagnosis.reason}`, + `Step outcomes: ${JSON.stringify(stepOutcomes)}`, + "", "Triage order (docs/observability-slos.md §3): inspect the failed step first.", "Resolve provider quota/auth/config failures before rerunning; for completed evals,", "check hybrid_rpc_errors and `npm run check:indexing`, then investigate corpus or code drift.", @@ -137,7 +175,7 @@ jobs: await github.rest.issues.create({ owner: context.repo.owner, repo: context.repo.repo, - title: "Eval canary failure: nightly evaluation did not complete", + title: "Eval canary failure: weekly evaluation did not complete", labels: [label], body, }); diff --git a/AGENTS.md b/AGENTS.md index 0e1ca29ca..7f86db724 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -321,15 +321,17 @@ After completing `upload`, summarize the current branch and worktree state, whet ## Repository productivity skills -Automatically apply repo-local skills under `.agents/skills/` when their descriptions match the user's request. They are thin orchestration layers over `scripts/productivity-workflow.mjs`; do not copy their procedures into this file. - -- `database-flightplan`: plan risk-scoped verification before non-trivial changes. -- `verify-triage-fix`: diagnose and repair local verification failures with the smallest reproducer. -- `clinical-change-proof`: assemble clinical, privacy, source, and rollback evidence. -- `live-design-sweep`: inspect the running app across routes, breakpoints, and accessibility modes. -- `rag-change-lab`: validate retrieval and answer changes offline first, then prepare live-eval approval gates. -- `operator-closeout`: turn pending operator debt into a deduplicated, approval-gated batch. -- `session-lifecycle`: manage safe start, handoff, merge proof, and cleanup transitions. +Automatically apply repo-local skills under `.agents/skills/` when their descriptions match the user's request. Run `npm run skills` for the validated catalog of 32 canonical single-word skills and `npm run check:skills` to verify catalog integrity. The older long names remain compatibility aliases and must not be counted as unique skills. + +The foundational orchestration skills are: + +- `plan`: plan risk-scoped verification before non-trivial changes. +- `fix`: diagnose and repair local verification failures with the smallest reproducer. +- `clinical`: assemble clinical, privacy, source, and rollback evidence. +- `ui`: inspect the running app across routes, breakpoints, and accessibility modes. +- `rag`: validate retrieval and answer changes offline first, then prepare live-eval approval gates. +- `operations`: turn pending operator debt into a deduplicated, approval-gated batch. +- `task`: manage safe start, handoff, merge proof, and cleanup transitions. Run the matching planner command in `docs/productivity-workflows.md` without side effects by default. Add `-- --run` only to execute its local/offline checks. The workflow engine must never execute commands listed under `approvalRequired`. diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index 279897a40..33231b1d2 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -629,3 +629,4 @@ Use this ledger to prevent repeated branch and PR reviews when the reviewed HEAD | 2026-07-19 | local branches and worktrees after PRs #905 and #907 | e377ab1aed44d56c303eede80572c1df82ddcd6e | final local branch/worktree cleanup and useful-history preservation | Reduced 153 local branches to `main` plus the two actively edited task branches. Deleted 150 redundant refs using cherry-pick containment, exact merged-PR heads, synthetic no-op merge trees, exact duplicate-head retention, merged-PR commit association, and explicit supersession review. Removed two clean obsolete worktrees and retained only the active auth/Supabase and P2-remediation worktrees for their owners. Archived the final 66 superseded historical heads in a verified 76,395,256-byte Git bundle before ref deletion. PRs #905 and #907 merged through protected `main`; PR #906 subsequently merged through protected `main` and its remote branch was removed. | Fresh fetch/prune and PR inventory; exact-old-value `git update-ref`; `git merge-tree --write-tree`; GraphQL commit-to-PR association for 178 commits; reverse-patch checks against a detached fresh-main worktree; verified bundle `Database-local-refs-before-final-cleanup-20260719-0525.bundle`; final branch/worktree/remote checks. No OpenAI, Supabase, deployment, live clinical, or production-data workflow ran. | | 2026-07-19 | claude/audit-recent-changes-kde66i (audit-remediation follow-up to the ef042cacd audit row) | see PR head | remediation of the three 2026-07-19 audit findings | Fixed all three findings from the 24-hour merged-window audit. (1) Inert settings honesty (P2): every preference the app does not yet consume — jurisdiction, population, answer style, landing, both home-content toggles, compact citations, and all three notification toggles — now renders an explicit "Saved for later — not active yet" marker in `settings-dialog.tsx`; the functional appearance/density/motion rows stay unmarked. Wiring the preferences into answer generation was deliberately NOT done here (clinical-behavior change requiring governance review); the new dom test documents the contract for flipping a control live. (2) Auth offline resolution (P2/P3): `resolveInitialAuthState` gains `verificationUnavailable`, set from `isAuthRetryableFetchError(getUser().error)`, so an unreachable auth server keeps the stored session signed in while a reachable server that rejects the token still resolves signed_out; server-side bearer validation is unchanged. (3) `/medications` redirect (P3): now preserves the sanitized q/focus/run search context with the same allowlist as the root legacy-mode redirect. | Focused Vitest 30/30 across `settings-inert-preferences.dom` (new), `private-client-auth` (3 new cases), `audit-navigation-auth-regressions` (query-preservation case), `app-preferences`, and `site-map`. Full `verify:cheap` run recorded on the PR. No OpenAI, Supabase, deployment, or provider-backed check ran. | | 2026-07-19 | claude/audit-recent-changes-kde66i (preference wiring + session-start hook follow-up to the #906 remediation) | see PR head | wiring the wireable inert preferences live and fixing the stale-node_modules session-start gap | Wired three of the seven remaining inert preferences into real behavior and removed their "Saved for later" markers: (1) Default landing view — `GlobalSearchShellClient` applies a one-shot `router.replace` to the saved landing mode (`search`→documents, `browse`→tools via `landingModeForPreference`) on a bare "/" load only; explicit mode/query/run params always win, and the dashboard's existing URL-sync effect performs the switch. (2) Recent searches on home — `AnswerEmptyState` now gates its recent-query chips on `showRecentOnHome`. (3) Compact citations — the answer source capsule drops its text label to icon+count when `compactCitations` is on, with the "No direct source found" warning explicitly exempted so compact mode can never hide a missing-source signal. Still marked inactive with reasons documented in the test contract: jurisdiction/population/answer-style (wiring them into answer generation is provider-eval-gated per the confirmation boundary), saved-protocols-on-home (no protocols module exists), and the three notification toggles (no delivery infrastructure). Separately, `.claude/hooks/session-start.sh` now stamps the `package-lock.json` sha256 into `node_modules/.session-start-lock-hash` after `npm ci` and reinstalls when the lockfile no longer matches, closing the stale-container gap that faked a typecheck regression during the 24h audit. | New `tests/answer-preferences.dom.test.tsx` (recents gate on/off, compact capsule display incl. the missing-source exemption, landing mapping) and the updated `settings-inert-preferences.dom` contract (3 rows moved inert→functional) pass with `app-preferences` and `private-client-auth`: 26/26 focused. Full typecheck, scoped zero-warning ESLint, and the full Vitest suite recorded on the PR; `bash -n` on the hook. `check:production-readiness` not run: no secrets in this container (documented demo-mode expectation) and no answer-generation, retrieval, or source-governance logic changed — the capsule change is presentational with the missing-source warning locked by test. No OpenAI, Supabase, deployment, or provider-backed check ran. | +| 2026-07-19 | main / `codex/supabase-database-review` | 4034d2e60ebb6616130ff17bf3cb69368f36f8f6 + reviewed working diff | live `Clinical KB Database` security, migration, schema-drift, integrity, and performance review | Confirmed and remediated private title-vocabulary exposure, applied the reviewed retrieval/audit/registry/title/rate-limit migrations, recovered and hardened the ingestion Edge Function to require POST plus a gateway-verified `service_role` claim, and restored the schema mirror and regression coverage. The review also documented the then-pending managed-role default-ACL repair and the intentional service-role-only table advisor. | Supabase project, migration, Edge Function, drift, advisor, ACL, index, Vault, post-apply, source-hash, and unauthenticated-rejection checks; focused schema/retrieval/Edge tests; offline RAG; function-grant guard; scoped lint, formatting, and `git diff --check`. Full PR-local and provider-backed follow-up gates were not run in that review task. | diff --git a/docs/observability-slos.md b/docs/observability-slos.md index 5b9790a8e..166371dd7 100644 --- a/docs/observability-slos.md +++ b/docs/observability-slos.md @@ -1,7 +1,7 @@ # Observability & SLOs Service-level objectives for the Clinical KB answer pipeline, the alert -thresholds attached to them, and the nightly production eval canary that turns +thresholds attached to them, and the weekly production eval canary that turns the golden eval into a standing guard. Written 2026-07-06. Context: this repo's defining failure mode is **silent degradation** — hybrid @@ -105,7 +105,7 @@ group by 1 order by 2 desc; ``` Complementary standing checks: `search_schema_health()` via -`npm run check:indexing` (fails closed on RPC regression) and the nightly eval +`npm run check:indexing` (fails closed on RPC regression) and the weekly eval canary below (fails closed on quality regression). ### Reliability — degraded/source-only answer rate @@ -125,19 +125,20 @@ recent pre-flag provider failures remain visible until they age out. Keep `degra the broader source-only UI state and `fallback_reason` as diagnostic detail; neither is narrow enough for provider health on its own. -## 3. Nightly production eval canary +## 3. Weekly production eval canary -`.github/workflows/eval-canary.yml` — scheduled nightly (18:00 UTC = 02:00 -Australia/Perth) plus `workflow_dispatch` for on-demand runs. +`.github/workflows/eval-canary.yml` — scheduled weekly on Sunday at 18:00 UTC +(Monday 02:00 Australia/Perth) plus `workflow_dispatch` for on-demand runs. What it does, in order: 1. `npm run check:supabase-project` — hard guard that the configured env points at `sjrfecxgysukkwxsowpy` and nothing else. 2. `npm run eval:retrieval:quality -- --fail-on-threshold` — the golden - retrieval eval (34 cases incl. forced-vector probes) against the live + retrieval eval (36 committed cases plus any captured cases, including + forced-vector probes) against the live corpus. This is the eval CI never runs on PRs (it needs live Supabase + - OpenAI keys); the canary makes it a standing nightly guard instead of a + OpenAI keys); the canary makes it a standing weekly guard instead of a manual pre-merge step that can be skipped. 3. `npm run eval:quality -- --rag-only --limit 8 --fail-on-threshold` — a small answer-quality subset (grounding, citations, unsupported-correctness) @@ -145,33 +146,38 @@ What it does, in order: Failing loudly: -- Any threshold failure fails the workflow run (red nightly badge, email per +- Any threshold failure fails the workflow run (red scheduled-run badge, email per GitHub notification settings). - On scheduled failures the workflow **opens a GitHub issue** labeled `eval-canary` (or comments on the existing open one), so a regression creates a durable, assignable artifact rather than a missed notification. +- The failure comment records every step outcome plus a deterministic failure + class derived from the captured eval logs. A completed non-zero + `failed_cases` summary is classified as a probable regression; raw provider + failures and the all-cases/no-retrieval-layers signature are classified as + provider or live-configuration failures. -Required repo secrets: `SUPABASE_SERVICE_ROLE_KEY`, `OPENAI_API_KEY`, -`E2E_USER_EMAIL` (resolved to the eval owner via `RAG_EVAL_OWNER_EMAIL`). -The workflow preflights these and fails with an explicit message when absent. +Required repo secrets: `SUPABASE_SERVICE_ROLE_KEY` and `OPENAI_API_KEY`. The workflow preflights these and fails with an explicit message when absent. Operational notes: - The canary reads live shared corpus state; a pass is a snapshot, and a failure can be corpus-state-dependent (see the clozapine-wcc history). - Triage order: rerun via `workflow_dispatch` → check `hybrid_rpc_errors` and - `check:indexing` → only then bisect code. + Triage order: inspect the recorded failure class and failed step → resolve + provider/configuration failures or inspect `hybrid_rpc_errors` and + `check:indexing` → only then bisect code. A `workflow_dispatch` rerun and all + live checks require explicit provider approval. - Forced-vector golden cases (`forceEmbedding`) run after many text-fast-path cases; the workflow sets `RAG_EVAL_CASE_DELAY_MS` and `RAG_EVAL_FORCE_EMBEDDING_DELAY_MS` so embedding calls do not exhaust the - nightly OpenAI rate limit mid-run. + scheduled run's OpenAI rate limit mid-run. - Evals write telemetry rows (`rag_queries`) but mutate no content. -- Cost bound: ~34 retrieval cases (embedding calls only on forced-vector - probes) + 8 generated answers per night. +- Cost bound: 36 committed retrieval cases plus any captured cases (embedding + calls only on forced-vector probes) + 8 generated answers per week. - **The schedule only runs from `main`.** After merging, trigger one `workflow_dispatch` run and confirm it goes green before trusting the - nightly cadence (repo gate for this workflow). + weekly cadence (repo gate for this workflow). ## 4. Degradation counters on `/api/health` (shipped) diff --git a/docs/productivity-workflows.md b/docs/productivity-workflows.md index 112a66e8e..ae9ae4831 100644 --- a/docs/productivity-workflows.md +++ b/docs/productivity-workflows.md @@ -1,5 +1,7 @@ # Productivity workflows +The repository also exposes a validated catalog of 32 single-word Database skills. Run `npm run skills` to list them by category, or `npm run check:skills` to verify that the catalog, skill folders, descriptions, and compatibility aliases agree. + The repository exposes seven offline-first workflow planners. Each planner inspects the current change through `scripts/ci-change-scope.mjs`, prints a minimal local verification sequence, and separates provider-backed commands into an explicit approval section. | Command | Purpose | diff --git a/docs/tenancy-defense-in-depth-review.md b/docs/tenancy-defense-in-depth-review.md index 9549f09bf..dc6ee7d0f 100644 --- a/docs/tenancy-defense-in-depth-review.md +++ b/docs/tenancy-defense-in-depth-review.md @@ -124,14 +124,14 @@ transcripts; this is the consolidated verdict. ### Mutations · signed URLs · upload (highest blast radius) -| Route · method | Verdict | Owner mechanism | -| ------------------------------------ | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `GET /api/documents/[id]/signed-url` | ✅ verified-scoped | `withOwnerReadScope` on doc **before** `createSignedUrl`; `storage_path` from owner-verified row ([signed-url/route.ts:40-51](src/app/api/documents/[id]/signed-url/route.ts)) | -| `GET /api/images/[id]/signed-url` | ✅ verified-scoped | image has no `owner_id`; tenancy via parent-document `withOwnerReadScope` ([images/[id]/signed-url/route.ts:49-55](src/app/api/images/[id]/signed-url/route.ts)) | -| `POST /api/documents/[id]/reindex` | ✅ verified-scoped | `requireAuthenticatedUser` + `.eq('owner_id',user.id)`; every state write re-scoped ([reindex/route.ts:110-255](src/app/api/documents/[id]/reindex/route.ts)) | -| `POST /api/documents/bulk` | ✅ verified-scoped | pre-scoping select `.eq('owner_id',user.id).in('id',ids)`; body ids intersected with ownership ([bulk/route.ts:127-204](src/app/api/documents/bulk/route.ts)) | -| `POST /api/documents/bulk/reindex` | ✅ verified-scoped | pre-scoping select `.eq('owner_id',user.id)`; per-doc writes re-scoped ([bulk/reindex/route.ts:101-247](src/app/api/documents/bulk/reindex/route.ts)) | -| `POST /api/upload` | ✅ verified-scoped | `owner_id` = session id, or configured `PUBLIC_WORKSPACE_OWNER_ID` only if public uploads enabled, else 503 ([upload/route.ts:94-97](src/app/api/upload/route.ts)); operator note TEN-N3 | +| Route · method | Verdict | Owner mechanism | +| ------------------------------------ | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `GET /api/documents/[id]/signed-url` | ✅ verified-scoped | `withOwnerReadScope` on doc **before** `createSignedUrl`; `storage_path` from owner-verified row ([signed-url/route.ts:40-51](src/app/api/documents/[id]/signed-url/route.ts)) | +| `GET /api/images/[id]/signed-url` | ✅ verified-scoped | image has no `owner_id`; tenancy via parent-document `withOwnerReadScope` ([images/[id]/signed-url/route.ts:49-55](src/app/api/images/[id]/signed-url/route.ts)) | +| `POST /api/documents/[id]/reindex` | ✅ verified-scoped | `requireAuthenticatedUser` + `.eq('owner_id',user.id)`; every state write re-scoped ([reindex/route.ts:110-255](src/app/api/documents/[id]/reindex/route.ts)) | +| `POST /api/documents/bulk` | ✅ verified-scoped | pre-scoping select `.eq('owner_id',user.id).in('id',ids)`; body ids intersected with ownership ([bulk/route.ts:127-204](src/app/api/documents/bulk/route.ts)) | +| `POST /api/documents/bulk/reindex` | ✅ verified-scoped | pre-scoping select `.eq('owner_id',user.id)`; per-doc writes re-scoped ([bulk/reindex/route.ts:101-247](src/app/api/documents/bulk/reindex/route.ts)) | +| `POST /api/upload` | ✅ admin-only | Validated Supabase session plus immutable `app_metadata.site_role = administrator`; uploaded rows remain scoped to that administrator until publication review promotes them ([upload/route.ts](src/app/api/upload/route.ts)) | ### Search @@ -336,11 +336,9 @@ new entry must be added here and to the list in the guard, or the regression tes only caller passes `user.id` ([summarize/route.ts:34](src/app/api/documents/[id]/summarize/route.ts)), so no live exploit — but make the parameter required (or fail closed) so a future caller can't reintroduce a gap. -- **TEN-N3 (operator note, intended):** with `NEXT_PUBLIC_PUBLIC_UPLOADS_ENABLED=true`, all anonymous - uploads are pooled under the single configured `PUBLIC_WORKSPACE_OWNER_ID` - ([upload/route.ts:94](src/app/api/upload/route.ts)) — anonymous X's upload is visible to anonymous Y - and the workspace owner. This is the documented public-workspace model, not a private-row A→B leak, - but operators enabling public uploads should understand it. +- **TEN-N3 (resolved):** the public-workspace upload path was removed. Anonymous and ordinary + authenticated users cannot upload; both the server route and Storage table privileges enforce the + administrator-only boundary. --- diff --git a/scripts/check-edge-functions.mjs b/scripts/check-edge-functions.mjs index 860b2a0aa..265c5a9a9 100644 --- a/scripts/check-edge-functions.mjs +++ b/scripts/check-edge-functions.mjs @@ -1,6 +1,22 @@ import { spawnSync } from "node:child_process"; +import { existsSync, readdirSync } from "node:fs"; +import { join, relative, resolve } from "node:path"; -const args = ["check", "--node-modules-dir=false", "supabase/functions/indexing-v3-agent/index.ts"]; +const root = process.cwd(); +const functionsRoot = resolve(root, "supabase/functions"); +const entrypoints = readdirSync(functionsRoot, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry) => join(functionsRoot, entry.name, "index.ts")) + .filter((entrypoint) => existsSync(entrypoint)) + .map((entrypoint) => relative(root, entrypoint).replaceAll("\\", "/")) + .sort(); + +if (entrypoints.length === 0) { + console.error("[check:edge:functions] No supabase/functions/*/index.ts entrypoints were found."); + process.exit(1); +} + +const args = ["check", "--node-modules-dir=false", "--no-lock", ...entrypoints]; const deno = spawnSync("deno", ["--version"], { encoding: "utf8" }); const denoVersionLine = deno.stdout?.split("\n")[0]?.trim() ?? ""; const denoVersionMatch = denoVersionLine.match(/^deno\s+(\d+)\./); diff --git a/scripts/list-database-skills.mjs b/scripts/list-database-skills.mjs index ada079035..a0d4c766e 100644 --- a/scripts/list-database-skills.mjs +++ b/scripts/list-database-skills.mjs @@ -1,120 +1,198 @@ -#!/usr/bin/env node -/** - * list-database-skills.mjs — list Database workflow skills under .agents/skills - * and verify the catalog stays in sync. - * - * npm run skills — print skill names and descriptions - * npm run check:skills — fail if filesystem skills and database-skills.md diverge - */ -import { readdirSync, readFileSync, statSync } from "node:fs"; +import fs from "node:fs"; import path from "node:path"; -import { fileURLToPath, pathToFileURL } from "node:url"; +import { fileURLToPath } from "node:url"; -const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); -const skillsRoot = path.join(repoRoot, ".agents", "skills"); -const catalogPath = path.join(skillsRoot, "database-skills.md"); +const scriptDirectory = path.dirname(fileURLToPath(import.meta.url)); +export const repositoryRoot = path.resolve(scriptDirectory, ".."); +export const skillsRoot = path.join(repositoryRoot, ".agents", "skills"); +export const catalogPath = path.join(skillsRoot, "catalog.json"); -function parseFrontmatter(markdown) { - const match = markdown.match(/^---\r?\n([\s\S]*?)\r?\n---/); - if (!match) return {}; - const fields = {}; +function wordCount(value) { + return String(value || "") + .trim() + .split(/\s+/) + .filter(Boolean).length; +} + +function readAgentMetadata(metadataFile) { + const content = fs.readFileSync(metadataFile, "utf8"); + return { + content, + shortDescription: content.match(/short_description:\s*"([^"]+)"/)?.[1] || "", + defaultPrompt: content.match(/default_prompt:\s*"([^"]+)"/)?.[1] || "", + }; +} + +function readFrontmatter(skillFile) { + const content = fs.readFileSync(skillFile, "utf8"); + const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/); + if (!match) throw new Error(`Missing YAML frontmatter: ${skillFile}`); + + const values = {}; for (const line of match[1].split(/\r?\n/)) { - const field = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/); - if (field) fields[field[1]] = field[2].trim(); + const separator = line.indexOf(":"); + if (separator < 1) continue; + values[line.slice(0, separator).trim()] = line.slice(separator + 1).trim(); } - return fields; + return values; } -/** Discover workflow skills from immediate child directories containing SKILL.md. */ -export function discoverSkills(root = skillsRoot, readFile = (filePath) => readFileSync(filePath, "utf8")) { - const entries = readdirSync(root, { withFileTypes: true }) +export function loadSkillCatalog(file = catalogPath) { + return JSON.parse(fs.readFileSync(file, "utf8")); +} + +export function discoverSkillDefinitions(root = skillsRoot) { + return fs + .readdirSync(root, { withFileTypes: true }) .filter((entry) => entry.isDirectory()) - .map((entry) => entry.name) - .sort(); + .map((entry) => { + const skillFile = path.join(root, entry.name, "SKILL.md"); + if (!fs.existsSync(skillFile)) return null; + const frontmatter = readFrontmatter(skillFile); + return { directory: entry.name, name: frontmatter.name, description: frontmatter.description }; + }) + .filter(Boolean) + .sort((left, right) => left.name.localeCompare(right.name)); +} - return entries.map((id) => { - const skillPath = path.join(root, id, "SKILL.md"); - if (!statSync(skillPath, { throwIfNoEntry: false })?.isFile()) { - return { id, name: id, description: "", missingSkillFile: true }; +export function validateSkillCatalog(catalog = loadSkillCatalog(), discovered = discoverSkillDefinitions()) { + const errors = []; + if (catalog.version !== 1) errors.push(`Unsupported catalog version: ${catalog.version}`); + const categories = Array.isArray(catalog.categories) ? catalog.categories : []; + if (categories.length === 0) errors.push("Catalog must contain at least one category"); + for (const category of categories) { + if (typeof category.name !== "string" || !category.name.trim()) errors.push("Catalog category is missing a name"); + if (!Array.isArray(category.skills) || category.skills.length === 0) { + errors.push(`Catalog category has no skills: ${category.name || "unnamed"}`); } - const frontmatter = parseFrontmatter(readFile(skillPath)); - return { - id, - name: frontmatter.name ?? id, - description: frontmatter.description ?? "", - missingSkillFile: false, - }; - }); -} + } + + const categoryNames = categories.map((category) => category.name); + const canonical = categories.flatMap((category) => + (Array.isArray(category.skills) ? category.skills : []).map((name) => ({ name, category: category.name })), + ); + const canonicalNames = canonical.map((skill) => skill.name); + const aliases = Object.entries(catalog.aliases || {}); + const aliasNames = aliases.map(([alias]) => alias); + const aliasTargets = new Map(aliases); + const discoveredByName = new Map(discovered.map((skill) => [skill.name, skill])); + const discoveredNames = discovered.map((skill) => skill.name); + + for (const [label, names] of [ + ["category", categoryNames], + ["canonical skill", canonicalNames], + ["alias", aliasNames], + ["discovered skill", discoveredNames], + ]) { + const duplicates = names.filter((name, index) => names.indexOf(name) !== index); + if (duplicates.length) errors.push(`Duplicate ${label} names: ${[...new Set(duplicates)].join(", ")}`); + } + + for (const skill of canonical) { + if (!/^[a-z0-9-]+$/.test(skill.name)) errors.push(`Invalid canonical skill name: ${skill.name}`); + const discoveredSkill = discoveredByName.get(skill.name); + if (!discoveredSkill) errors.push(`Missing canonical skill folder: ${skill.name}`); + else if (discoveredSkill.directory !== skill.name) { + errors.push(`Canonical skill directory mismatch: ${skill.name} is in ${discoveredSkill.directory}`); + } else { + skill.description = discoveredSkill.description; + } + } + + for (const [alias, target] of aliases) { + if (!canonicalNames.includes(target)) errors.push(`Alias ${alias} targets missing canonical skill ${target}`); + if (canonicalNames.includes(alias)) errors.push(`Alias duplicates canonical skill: ${alias}`); + if (!discoveredByName.has(alias)) errors.push(`Missing compatibility alias folder: ${alias}`); + } -/** Extract backtick skill ids from the maintained catalog markdown. */ -export function extractCatalogSkillIds(catalogMarkdown) { - const ids = new Set(); - for (const match of catalogMarkdown.matchAll(/`([a-z0-9][a-z0-9-]*)`/g)) { - ids.add(match[1]); + const allowed = new Set([...canonicalNames, ...aliasNames]); + for (const skill of discovered) { + if (!allowed.has(skill.name)) errors.push(`Uncatalogued Database skill: ${skill.name}`); + if (skill.name !== skill.directory) + errors.push(`Skill name/directory mismatch: ${skill.directory} declares ${skill.name}`); + if (!skill.description?.trim()) errors.push(`Missing skill description: ${skill.name}`); + if (wordCount(skill.description) > 60) errors.push(`Skill description exceeds 60 words: ${skill.name}`); + + const skillFile = path.join(skillsRoot, skill.directory, "SKILL.md"); + const skillContent = fs.readFileSync(skillFile, "utf8"); + const lineCount = skillContent.split(/\r?\n/).length; + const metadataFile = path.join(skillsRoot, skill.directory, "agents", "openai.yaml"); + if (!fs.existsSync(metadataFile)) { + errors.push(`Missing agents/openai.yaml: ${skill.name}`); + continue; + } + + const metadata = readAgentMetadata(metadataFile); + if (metadata.shortDescription.length < 25 || metadata.shortDescription.length > 64) { + errors.push(`Invalid short_description length for ${skill.name}: ${metadata.shortDescription.length}`); + } + if (!metadata.defaultPrompt.includes(`$${skill.name}`)) { + errors.push(`default_prompt does not mention $${skill.name}`); + } + + const aliasTarget = aliasTargets.get(skill.name); + if (aliasTarget) { + if (!skillContent.includes(`.agents/skills/${aliasTarget}/SKILL.md`)) { + errors.push(`Alias ${skill.name} does not redirect to ${aliasTarget}`); + } + if (lineCount > 15) errors.push(`Alias skill is too long: ${skill.name} (${lineCount} lines)`); + if (skillContent.includes("npm run")) errors.push(`Alias duplicates executable procedure: ${skill.name}`); + if (!metadata.content.includes("allow_implicit_invocation: false")) { + errors.push(`Alias allows implicit invocation: ${skill.name}`); + } + } else { + if (lineCount > 30) errors.push(`Canonical skill is too long: ${skill.name} (${lineCount} lines)`); + if (wordCount(skillContent) > 220) errors.push(`Canonical skill exceeds 220 words: ${skill.name}`); + } } - return [...ids].sort(); + + return { errors, canonical, aliases, discovered }; } -/** Return drift between discovered skills and the catalog. */ -export function findSkillDrift(skills, catalogIds) { - const discoveredIds = skills.filter((skill) => !skill.missingSkillFile).map((skill) => skill.id); - const discovered = new Set(discoveredIds); - const catalog = new Set(catalogIds); - const missingFromCatalog = discoveredIds.filter((id) => !catalog.has(id)); - const missingFromDisk = catalogIds.filter((id) => !discovered.has(id)); - const nameMismatches = skills - .filter((skill) => !skill.missingSkillFile && skill.name !== skill.id) - .map((skill) => ({ id: skill.id, name: skill.name })); - const missingSkillFiles = skills.filter((skill) => skill.missingSkillFile).map((skill) => skill.id); - return { missingFromCatalog, missingFromDisk, nameMismatches, missingSkillFiles }; +export function summarizeSkillDescription(description) { + return String(description || "") + .split(/\.\s+Use\b/, 1)[0] + .trim() + .replace(/\.$/, ""); } -function printSkills(skills) { - const available = skills.filter((skill) => !skill.missingSkillFile); - console.log(`Database skills (${available.length}):`); - for (const skill of available) { - const description = skill.description || "(no description)"; - console.log(`- ${skill.id} — ${description}`); +export function renderSkillCatalog(catalog = loadSkillCatalog(), discovered = discoverSkillDefinitions()) { + const validation = validateSkillCatalog(catalog, discovered); + if (validation.errors.length) throw new Error(validation.errors.join("\n")); + + const descriptions = new Map(validation.canonical.map((skill) => [skill.name, skill.description])); + const lines = [`Database skills (${validation.canonical.length})`, ""]; + for (const category of catalog.categories) { + lines.push(category.name); + for (const name of category.skills) { + lines.push(`- ${name} — ${summarizeSkillDescription(descriptions.get(name))}.`); + } + lines.push(""); } + return lines.join("\n").trimEnd(); } -function main() { - const checkMode = process.argv.includes("--check"); - const skills = discoverSkills(); - const catalogMarkdown = readFileSync(catalogPath, "utf8"); - const catalogIds = extractCatalogSkillIds(catalogMarkdown); - const drift = findSkillDrift(skills, catalogIds); - - if (!checkMode) { - printSkills(skills); +function run(argv = process.argv.slice(2)) { + const catalog = loadSkillCatalog(); + const validation = validateSkillCatalog(catalog); + if (validation.errors.length) { + console.error(validation.errors.map((error) => `- ${error}`).join("\n")); + process.exitCode = 1; return; } - const problems = []; - if (drift.missingSkillFiles.length) { - problems.push(`missing SKILL.md: ${drift.missingSkillFiles.join(", ")}`); + if (argv.includes("--json")) { + console.log(JSON.stringify({ count: validation.canonical.length, categories: catalog.categories }, null, 2)); + return; } - if (drift.nameMismatches.length) { - problems.push( - `frontmatter name mismatch: ${drift.nameMismatches.map((item) => `${item.id} -> ${item.name}`).join(", ")}`, + if (argv.includes("--check")) { + console.log( + `Database skill catalog valid: ${validation.canonical.length} canonical skills, ${validation.aliases.length} aliases.`, ); + return; } - if (drift.missingFromCatalog.length) { - problems.push(`not listed in database-skills.md: ${drift.missingFromCatalog.join(", ")}`); - } - if (drift.missingFromDisk.length) { - problems.push(`catalog entries without skill directories: ${drift.missingFromDisk.join(", ")}`); - } - - if (problems.length > 0) { - console.error("skills check FAILED:"); - for (const problem of problems) console.error(`- ${problem}`); - process.exit(1); - } - - console.log(`skills check passed: ${skills.length} workflow skill(s) match database-skills.md.`); + console.log(renderSkillCatalog(catalog)); } -const invokedDirectly = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href; -if (invokedDirectly) main(); +const invokedPath = process.argv[1] ? path.resolve(process.argv[1]) : ""; +if (invokedPath === fileURLToPath(import.meta.url)) run(); diff --git a/scripts/productivity-core.mjs b/scripts/productivity-core.mjs index 43f634bdc..1f51d2a93 100644 --- a/scripts/productivity-core.mjs +++ b/scripts/productivity-core.mjs @@ -276,13 +276,29 @@ export function buildWorkflowPlan(workflow, files = [], options = {}) { export function analyzeFailureText(text = "", knownFlakes = []) { const value = String(text); const lower = value.toLowerCase(); + const goldenCaseCount = Number.parseInt(lower.match(/(?:^|\n)\s*cases=(\d+)/)?.[1] ?? "", 10); + const goldenFailureCount = Number.parseInt(lower.match(/(?:^|\n)\s*failed_cases=(\d+)/)?.[1] ?? "", 10); + const retrievalLayers = lower.match(/(?:^|\n)\s*retrieval_layer_counts=(\{[^\n]*\})/)?.[1]; const knownFlake = knownFlakes.find((entry) => entry.pattern && lower.includes(String(entry.pattern).toLowerCase())); if (knownFlake) return { category: "known-flake", confidence: "high", reason: knownFlake.id || knownFlake.pattern }; if (/module_not_found|cannot find module|enoent|not recognized as an internal|command not found/.test(lower)) { return { category: "environment", confidence: "high", reason: "Missing executable, module, or path." }; } if ( - /missing.*(?:api[_ -]?key|secret|credential)|(?:api[_ -]?key|secret|credential)\s+(?:is\s+)?missing|unauthorized|forbidden|quota|rate limit|supabase project/.test( + Number.isInteger(goldenCaseCount) && + goldenCaseCount > 0 && + goldenFailureCount === goldenCaseCount && + retrievalLayers === "{}" + ) { + return { + category: "provider-or-configuration", + confidence: "high", + reason: + "Every golden case failed without any retrieval layer; verify eval owner, corpus scope, and live retrieval health.", + }; + } + if ( + /missing.*(?:api[_ -]?key|secret|credential)|(?:api[_ -]?key|secret|credential)\s+(?:is\s+)?missing|unauthorized|forbidden|unregistered api key|insufficient[_ -]?quota|quota|billing|\b429\b|rate[_ -]?limit|too many requests|supabase project/.test( lower, ) ) { @@ -292,6 +308,13 @@ export function analyzeFailureText(text = "", knownFlakes = []) { reason: "Credentials, authorization, quota, or live-provider configuration.", }; } + if (Number.isInteger(goldenFailureCount) && goldenFailureCount > 0) { + return { + category: "probable-regression", + confidence: "high", + reason: `A completed golden evaluation reported ${goldenFailureCount} failed case(s).`, + }; + } if (/timed? out|timeout|etimedout|browser has been closed|worker.*exited/.test(lower)) { return { category: "environment-or-timeout", diff --git a/supabase/config.toml b/supabase/config.toml index 268d42797..75daa99c9 100644 --- a/supabase/config.toml +++ b/supabase/config.toml @@ -1,2 +1,5 @@ [functions.indexing-v3-agent] verify_jwt = false + +[functions.ingestion-worker] +verify_jwt = true diff --git a/supabase/drift-manifest.json b/supabase/drift-manifest.json index fad433e4f..ec7745dae 100644 --- a/supabase/drift-manifest.json +++ b/supabase/drift-manifest.json @@ -1,9 +1,9 @@ { - "generated_at": "2026-07-19T09:58:22.040Z", + "generated_at": "2026-07-19T10:43:08.249Z", "generator": "scripts/generate-drift-manifest.ts", "postgres_image": "supabase/postgres:17.6.1.127", - "schema_sha256": "73d993e84e97b036363501a78788c27444c894feb5c4901b7cbd3ccead8e38a5", - "replay_seconds": 73, + "schema_sha256": "cf3e4a10899bbc691f21c1934be872ba01c9589302b9fd23528bb21e683b53ad", + "replay_seconds": 44, "snapshot": { "views": [ { @@ -6918,7 +6918,7 @@ "postgres=X/postgres", "service_role=X/postgres" ], - "def_hash": "4bf81d62c0953a8d11c80c37de6045e7", + "def_hash": "3d99483e01a5c93374408b9e585d3962", "signature": "public.match_document_chunks_text_v2(text,integer,uuid[],uuid,boolean)" }, { @@ -6982,7 +6982,7 @@ "postgres=X/postgres", "service_role=X/postgres" ], - "def_hash": "38282636c4c22a4f01974a943aa20d42", + "def_hash": "b72c524f3be13ec1a950cc30e922ec78", "signature": "public.match_document_index_units_hybrid_v2(extensions.vector,text,integer,double precision,uuid[],uuid,boolean)" }, { diff --git a/supabase/functions/ingestion-worker/auth.ts b/supabase/functions/ingestion-worker/auth.ts new file mode 100644 index 000000000..aec9c87a3 --- /dev/null +++ b/supabase/functions/ingestion-worker/auth.ts @@ -0,0 +1,30 @@ +type JwtPayload = { + role?: unknown; +}; + +function decodeJwtPayload(token: string): JwtPayload | null { + const parts = token.split("."); + if (parts.length !== 3 || !parts[1]) return null; + + try { + const base64 = parts[1] + .replace(/-/g, "+") + .replace(/_/g, "/") + .padEnd(Math.ceil(parts[1].length / 4) * 4, "="); + const payload = JSON.parse(atob(base64)) as unknown; + return payload && typeof payload === "object" && !Array.isArray(payload) ? (payload as JwtPayload) : null; + } catch { + return null; + } +} + +/** + * The Edge gateway verifies the JWT before the request reaches this function + * (`verify_jwt = true`). This second authorization gate restricts the + * privileged direct-Postgres worker to the service-role cron token. + */ +export function hasServiceRoleAuthorization(authorization: string | null): boolean { + const token = authorization?.match(/^Bearer\s+(.+)$/i)?.[1]?.trim(); + if (!token) return false; + return decodeJwtPayload(token)?.role === "service_role"; +} diff --git a/supabase/functions/ingestion-worker/index.ts b/supabase/functions/ingestion-worker/index.ts new file mode 100644 index 000000000..db39cbe0b --- /dev/null +++ b/supabase/functions/ingestion-worker/index.ts @@ -0,0 +1,311 @@ +import "jsr:@supabase/functions-js/edge-runtime.d.ts"; +import postgres from "npm:postgres@3.4.7"; + +import { hasServiceRoleAuthorization } from "./auth.ts"; + +declare const Supabase: { + ai: { + Session: new (model: string) => { + run(input: string, options: { mean_pool: boolean; normalize: boolean }): Promise; + }; + }; +}; + +type ClaimedJob = { + id: string; + document_id: string; + batch_id: string | null; + attempt_count: number; + max_attempts: number; + documents: { + id: string; + owner_id: string | null; + title: string | null; + metadata: Record | null; + }; +}; + +type LeaseRpcRow = { + result: { ok?: boolean } | null; +}; + +const SUPABASE_DB_URL = Deno.env.get("SUPABASE_DB_URL"); +if (!SUPABASE_DB_URL) throw new Error("SUPABASE_DB_URL is required"); + +const sql = postgres(SUPABASE_DB_URL, { + max: 3, + idle_timeout: 20, + connect_timeout: 10, +}); + +const embeddingModel = new Supabase.ai.Session("gte-small"); + +function normalizeText(input: string): string { + return input.replace(/\s+/g, " ").trim(); +} + +async function sha256Hex(input: string): Promise { + const data = new TextEncoder().encode(input); + const digest = await crypto.subtle.digest("SHA-256", data); + return Array.from(new Uint8Array(digest)) + .map((b) => b.toString(16).padStart(2, "0")) + .join(""); +} + +async function generateEmbedding(text: string): Promise { + const vector = await embeddingModel.run(text, { mean_pool: true, normalize: true }); + if (!Array.isArray(vector)) throw new Error("Embedding model returned non-array output"); + return vector as number[]; +} + +async function buildSummary(documentId: string): Promise<{ summary: string; sourceChunkIds: string[] }> { + const chunks = await sql< + { + id: string; + content: string; + chunk_index: number; + }[] + >` + select id, content, chunk_index + from public.document_chunks + where document_id = ${documentId}::uuid + order by chunk_index asc + limit 8 + `; + + if (chunks.length === 0) { + return { + summary: "No chunk content available for this document yet.", + sourceChunkIds: [], + }; + } + + const merged = normalizeText(chunks.map((c) => c.content ?? "").join(" ")); + const summary = merged.length > 1600 ? `${merged.slice(0, 1600)}...` : merged; + + return { + summary, + sourceChunkIds: chunks.map((c) => c.id), + }; +} + +async function upsertDocumentSummary(job: ClaimedJob): Promise { + const docId = job.document_id; + const ownerId = job.documents.owner_id; + + const existing = await sql<{ id: string }[]>` + select id + from public.document_summaries + where document_id = ${docId}::uuid + limit 1 + `; + + if (existing.length > 0) { + const row = await sql<{ summary: string | null }[]>` + select summary + from public.document_summaries + where document_id = ${docId}::uuid + limit 1 + `; + + return normalizeText(row[0]?.summary ?? ""); + } + + const { summary, sourceChunkIds } = await buildSummary(docId); + + await sql` + insert into public.document_summaries ( + document_id, + owner_id, + summary, + source_chunk_ids, + model, + metadata, + generated_at + ) values ( + ${docId}::uuid, + ${ownerId}::uuid, + ${summary}, + ${sourceChunkIds}::uuid[], + ${"gte-small-heuristic-summary-v1"}, + ${JSON.stringify({ generated_by: "ingestion-worker", mode: "backfill" })}::jsonb, + now() + ) + on conflict (document_id) + do update set + summary = excluded.summary, + source_chunk_ids = excluded.source_chunk_ids, + model = excluded.model, + metadata = excluded.metadata, + generated_at = now(), + updated_at = now() + `; + + return summary; +} + +async function upsertEmbeddingFields(job: ClaimedJob, summaryText: string): Promise { + const docId = job.document_id; + const ownerId = job.documents.owner_id; + const title = normalizeText(job.documents.title ?? ""); + const summary = normalizeText(summaryText); + + const entries = [ + { fieldType: "document_title", content: title.length > 0 ? title : "Untitled document" }, + { fieldType: "document_summary", content: summary.length > 0 ? summary : "Summary unavailable" }, + ]; + + const prepared = await Promise.all( + entries.map(async (entry) => ({ + ...entry, + embedding: await generateEmbedding(entry.content), + contentHash: await sha256Hex(entry.content), + })), + ); + + await sql.begin(async (transaction) => { + await transaction` + delete from public.document_embedding_fields + where document_id = ${docId}::uuid + and field_type = any(${entries.map((entry) => entry.fieldType)}::text[]) + `; + + for (const entry of prepared) { + await transaction` + insert into public.document_embedding_fields ( + owner_id, + document_id, + source_chunk_id, + field_type, + content, + embedding, + metadata, + content_hash + ) values ( + ${ownerId}::uuid, + ${docId}::uuid, + null, + ${entry.fieldType}, + ${entry.content}, + ${JSON.stringify(entry.embedding)}::vector, + ${JSON.stringify({ generated_by: "ingestion-worker", mode: "backfill" })}::jsonb, + ${entry.contentHash} + ) + `; + } + }); +} + +async function markEnrichmentMetadata(documentId: string): Promise { + await sql` + update public.documents + set + metadata = coalesce(metadata, '{}'::jsonb) + || jsonb_build_object( + 'enrichment_status', 'completed', + 'rag_enrichment_updated_at', now() + ), + updated_at = now() + where id = ${documentId}::uuid + `; +} + +function leaseMutationSucceeded(rows: LeaseRpcRow[]): boolean { + return rows[0]?.result?.ok === true; +} + +async function processJob(job: ClaimedJob, workerId: string): Promise { + const summary = await upsertDocumentSummary(job); + await upsertEmbeddingFields(job, summary); + await markEnrichmentMetadata(job.document_id); + + const completion = await sql` + select public.complete_ingestion_job( + ${job.id}::uuid, + ${job.document_id}::uuid, + ${job.batch_id}::uuid, + ${"indexed + enrichment backfill"}, + ${workerId} + ) as result + `; + + return leaseMutationSucceeded(completion); +} + +Deno.serve(async (req: Request) => { + try { + if (req.method !== "POST") { + return new Response("Method not allowed", { status: 405, headers: { Allow: "POST" } }); + } + if (!hasServiceRoleAuthorization(req.headers.get("authorization"))) { + return Response.json({ ok: false, error: "Unauthorized" }, { status: 401 }); + } + + const url = new URL(req.url); + const limitRaw = url.searchParams.get("limit") ?? "10"; + const limit = Number.isFinite(Number(limitRaw)) ? Math.max(1, Math.min(50, Number(limitRaw))) : 10; + const workerId = `edge-ingestion-worker-${crypto.randomUUID()}`; + + const claimed = await sql` + select * + from public.claim_ingestion_jobs(${workerId}, ${limit}, 45) + `; + + if (claimed.length === 0) { + return Response.json({ ok: true, claimed: 0, processed: 0, failed: 0 }); + } + + let processed = 0; + let failed = 0; + let leaseLost = 0; + const failures: Array<{ job_id: string; document_id: string; error: string }> = []; + + for (const job of claimed) { + try { + const completed = await processJob(job, workerId); + if (!completed) { + leaseLost += 1; + continue; + } + processed += 1; + } catch (error) { + const message = error instanceof Error ? error.message : JSON.stringify(error); + const shouldRetry = job.attempt_count < job.max_attempts; + + const failureUpdate = await sql` + select public.fail_or_retry_ingestion_job( + ${job.id}::uuid, + ${job.document_id}::uuid, + ${job.batch_id}::uuid, + ${shouldRetry}, + ${"indexed"}, + ${"enrichment backfill failed"}, + ${message}, + ${new Date(Date.now() + 60_000).toISOString()}::timestamptz, + ${workerId} + ) as result + `; + + if (!leaseMutationSucceeded(failureUpdate)) { + leaseLost += 1; + continue; + } + + failed += 1; + failures.push({ job_id: job.id, document_id: job.document_id, error: message }); + } + } + + return Response.json({ + ok: true, + claimed: claimed.length, + processed, + failed, + lease_lost: leaseLost, + failures, + }); + } catch (error) { + const message = error instanceof Error ? error.message : JSON.stringify(error); + return Response.json({ ok: false, error: message }, { status: 500 }); + } +}); diff --git a/supabase/schema.sql b/supabase/schema.sql index 35bab0eea..29d4a4e34 100644 --- a/supabase/schema.sql +++ b/supabase/schema.sql @@ -7599,7 +7599,13 @@ stable set search_path = public, extensions, pg_temp as $$ select * - from public.match_document_chunks_text_scoped($1, $2, $3, $4, $5); + from public.match_document_chunks_text_scoped( + $1, + least(greatest(coalesce($2, 12), 1), 96), + $3, + $4, + $5 + ); $$; create or replace function public.match_document_chunks_hybrid_v2( @@ -7776,7 +7782,15 @@ stable set search_path = public, extensions, pg_temp as $$ select * - from public.match_document_index_units_hybrid_scoped($1, $2, $3, $4, $5, $6, $7); + from public.match_document_index_units_hybrid_scoped( + $1, + $2, + least(greatest(coalesce($3, 24), 1), 96), + $4, + $5, + $6, + $7 + ); $$; create or replace function public.match_document_memory_cards_hybrid_v3( diff --git a/tests/database-skills.test.ts b/tests/database-skills.test.ts new file mode 100644 index 000000000..a98fa8d6e --- /dev/null +++ b/tests/database-skills.test.ts @@ -0,0 +1,86 @@ +import fs from "node:fs"; +import path from "node:path"; +import { describe, expect, it } from "vitest"; + +import { + discoverSkillDefinitions, + loadSkillCatalog, + renderSkillCatalog, + skillsRoot, + validateSkillCatalog, +} from "../scripts/list-database-skills.mjs"; + +type SkillDefinition = { + directory: string; + name: string; + description: string; +}; + +type CatalogSkill = { + name: string; + category: string; + description?: string; +}; + +describe("Database skill catalog", () => { + it("contains every canonical skill exactly once and validates every alias", () => { + const catalog = loadSkillCatalog(); + const result = validateSkillCatalog(); + + expect(result.errors).toEqual([]); + expect(result.canonical).toHaveLength(32); + expect(new Set(result.canonical.map((skill: CatalogSkill) => skill.name))).toHaveProperty("size", 32); + expect(result.aliases).toHaveLength(8); + for (const category of catalog.categories) { + expect(category.skills.every((skill: unknown) => typeof skill === "string")).toBe(true); + } + }); + + it("discovers each declared skill from its folder metadata", () => { + const discovered = discoverSkillDefinitions() as SkillDefinition[]; + + expect(discovered).toHaveLength(40); + for (const skill of discovered) { + const metadataPath = path.join(skillsRoot, skill.name, "agents", "openai.yaml"); + expect(skill.directory).toBe(skill.name); + expect(skill.description).toBeTruthy(); + expect(fs.existsSync(metadataPath)).toBe(true); + + const metadata = fs.readFileSync(metadataPath, "utf8"); + const shortDescription = metadata.match(/short_description:\s*"([^"]+)"/)?.[1] || ""; + const defaultPrompt = metadata.match(/default_prompt:\s*"([^"]+)"/)?.[1] || ""; + expect(shortDescription.length).toBeGreaterThanOrEqual(25); + expect(shortDescription.length).toBeLessThanOrEqual(64); + expect(defaultPrompt).toContain(`$${skill.name}`); + } + }); + + it("keeps canonical skills compact and compatibility aliases as one-hop redirects", () => { + const result = validateSkillCatalog(); + + for (const skill of result.canonical) { + const content = fs.readFileSync(path.join(skillsRoot, skill.name, "SKILL.md"), "utf8"); + expect(content.split(/\r?\n/).length).toBeLessThanOrEqual(30); + expect(content.split(/\s+/).filter(Boolean).length).toBeLessThanOrEqual(220); + } + + for (const [alias, target] of result.aliases) { + const content = fs.readFileSync(path.join(skillsRoot, alias, "SKILL.md"), "utf8"); + const metadata = fs.readFileSync(path.join(skillsRoot, alias, "agents", "openai.yaml"), "utf8"); + expect(content).toContain(`.agents/skills/${target}/SKILL.md`); + expect(content.split(/\r?\n/).length).toBeLessThanOrEqual(15); + expect(content).not.toContain("npm run"); + expect(metadata).toContain("allow_implicit_invocation: false"); + } + }); + + it("renders canonical skills by category without duplicating compatibility aliases", () => { + const catalog = loadSkillCatalog(); + const rendered = renderSkillCatalog(catalog); + + expect(rendered).toContain("Database skills (32)"); + expect(rendered).toContain("- skills — List every unique Database-specific skill with a clear explanation"); + expect(rendered).not.toContain("- workflows —"); + for (const category of catalog.categories) expect(rendered).toContain(category.name); + }); +}); diff --git a/tests/eval-canary-workflow.test.ts b/tests/eval-canary-workflow.test.ts index b41c83891..69b40d2d1 100644 --- a/tests/eval-canary-workflow.test.ts +++ b/tests/eval-canary-workflow.test.ts @@ -4,6 +4,12 @@ import { describe, expect, it } from "vitest"; const workflow = readFileSync(new URL("../.github/workflows/eval-canary.yml", import.meta.url), "utf8"); describe("eval canary workflow input", () => { + it("runs weekly on Monday morning in Australia/Perth", () => { + expect(workflow).toContain('- cron: "0 18 * * 0"'); + expect(workflow).not.toContain('- cron: "0 18 * * *"'); + expect(workflow).toContain("Sunday 18:00 UTC = Monday 02:00 Australia/Perth"); + }); + it("validates the dispatch limit outside shell source and passes it as one quoted argument", () => { expect(workflow).toContain("ANSWER_CASE_LIMIT: ${{ github.event.inputs.answer_case_limit || '8' }}"); expect(workflow).toContain('[[ ! "$ANSWER_CASE_LIMIT" =~ ^[0-9]+$ ]]'); @@ -13,11 +19,23 @@ describe("eval canary workflow input", () => { }); it("distinguishes provider outages from retrieval regressions in the failure issue", () => { - expect(workflow).toContain('title: "Eval canary failure: nightly evaluation did not complete"'); + expect(workflow).toContain('title: "Eval canary failure: weekly evaluation did not complete"'); expect(workflow).toContain("Resolve provider quota/auth/config failures before rerunning"); expect(workflow).toContain( "Do not bisect or revert code until provider health and the failure class are confirmed", ); - expect(workflow).not.toContain("Eval canary regression: nightly golden eval failed"); + expect(workflow).not.toContain("Eval canary regression:"); + }); + + it("captures eval logs and adds deterministic failure classification to the issue", () => { + expect(workflow).toContain("set -o pipefail"); + expect(workflow).toContain("tee .local/eval-canary/golden-retrieval.log"); + expect(workflow).toContain("tee .local/eval-canary/answer-quality.log"); + expect(workflow).toContain( + "await import(pathToFileURL(`${process.env.GITHUB_WORKSPACE}/scripts/productivity-core.mjs`).href)", + ); + expect(workflow).toContain("analyzeFailureText(failureText)"); + expect(workflow).toContain("`Failure class: ${diagnosis.category} (${diagnosis.confidence})`"); + expect(workflow).toContain("`Step outcomes: ${JSON.stringify(stepOutcomes)}`"); }); }); diff --git a/tests/forms-back-navigation.dom.test.tsx b/tests/forms-back-navigation.dom.test.tsx index 501df680f..a668315c4 100644 --- a/tests/forms-back-navigation.dom.test.tsx +++ b/tests/forms-back-navigation.dom.test.tsx @@ -13,6 +13,13 @@ vi.mock("next/navigation", () => ({ useRouter: () => router, })); +vi.mock("@/components/account-data-provider", () => ({ + useAccountData: () => ({ + isSaved: () => false, + setFavourite: vi.fn(async () => true), + }), +})); + beforeEach(() => { router.back.mockReset(); router.push.mockReset(); diff --git a/tests/ingestion-edge-function-auth.test.ts b/tests/ingestion-edge-function-auth.test.ts new file mode 100644 index 000000000..32c16678f --- /dev/null +++ b/tests/ingestion-edge-function-auth.test.ts @@ -0,0 +1,52 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + +import { describe, expect, it } from "vitest"; + +import { hasServiceRoleAuthorization } from "../supabase/functions/ingestion-worker/auth"; + +const root = process.cwd(); +const source = readFileSync(join(root, "supabase/functions/ingestion-worker/index.ts"), "utf8"); +const config = readFileSync(join(root, "supabase/config.toml"), "utf8"); + +function tokenForRole(role: string): string { + const encode = (value: unknown) => Buffer.from(JSON.stringify(value)).toString("base64url"); + return `${encode({ alg: "HS256", typ: "JWT" })}.${encode({ role })}.signature`; +} + +describe("ingestion-worker Edge Function authorization", () => { + it("accepts only a gateway-verified service-role JWT", () => { + expect(hasServiceRoleAuthorization(`Bearer ${tokenForRole("service_role")}`)).toBe(true); + expect(hasServiceRoleAuthorization(`Bearer ${tokenForRole("authenticated")}`)).toBe(false); + expect(hasServiceRoleAuthorization(`Bearer ${tokenForRole("anon")}`)).toBe(false); + expect(hasServiceRoleAuthorization("Bearer malformed")).toBe(false); + expect(hasServiceRoleAuthorization(null)).toBe(false); + }); + + it("keeps gateway verification enabled and rejects state-changing GET requests", () => { + expect(config).toContain("[functions.ingestion-worker]\nverify_jwt = true"); + expect(source).toContain('if (req.method !== "POST")'); + expect(source).toContain('hasServiceRoleAuthorization(req.headers.get("authorization"))'); + expect(source.indexOf("hasServiceRoleAuthorization")).toBeLessThan(source.indexOf("public.claim_ingestion_jobs")); + }); + + it("fences completion and failure mutations to the worker that owns the lease", () => { + expect(source).toContain("async function processJob(job: ClaimedJob, workerId: string): Promise"); + expect(source).toMatch(/complete_ingestion_job\([\s\S]*?\$\{workerId\}[\s\S]*?\) as result/); + expect(source).toMatch(/fail_or_retry_ingestion_job\([\s\S]*?\$\{workerId\}[\s\S]*?\) as result/); + expect(source).toContain("rows[0]?.result?.ok === true"); + expect(source).toContain("lease_lost: leaseLost"); + }); + + it("prepares replacement embeddings before swapping them atomically", () => { + const prepareIndex = source.indexOf("const prepared = await Promise.all"); + const transactionIndex = source.indexOf("await sql.begin(async (transaction)"); + const deleteIndex = source.indexOf("delete from public.document_embedding_fields"); + + expect(prepareIndex).toBeGreaterThan(0); + expect(transactionIndex).toBeGreaterThan(prepareIndex); + expect(deleteIndex).toBeGreaterThan(transactionIndex); + expect(source).toContain("for (const entry of prepared)"); + expect(source).toContain("await transaction`"); + }); +}); diff --git a/tests/list-database-skills.test.ts b/tests/list-database-skills.test.ts deleted file mode 100644 index 0f826e08d..000000000 --- a/tests/list-database-skills.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { readFileSync } from "node:fs"; -import { describe, expect, it } from "vitest"; - -import { discoverSkills, extractCatalogSkillIds, findSkillDrift } from "../scripts/list-database-skills.mjs"; - -describe("list-database-skills", () => { - it("extracts frontmatter-backed skill metadata from a directory listing", () => { - const drift = findSkillDrift( - [ - { - id: "database-flightplan", - name: "database-flightplan", - description: "Plan safe work.", - missingSkillFile: false, - }, - { id: "workflows", name: "workflows", description: "List workflow skills.", missingSkillFile: false }, - ], - ["database-flightplan", "workflows"], - ); - expect(drift).toEqual({ - missingFromCatalog: [], - missingFromDisk: [], - nameMismatches: [], - missingSkillFiles: [], - }); - }); - - it("extracts catalog skill ids from database-skills.md", () => { - const catalog = readFileSync(new URL("../.agents/skills/database-skills.md", import.meta.url), "utf8"); - const ids = extractCatalogSkillIds(catalog); - expect(ids).toContain("database-flightplan"); - expect(ids).toContain("workflows"); - expect(ids.length).toBeGreaterThanOrEqual(8); - }); - - it("passes check mode against the committed skills catalog", () => { - const catalog = readFileSync(new URL("../.agents/skills/database-skills.md", import.meta.url), "utf8"); - const drift = findSkillDrift(discoverSkills(), extractCatalogSkillIds(catalog)); - expect(drift.missingSkillFiles).toEqual([]); - expect(drift.nameMismatches).toEqual([]); - expect(drift.missingFromCatalog).toEqual([]); - expect(drift.missingFromDisk).toEqual([]); - }); -}); diff --git a/tests/productivity-workflow.test.ts b/tests/productivity-workflow.test.ts index 219a1c5c4..06d6f18bc 100644 --- a/tests/productivity-workflow.test.ts +++ b/tests/productivity-workflow.test.ts @@ -111,6 +111,35 @@ describe("productivity workflow planning", () => { expect(analyzeFailureText("TypeError: value is not iterable").category).toBe("probable-regression"); }); + it("distinguishes historical eval-canary provider failures from a completed golden regression", () => { + const july7OwnerMismatch = [ + "Golden retrieval eval summary:", + " cases=36", + " retrieval_layer_counts={}", + " failed_cases=36", + ].join("\n"); + const july10ProviderThrottle = "Error: 429 Too Many Requests while creating an embedding"; + const completedGoldenRegression = [ + "Golden retrieval eval summary:", + " cases=36", + ' retrieval_layer_counts={"lexical":72,"hybrid_vector":36}', + " failed_cases=3", + ].join("\n"); + + expect(analyzeFailureText(july7OwnerMismatch)).toMatchObject({ + category: "provider-or-configuration", + confidence: "high", + }); + expect(analyzeFailureText(july10ProviderThrottle)).toMatchObject({ + category: "provider-or-configuration", + confidence: "high", + }); + expect(analyzeFailureText(completedGoldenRegression)).toMatchObject({ + category: "probable-regression", + confidence: "high", + }); + }); + it("extracts only actionable operator markers", () => { const items = extractOperatorItemsFromText( "# Heading\n| Live gate | ⏳ pending | run later |\n- Operator-only: rotate key\n- completed", diff --git a/tests/retrieval-access-scope.test.ts b/tests/retrieval-access-scope.test.ts index fd431e811..453add950 100644 --- a/tests/retrieval-access-scope.test.ts +++ b/tests/retrieval-access-scope.test.ts @@ -98,7 +98,7 @@ describe("owner-plus-public retrieval contract", () => { ? "create or replace function public.match_document_chunks_hybrid_v2(" : "create or replace function public.match_document_index_units_hybrid_v2(", ); - expect(textWrapper).toContain("from public.match_document_chunks_text_scoped($1, $2, $3, $4, $5)"); + expect(textWrapper).toContain("from public.match_document_chunks_text_scoped("); expect(textWrapper).not.toContain("union all"); const indexWrapper = between( @@ -108,9 +108,7 @@ describe("owner-plus-public retrieval contract", () => { ? "create or replace function public.match_document_memory_cards_hybrid_v3(" : "revoke all on function public.match_document_chunks_text_scoped(", ); - expect(indexWrapper).toContain( - "from public.match_document_index_units_hybrid_scoped($1, $2, $3, $4, $5, $6, $7)", - ); + expect(indexWrapper).toContain("from public.match_document_index_units_hybrid_scoped("); expect(indexWrapper).not.toContain("union all"); } }); @@ -119,10 +117,12 @@ describe("owner-plus-public retrieval contract", () => { // LIMIT NULL and LIMIT -1 mean "no limit" in PostgreSQL. The wrappers are // the RPC boundary, so clamp here rather than trusting every service-role // caller to validate an optional PostgREST argument. - expect(boundedRetrievalMigration).toContain("coalesce($2, 12)"); - expect(boundedRetrievalMigration).toContain("coalesce($3, 24)"); - expect(boundedRetrievalMigration).toContain("least(greatest(coalesce($2, 12), 1), 96)"); - expect(boundedRetrievalMigration).toContain("least(greatest(coalesce($3, 24), 1), 96)"); + for (const source of [boundedRetrievalMigration, canonicalSchema]) { + expect(source).toContain("coalesce($2, 12)"); + expect(source).toContain("coalesce($3, 24)"); + expect(source).toContain("least(greatest(coalesce($2, 12), 1), 96)"); + expect(source).toContain("least(greatest(coalesce($3, 24), 1), 96)"); + } expect(boundedRetrievalMigration).toContain( "revoke all on function public.match_document_chunks_text_v2(text, integer, uuid[], uuid, boolean)\n from public, anon, authenticated;", );