From ccd53914218d3cbab52e95ef9166899cf92c3197 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Fri, 17 Jul 2026 18:46:43 +0800 Subject: [PATCH 1/7] feat(workflow): add risk-scoped productivity toolkit --- .agents/skills/clinical-change-proof/SKILL.md | 20 + .../clinical-change-proof/agents/openai.yaml | 4 + .agents/skills/database-flightplan/SKILL.md | 18 + .../database-flightplan/agents/openai.yaml | 4 + .agents/skills/live-design-sweep/SKILL.md | 17 + .../live-design-sweep/agents/openai.yaml | 4 + .agents/skills/operator-closeout/SKILL.md | 23 ++ .../operator-closeout/agents/openai.yaml | 4 + .agents/skills/rag-change-lab/SKILL.md | 16 + .../skills/rag-change-lab/agents/openai.yaml | 4 + .agents/skills/session-lifecycle/SKILL.md | 17 + .../session-lifecycle/agents/openai.yaml | 4 + .agents/skills/verify-triage-fix/SKILL.md | 21 + .../verify-triage-fix/agents/openai.yaml | 4 + .gitignore | 3 + AGENTS.md | 18 + docs/productivity-workflows.md | 27 ++ package.json | 21 +- scripts/ci-change-scope.mjs | 9 +- scripts/external-workflow.mjs | 64 ++++ scripts/productivity-core.mjs | 361 ++++++++++++++++++ scripts/productivity-workflow.mjs | 150 ++++++++ tests/productivity-workflow.test.ts | 88 +++++ 23 files changed, 893 insertions(+), 8 deletions(-) create mode 100644 .agents/skills/clinical-change-proof/SKILL.md create mode 100644 .agents/skills/clinical-change-proof/agents/openai.yaml create mode 100644 .agents/skills/database-flightplan/SKILL.md create mode 100644 .agents/skills/database-flightplan/agents/openai.yaml create mode 100644 .agents/skills/live-design-sweep/SKILL.md create mode 100644 .agents/skills/live-design-sweep/agents/openai.yaml create mode 100644 .agents/skills/operator-closeout/SKILL.md create mode 100644 .agents/skills/operator-closeout/agents/openai.yaml create mode 100644 .agents/skills/rag-change-lab/SKILL.md create mode 100644 .agents/skills/rag-change-lab/agents/openai.yaml create mode 100644 .agents/skills/session-lifecycle/SKILL.md create mode 100644 .agents/skills/session-lifecycle/agents/openai.yaml create mode 100644 .agents/skills/verify-triage-fix/SKILL.md create mode 100644 .agents/skills/verify-triage-fix/agents/openai.yaml create mode 100644 docs/productivity-workflows.md create mode 100644 scripts/external-workflow.mjs create mode 100644 scripts/productivity-core.mjs create mode 100644 scripts/productivity-workflow.mjs create mode 100644 tests/productivity-workflow.test.ts diff --git a/.agents/skills/clinical-change-proof/SKILL.md b/.agents/skills/clinical-change-proof/SKILL.md new file mode 100644 index 000000000..613f73335 --- /dev/null +++ b/.agents/skills/clinical-change-proof/SKILL.md @@ -0,0 +1,20 @@ +--- +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. +--- + +# Clinical Change Proof + +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. Present every live Supabase, OpenAI, hosted CI, or production-readiness command as a separate approval request. Never bundle approval implicitly into a local gate. +7. Complete the Clinical Governance Preflight and report evidence, gaps, rollback, and any SaMD implication. diff --git a/.agents/skills/clinical-change-proof/agents/openai.yaml b/.agents/skills/clinical-change-proof/agents/openai.yaml new file mode 100644 index 000000000..b526d3734 --- /dev/null +++ b/.agents/skills/clinical-change-proof/agents/openai.yaml @@ -0,0 +1,4 @@ +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." diff --git a/.agents/skills/database-flightplan/SKILL.md b/.agents/skills/database-flightplan/SKILL.md new file mode 100644 index 000000000..8fd01a935 --- /dev/null +++ b/.agents/skills/database-flightplan/SKILL.md @@ -0,0 +1,18 @@ +--- +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. +--- + +# Database Flightplan + +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. diff --git a/.agents/skills/database-flightplan/agents/openai.yaml b/.agents/skills/database-flightplan/agents/openai.yaml new file mode 100644 index 000000000..f2f201b90 --- /dev/null +++ b/.agents/skills/database-flightplan/agents/openai.yaml @@ -0,0 +1,4 @@ +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." diff --git a/.agents/skills/live-design-sweep/SKILL.md b/.agents/skills/live-design-sweep/SKILL.md new file mode 100644 index 000000000..40dfe469c --- /dev/null +++ b/.agents/skills/live-design-sweep/SKILL.md @@ -0,0 +1,17 @@ +--- +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. +--- + +# Live Design Sweep + +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. diff --git a/.agents/skills/live-design-sweep/agents/openai.yaml b/.agents/skills/live-design-sweep/agents/openai.yaml new file mode 100644 index 000000000..6202e84bf --- /dev/null +++ b/.agents/skills/live-design-sweep/agents/openai.yaml @@ -0,0 +1,4 @@ +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." diff --git a/.agents/skills/operator-closeout/SKILL.md b/.agents/skills/operator-closeout/SKILL.md new file mode 100644 index 000000000..30f81466c --- /dev/null +++ b/.agents/skills/operator-closeout/SKILL.md @@ -0,0 +1,23 @@ +--- +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. +--- + +# Operator Closeout + +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. diff --git a/.agents/skills/operator-closeout/agents/openai.yaml b/.agents/skills/operator-closeout/agents/openai.yaml new file mode 100644 index 000000000..90a997884 --- /dev/null +++ b/.agents/skills/operator-closeout/agents/openai.yaml @@ -0,0 +1,4 @@ +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." diff --git a/.agents/skills/rag-change-lab/SKILL.md b/.agents/skills/rag-change-lab/SKILL.md new file mode 100644 index 000000000..c25003a1e --- /dev/null +++ b/.agents/skills/rag-change-lab/SKILL.md @@ -0,0 +1,16 @@ +--- +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. +--- + +# RAG Change Lab + +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. diff --git a/.agents/skills/rag-change-lab/agents/openai.yaml b/.agents/skills/rag-change-lab/agents/openai.yaml new file mode 100644 index 000000000..dd40ad7a9 --- /dev/null +++ b/.agents/skills/rag-change-lab/agents/openai.yaml @@ -0,0 +1,4 @@ +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." diff --git a/.agents/skills/session-lifecycle/SKILL.md b/.agents/skills/session-lifecycle/SKILL.md new file mode 100644 index 000000000..2366083dc --- /dev/null +++ b/.agents/skills/session-lifecycle/SKILL.md @@ -0,0 +1,17 @@ +--- +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. +--- + +# Session Lifecycle + +1. Select the phase and generate its plan: + `npm run workflow:lifecycle -- --phase ` +2. At task start, run the mandatory `start-codex-task.ps1` preflight, read applicable `AGENTS.md`, and preserve all existing work. 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. diff --git a/.agents/skills/session-lifecycle/agents/openai.yaml b/.agents/skills/session-lifecycle/agents/openai.yaml new file mode 100644 index 000000000..ff66a9a4f --- /dev/null +++ b/.agents/skills/session-lifecycle/agents/openai.yaml @@ -0,0 +1,4 @@ +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." diff --git a/.agents/skills/verify-triage-fix/SKILL.md b/.agents/skills/verify-triage-fix/SKILL.md new file mode 100644 index 000000000..670f26004 --- /dev/null +++ b/.agents/skills/verify-triage-fix/SKILL.md @@ -0,0 +1,21 @@ +--- +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. +--- + +# Verify Triage Fix + +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. diff --git a/.agents/skills/verify-triage-fix/agents/openai.yaml b/.agents/skills/verify-triage-fix/agents/openai.yaml new file mode 100644 index 000000000..20ea968cd --- /dev/null +++ b/.agents/skills/verify-triage-fix/agents/openai.yaml @@ -0,0 +1,4 @@ +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." diff --git a/.gitignore b/.gitignore index 66f2dcf3b..b17f695ae 100644 --- a/.gitignore +++ b/.gitignore @@ -18,11 +18,14 @@ /playwright-cli/ /.playwright-cli/ /playwright/.auth/ +/.local/ /.codex/ /.agents/* !/.agents/plugins/ !/.agents/plugins/marketplace.json !/.agents/plugins/api_marketplace.json +!/.agents/skills/ +!/.agents/skills/** /skills-lock.json # next.js diff --git a/AGENTS.md b/AGENTS.md index d5d498c51..c834b0539 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -317,6 +317,24 @@ After completing `upload`, summarize the current branch and worktree state, whet + + +## Repository productivity skills + +Use the repo-local skills under `.agents/skills/` when their descriptions match. 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. + +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`. + + + ## Codex GitHub review behavior These instructions apply to Codex GitHub pull request reviews and Codex tasks started from PR comments. diff --git a/docs/productivity-workflows.md b/docs/productivity-workflows.md new file mode 100644 index 000000000..112a66e8e --- /dev/null +++ b/docs/productivity-workflows.md @@ -0,0 +1,27 @@ +# Productivity workflows + +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 | +| ----------------------------------------------- | -------------------------------------------------------------------------------------- | +| `npm run workflow:flightplan` | Classify the current diff and select the smallest appropriate verification ladder. | +| `npm run workflow:triage` | Classify a saved or recent workflow failure before attempting a fix. | +| `npm run workflow:clinical-proof` | Produce clinical-governance, privacy, source, rollback, and verification requirements. | +| `npm run workflow:design-sweep` | Plan the live route, breakpoint, accessibility, and Chromium sweep. | +| `npm run workflow:rag-lab` | Select focused retrieval tests, offline RAG evaluation, and gated live evaluations. | +| `npm run workflow:operator-closeout` | Inventory and deduplicate pending operator or confirmation-required actions. | +| `npm run workflow:lifecycle -- --phase ` | Plan `status`, `start`, `handoff`, `landed`, or `cleanup` lifecycle work. | + +## Safe execution + +- Planning is read-only by default. +- Add `-- --run` to run only the printed local/offline checks. +- Provider-backed commands are never executed by the planner, even with `--run`. +- Add `-- --write-evidence` to save structured evidence under ignored `.local/workflow-evidence/`. +- Add `-- --json` for machine-readable output. +- Use `-- --files pathA,pathB` to plan an explicit proposed change before editing. +- Use `workflow:triage -- --log ` to classify a captured failure. + +The existing shared `workflow:run`, `workflow:status`, `workflow:verify`, `workflow:deps`, `workflow:clean-state`, `workflow:export`, and `workflow:handoff` commands now resolve their shared implementation through the repository's Git common directory. This keeps them portable in linked and detached Codex worktrees. Set `CODEX_LOCAL_WORKFLOW_ROOT` only when the shared tools live somewhere non-standard. + +The matching agent skills live in `.agents/skills/`. Skills provide judgment and repair loops; scripts provide deterministic selection, safety enforcement, and evidence. diff --git a/package.json b/package.json index 759130ccd..df1c29c3d 100644 --- a/package.json +++ b/package.json @@ -145,13 +145,20 @@ "audit:tables": "node scripts/run-tsx.mjs scripts/audit-tables.ts", "samples": "node scripts/run-tsx.mjs scripts/generate-sample-documents.ts", "samples:check": "node scripts/run-tsx.mjs scripts/check-sample-extraction.ts", - "workflow:run": "node ../.local-dev/workflow-run.mjs", - "workflow:status": "node ../.local-dev/workflow-status.mjs", - "workflow:verify": "node ../.local-dev/workflow-verify.mjs", - "workflow:deps": "node ../.local-dev/workflow-deps.mjs", - "workflow:clean-state": "node ../.local-dev/workflow-clean-state.mjs", - "workflow:export": "node ../.local-dev/workflow-export.mjs", - "workflow:handoff": "node ../.local-dev/workflow-handoff.mjs", + "workflow:run": "node scripts/external-workflow.mjs run", + "workflow:status": "node scripts/external-workflow.mjs status", + "workflow:verify": "node scripts/external-workflow.mjs verify", + "workflow:deps": "node scripts/external-workflow.mjs deps", + "workflow:clean-state": "node scripts/external-workflow.mjs clean-state", + "workflow:export": "node scripts/external-workflow.mjs export", + "workflow:handoff": "node scripts/external-workflow.mjs handoff", + "workflow:flightplan": "node scripts/productivity-workflow.mjs flightplan", + "workflow:triage": "node scripts/productivity-workflow.mjs triage", + "workflow:clinical-proof": "node scripts/productivity-workflow.mjs clinical-proof", + "workflow:design-sweep": "node scripts/productivity-workflow.mjs design-sweep", + "workflow:rag-lab": "node scripts/productivity-workflow.mjs rag-lab", + "workflow:operator-closeout": "node scripts/productivity-workflow.mjs operator-closeout", + "workflow:lifecycle": "node scripts/productivity-workflow.mjs lifecycle", "check:drift": "node scripts/run-tsx.mjs scripts/check-drift.ts", "drift:manifest": "node scripts/run-tsx.mjs scripts/generate-drift-manifest.ts" }, diff --git a/scripts/ci-change-scope.mjs b/scripts/ci-change-scope.mjs index b5738a34b..e5eb96505 100644 --- a/scripts/ci-change-scope.mjs +++ b/scripts/ci-change-scope.mjs @@ -68,11 +68,12 @@ const docPatterns = [ const workflowPatterns = [ ".github/workflows", ".github/actions", + ".agents/skills", ".github/pull_request_template.md", "AGENTS.md", "docs/codex-review-protocol.md", "docs/process-hardening.md", - /^scripts\/(?:ci-change-scope|verify-pr-local|eval-rag-offline|check-github-action-pins|check-codex-autofix-workflow)\.mjs$/, + /^scripts\/(?:ci-change-scope|verify-pr-local|eval-rag-offline|check-github-action-pins|check-codex-autofix-workflow|productivity-core|productivity-workflow|external-workflow)\.mjs$/, ]; const codexAutofixPatterns = [ @@ -462,6 +463,12 @@ function selfTest() { docs_only: false, build_changed: false, }); + assertScope("repo-skill", [".agents/skills/database-flightplan/SKILL.md", "scripts/productivity-core.mjs"], { + workflow_changed: true, + source_changed: true, + docs_only: false, + build_changed: false, + }); assertScope( "codex-autofix", [".github/workflows/codex-autofix-review-comments.yml", "AGENTS.md", "scripts/check-codex-autofix-workflow.mjs"], diff --git a/scripts/external-workflow.mjs b/scripts/external-workflow.mjs new file mode 100644 index 000000000..f1b384f07 --- /dev/null +++ b/scripts/external-workflow.mjs @@ -0,0 +1,64 @@ +#!/usr/bin/env node +import { execFileSync, spawnSync } from "node:child_process"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const allowed = new Set(["run", "status", "verify", "deps", "clean-state", "export", "handoff"]); + +export function workflowRootCandidates(cwd = process.cwd(), gitCommonDir = "", env = process.env) { + const candidates = []; + if (env.CODEX_LOCAL_WORKFLOW_ROOT) candidates.push(path.resolve(env.CODEX_LOCAL_WORKFLOW_ROOT)); + candidates.push(path.resolve(cwd, "..", ".local-dev")); + if (gitCommonDir) { + const common = path.isAbsolute(gitCommonDir) ? gitCommonDir : path.resolve(cwd, gitCommonDir); + candidates.push(path.join(path.dirname(path.dirname(common)), ".local-dev")); + } + return [...new Set(candidates.map((candidate) => path.normalize(candidate)))]; +} + +export function resolveExternalWorkflow(name, cwd = process.cwd(), gitCommonDir = "", env = process.env) { + if (!allowed.has(name)) throw new Error(`Unknown external workflow: ${name}`); + const filename = `workflow-${name}.mjs`; + for (const root of workflowRootCandidates(cwd, gitCommonDir, env)) { + const candidate = path.join(root, filename); + if (fs.existsSync(candidate)) return candidate; + } + throw new Error( + `Could not locate ${filename}. Set CODEX_LOCAL_WORKFLOW_ROOT or install the shared .local-dev workflows.`, + ); +} + +function main() { + const [name, ...args] = process.argv.slice(2); + if (!allowed.has(name)) { + console.error(`Usage: node scripts/external-workflow.mjs <${[...allowed].join("|")}> [args]`); + process.exit(1); + } + let gitCommonDir = ""; + try { + gitCommonDir = execFileSync("git", ["rev-parse", "--git-common-dir"], { + cwd: process.cwd(), + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], + }).trim(); + } catch { + // The direct parent candidate and explicit environment override still work outside Git. + } + const target = resolveExternalWorkflow(name, process.cwd(), gitCommonDir); + const result = spawnSync(process.execPath, [target, ...args], { + cwd: process.cwd(), + stdio: "inherit", + windowsHide: true, + }); + process.exit(result.status ?? 1); +} + +if (path.resolve(process.argv[1] || "") === path.resolve(fileURLToPath(import.meta.url))) { + try { + main(); + } catch (error) { + console.error(`[external-workflow] ${error instanceof Error ? error.message : String(error)}`); + process.exit(1); + } +} diff --git a/scripts/productivity-core.mjs b/scripts/productivity-core.mjs new file mode 100644 index 000000000..872bf5602 --- /dev/null +++ b/scripts/productivity-core.mjs @@ -0,0 +1,361 @@ +import { execFileSync, spawnSync } from "node:child_process"; +import fs from "node:fs"; +import path from "node:path"; + +const PROVIDER_COMMAND_PATTERN = + /(?:check:supabase-project|check:production-readiness|verify:release(?:\s|$)|eval:retrieval:quality|eval:rag(?:\s|$)|eval:quality(?:\s|$)|gh\s|railway\s|supabase\s)/i; + +const RISK_PATTERNS = { + ui: [ + /^src\/app\/(?!api\/)/, + "src/components", + "src/styles", + "public", + /^tests\/ui-.*\.spec\.ts$/, + /^playwright.*\.ts$/, + ], + database: ["supabase", "src/lib/supabase", /^src\/app\/api\/(?:documents|images|ingestion|jobs|search|upload)/], + retrieval: [ + /^src\/lib\/(?:rag|retrieval|ranking|clinical-search|smart-rag|citations|evidence|source|chunking)/, + /^src\/app\/api\/(?:answer|search)/, + /^scripts\/(?:eval-|compare-retrieval|retrieval-|profile-retrieval|tune-search)/, + /^tests\/(?:rag|retrieval|answer|citation|evidence|source|clinical-safety)/, + ], + clinical: [ + /^src\/app\/api\/(?:answer|differentials|documents|ingestion|medications|registry|search)/, + /^src\/lib\/(?:rag|retrieval|clinical|answer|citation|evidence|source|owner-scope|query-privacy)/, + /^supabase\//, + /^docs\/(?:clinical-governance|privacy-impact|samd-|source-governance)/, + ], + privacy: [ + /(?:auth|permission|privacy|private|owner-scope|query-privacy|rls|security)/i, + /^src\/app\/api\/(?:documents|images|upload)/, + ], + deployment: [ + "Dockerfile", + "Dockerfile.worker", + "railway.app.json", + "railway.worker.json", + ".github/workflows", + "next.config.ts", + ], + dependency: ["package.json", "package-lock.json", ".npmrc"], + workflow: [ + "AGENTS.md", + ".agents/skills", + ".claude/skills", + "scripts/productivity-core.mjs", + "scripts/productivity-workflow.mjs", + "scripts/external-workflow.mjs", + ], +}; + +function normalizePath(value) { + return String(value || "") + .replaceAll("\\", "/") + .replace(/^\.\//, ""); +} + +function matches(file, pattern) { + if (typeof pattern === "string") return file === pattern || file.startsWith(`${pattern}/`); + return pattern.test(file); +} + +function uniqueBy(items, key) { + const seen = new Set(); + return items.filter((item) => { + const value = item[key]; + if (seen.has(value)) return false; + seen.add(value); + return true; + }); +} + +function check(command, reason) { + return { command, reason }; +} + +export function normalizeFiles(files = []) { + return [...new Set(files.map(normalizePath).filter(Boolean))].sort(); +} + +export function classifyRisks(files = []) { + const normalized = normalizeFiles(files); + const risks = Object.fromEntries( + Object.entries(RISK_PATTERNS).map(([name, patterns]) => [ + name, + normalized.some((file) => patterns.some((pattern) => matches(file, pattern))), + ]), + ); + risks.docsOnly = normalized.length > 0 && normalized.every((file) => /(?:^docs\/|\.mdx?$)/i.test(file)); + return risks; +} + +function baseLocalChecks(risks) { + if (risks.docsOnly) { + return [ + check("npm run format:check", "Confirm repository formatting."), + check("npm run docs:check-links", "Validate documentation links."), + check("npm run docs:check-scripts", "Validate documented package commands."), + ]; + } + + const checks = [check("npm run verify:cheap", "Run the fast offline repository gate.")]; + if (risks.retrieval || risks.clinical) { + checks.unshift( + check("npm run eval:rag:offline", "Protect retrieval and answer contracts without provider access."), + ); + } + if (risks.ui) { + checks.push(check("npm run ensure", "Start or verify the identity-checked local app before browser work.")); + checks.push(check("npm run test:e2e:critical", "Exercise the smallest high-value Chromium path.")); + checks.push(check("npm run verify:ui", "Run the full local Chromium UI gate after focused fixes.")); + } + checks.push(check("npm run verify:pr-local", "Mirror the risk-scoped PR gate before handoff.")); + return checks; +} + +function approvalChecks(risks) { + const approvals = []; + if (risks.database) { + approvals.push( + check("npm run check:supabase-project", "Confirm the live Supabase target after explicit approval."), + ); + } + if (risks.retrieval) { + approvals.push( + check( + "npm run eval:retrieval:quality", + "Run the live 36-case golden retrieval evaluation after explicit approval.", + ), + ); + } + if (risks.clinical) { + approvals.push( + check( + "npm run check:production-readiness", + "Run the production/provider readiness gate after explicit approval.", + ), + ); + } + if (risks.retrieval && risks.clinical) { + approvals.push(check("npm run eval:rag -- --limit 15", "Sample live answer generation after explicit approval.")); + approvals.push( + check("npm run eval:quality -- --rag-only", "Check live grounded-answer invariants after explicit approval."), + ); + } + if (risks.deployment) { + approvals.push( + check("npm run verify:release", "Run the complete release gate only after confirming its provider effects."), + ); + } + return approvals; +} + +function workflowSummary(workflow) { + return { + flightplan: "Risk-scoped change and verification plan", + triage: "Focused failure diagnosis and repair loop", + "clinical-proof": "Clinical governance and evidence plan", + "design-sweep": "Live route, responsive, and accessibility sweep", + "rag-lab": "Offline-first retrieval and answer validation lab", + "operator-closeout": "Approval-gated operator backlog batch", + lifecycle: "Safe task start, handoff, merge-proof, and cleanup lifecycle", + }[workflow]; +} + +export function buildWorkflowPlan(workflow, files = [], options = {}) { + const normalized = normalizeFiles(files); + const risks = classifyRisks(normalized); + let localChecks = baseLocalChecks(risks); + let approvalRequired = approvalChecks(risks); + const proof = ["Record exact commands and exit codes.", "Preserve unrelated staged, unstaged, and untracked work."]; + + if (workflow === "triage") { + localChecks = []; + proof.push( + "Reproduce the smallest failing target before changing code.", + "Rerun that target after each fix before widening verification.", + ); + } else if (workflow === "clinical-proof") { + localChecks = uniqueBy( + [check("npm run eval:rag:offline", "Prove the code-backed offline clinical contract."), ...localChecks], + "command", + ); + proof.push( + "Complete the Clinical Governance Preflight in .github/pull_request_template.md.", + "Document source, privacy, owner-scope, rollback, and SaMD implications.", + ); + } else if (workflow === "design-sweep") { + localChecks = [ + check("npm run ensure", "Start the verified app and use the printed URL."), + check("npm run test:e2e:critical", "Prove primary flows before the broad sweep."), + check("npm run test:e2e:accessibility", "Run the focused accessibility gate."), + check("npm run verify:ui", "Run the full local Chromium matrix."), + check("npm run verify:cheap", "Verify source changes after UI fixes."), + ]; + approvalRequired = []; + proof.push( + "Cover 320, 390, 639, 768, 1440, and 1920 px.", + "Check overflow, scroll ownership, keyboard use, focus, reduced motion, and forced colors.", + ); + } else if (workflow === "rag-lab") { + localChecks = [ + check( + "npm run test -- tests/retrieval-selection.test.ts tests/rag-routing.test.ts tests/rag-offline-contract.test.ts", + "Run focused retrieval and answer contract tests.", + ), + check("npm run eval:rag:offline", "Run the provider-free golden RAG preflight."), + check("npm run verify:cheap", "Catch cross-cutting static and unit regressions."), + ]; + approvalRequired = uniqueBy( + [ + check("npm run eval:retrieval:quality", "Require 36/36 against the live corpus after explicit approval."), + check("npm run eval:rag -- --limit 15", "Sample live answer generation after explicit approval."), + check("npm run eval:quality -- --rag-only", "Compare grounded-answer invariants after explicit approval."), + ], + "command", + ); + proof.push( + "Compare recall, MRR, grounded support, citation failure, and numeric-grounding failure with the accepted baseline.", + ); + } else if (workflow === "operator-closeout") { + localChecks = []; + approvalRequired = []; + proof.push( + "Deduplicate stale or already-completed actions before requesting approval.", + "Capture pre-state, command, post-state, and rollback evidence for every approved action.", + ); + } else if (workflow === "lifecycle") { + const phase = options.phase || "status"; + if (!new Set(["status", "start", "handoff", "landed", "cleanup"]).has(phase)) { + throw new Error(`Unknown lifecycle phase: ${phase}`); + } + localChecks = phase === "handoff" ? [check("npm run verify:pr-local", "Complete the local handoff gate.")] : []; + approvalRequired = + phase === "handoff" + ? [ + check("git push -u origin ", "Publishing requires explicit user authorization."), + check("gh pr create --base main", "GitHub interaction requires explicit user authorization."), + ] + : phase === "landed" + ? [ + check( + "gh pr view --json state,mergeCommit,mergedAt", + "GitHub interaction requires explicit user authorization.", + ), + ] + : []; + proof.push( + "Verify branch and worktree state at every transition.", + "Use content equality for squash-merge proof before cleanup.", + ); + } + + if (localChecks.some((item) => PROVIDER_COMMAND_PATTERN.test(item.command))) { + throw new Error(`Unsafe workflow definition: ${workflow} placed a provider-backed command in localChecks.`); + } + + return { + workflow, + summary: workflowSummary(workflow), + files: normalized, + risks, + localChecks: uniqueBy(localChecks, "command"), + approvalRequired: uniqueBy(approvalRequired, "command"), + proof, + }; +} + +export function analyzeFailureText(text = "", knownFlakes = []) { + const value = String(text); + const lower = value.toLowerCase(); + 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( + lower, + ) + ) { + return { + category: "provider-or-configuration", + confidence: "high", + reason: "Credentials, authorization, quota, or live-provider configuration.", + }; + } + if (/timed? out|timeout|etimedout|browser has been closed|worker.*exited/.test(lower)) { + return { + category: "environment-or-timeout", + confidence: "medium", + reason: "Timeout or runtime process failure; inspect active processes and artifacts.", + }; + } + if (/assertionerror|expected .* received|tests? failed|type error|eslint.*error|syntaxerror/.test(lower)) { + return { + category: "probable-regression", + confidence: "medium", + reason: "A deterministic code or test assertion failed.", + }; + } + return { + category: "unclassified", + confidence: "low", + reason: "Reproduce the smallest target and capture its complete error output.", + }; +} + +export function extractOperatorItemsFromText(text, source = "unknown") { + return String(text) + .split(/\r?\n/) + .map((line, index) => ({ source, line: index + 1, text: line.trim() })) + .filter(({ text: line }) => + /(?:⏳\s*pending|operator[- ]only|operator follow-up|confirmation-required|remaining operator items)/i.test(line), + ) + .filter(({ text: line }) => !/^#/.test(line)); +} + +export function scanOperatorBacklog(repoRoot = process.cwd()) { + const sources = ["docs/operator-backlog.md", "docs/process-hardening.md"]; + const items = sources.flatMap((source) => { + const absolute = path.join(repoRoot, source); + return fs.existsSync(absolute) ? extractOperatorItemsFromText(fs.readFileSync(absolute, "utf8"), source) : []; + }); + return uniqueBy(items, "text"); +} + +export function readChangeScope(files, repoRoot = process.cwd()) { + const args = ["scripts/ci-change-scope.mjs", "--json"]; + if (files?.length) args.push("--files", normalizeFiles(files).join(",")); + const output = execFileSync(process.execPath, args, { + cwd: repoRoot, + encoding: "utf8", + stdio: ["ignore", "pipe", "inherit"], + }); + return JSON.parse(output); +} + +export function runLocalChecks(checks, repoRoot = process.cwd()) { + for (const item of checks) { + console.log(`\n[workflow] ${item.command}`); + const result = + process.platform === "win32" + ? spawnSync("cmd.exe", ["/d", "/s", "/c", item.command], { cwd: repoRoot, stdio: "inherit", windowsHide: true }) + : spawnSync("sh", ["-lc", item.command], { cwd: repoRoot, stdio: "inherit" }); + if (result.status !== 0) return { status: result.status ?? 1, failed: item.command }; + } + return { status: 0 }; +} + +export function writeWorkflowEvidence(plan, repoRoot = process.cwd()) { + const directory = path.join(repoRoot, ".local", "workflow-evidence"); + fs.mkdirSync(directory, { recursive: true }); + const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); + const target = path.join(directory, `${timestamp}-${plan.workflow}.json`); + fs.writeFileSync(target, `${JSON.stringify({ ...plan, createdAt: new Date().toISOString() }, null, 2)}\n`); + return target; +} + +export const providerCommandPattern = PROVIDER_COMMAND_PATTERN; diff --git a/scripts/productivity-workflow.mjs b/scripts/productivity-workflow.mjs new file mode 100644 index 000000000..4330e4d02 --- /dev/null +++ b/scripts/productivity-workflow.mjs @@ -0,0 +1,150 @@ +#!/usr/bin/env node +import fs from "node:fs"; +import path from "node:path"; +import { + analyzeFailureText, + buildWorkflowPlan, + readChangeScope, + runLocalChecks, + scanOperatorBacklog, + writeWorkflowEvidence, +} from "./productivity-core.mjs"; + +const workflows = new Set([ + "flightplan", + "triage", + "clinical-proof", + "design-sweep", + "rag-lab", + "operator-closeout", + "lifecycle", +]); + +function usage() { + console.log(`Usage: node scripts/productivity-workflow.mjs [options] + +Workflows: flightplan, triage, clinical-proof, design-sweep, rag-lab, operator-closeout, lifecycle +Options: + --files Plan for explicit paths instead of the current change + --phase Lifecycle phase: status, start, handoff, landed, cleanup + --log Triage a saved failure log + --run Execute local/offline checks only + --json Emit structured JSON + --write-evidence Save the plan under .local/workflow-evidence + --help Show this help + +Provider-backed commands are never executed by this tool.`); +} + +function parseArgs(argv) { + const [workflow, ...tokens] = argv; + const options = { + workflow, + files: undefined, + phase: "status", + log: undefined, + run: false, + json: false, + writeEvidence: false, + }; + for (let index = 0; index < tokens.length; index += 1) { + const token = tokens[index]; + if (token === "--help" || token === "-h") options.help = true; + else if (token === "--run") options.run = true; + else if (token === "--json") options.json = true; + else if (token === "--write-evidence") options.writeEvidence = true; + else if (token === "--files") + options.files = (tokens[++index] || "") + .split(",") + .map((item) => item.trim()) + .filter(Boolean); + else if (token === "--phase") options.phase = tokens[++index] || ""; + else if (token === "--log") options.log = tokens[++index] || ""; + else throw new Error(`Unknown option: ${token}`); + } + return options; +} + +function render(plan) { + console.log(`\n${plan.summary}`); + console.log(`Workflow: ${plan.workflow}`); + console.log(`Changed files: ${plan.files.length ? plan.files.join(", ") : "none detected"}`); + const activeRisks = Object.entries(plan.risks) + .filter(([, active]) => active) + .map(([name]) => name); + console.log(`Risk classes: ${activeRisks.length ? activeRisks.join(", ") : "none"}`); + if (plan.diagnosis) + console.log(`Diagnosis: ${plan.diagnosis.category} (${plan.diagnosis.confidence}) - ${plan.diagnosis.reason}`); + if (plan.operatorItems) { + console.log(`\nPending/operator candidates: ${plan.operatorItems.length}`); + for (const item of plan.operatorItems) console.log(`- ${item.source}:${item.line} ${item.text}`); + } + console.log("\nLocal/offline checks:"); + if (!plan.localChecks.length) console.log("- none selected"); + for (const item of plan.localChecks) console.log(`- ${item.command} — ${item.reason}`); + console.log("\nApproval-required commands:"); + if (!plan.approvalRequired.length) console.log("- none"); + for (const item of plan.approvalRequired) console.log(`- ${item.command} — ${item.reason}`); + console.log("\nRequired proof:"); + for (const item of plan.proof) console.log(`- ${item}`); +} + +function loadKnownFlakes(repoRoot) { + try { + const data = JSON.parse(fs.readFileSync(path.join(repoRoot, "tests", "flake-ledger.json"), "utf8")); + const records = Array.isArray(data) ? data : data.flakes || []; + return records.flatMap((entry) => + [entry.title, ...(entry.patterns || [])].filter(Boolean).map((pattern) => ({ id: entry.id, pattern })), + ); + } catch { + return []; + } +} + +function loadFailureText(options, repoRoot) { + if (options.log) return fs.readFileSync(path.resolve(repoRoot, options.log), "utf8"); + const marker = path.join(repoRoot, ".local", "workflow-last-failure.json"); + return fs.existsSync(marker) ? fs.readFileSync(marker, "utf8") : ""; +} + +function main() { + const options = parseArgs(process.argv.slice(2)); + if (options.help || !workflows.has(options.workflow)) { + usage(); + process.exit(options.help ? 0 : 1); + } + if (options.run && options.workflow === "operator-closeout") { + throw new Error( + "operator-closeout is plan-only; execute approved provider actions individually after confirmation.", + ); + } + + const repoRoot = process.cwd(); + const scope = readChangeScope(options.files, repoRoot); + const plan = buildWorkflowPlan(options.workflow, scope.files, { phase: options.phase }); + plan.scope = scope; + if (options.workflow === "triage") { + plan.diagnosis = analyzeFailureText(loadFailureText(options, repoRoot), loadKnownFlakes(repoRoot)); + } + if (options.workflow === "operator-closeout") plan.operatorItems = scanOperatorBacklog(repoRoot); + + if (options.json) console.log(JSON.stringify(plan, null, 2)); + else render(plan); + + if (options.writeEvidence) console.log(`\nEvidence: ${writeWorkflowEvidence(plan, repoRoot)}`); + if (options.run) { + const result = runLocalChecks(plan.localChecks, repoRoot); + if (result.status !== 0) { + console.error(`\n[workflow] stopped at: ${result.failed}`); + process.exit(result.status); + } + } +} + +try { + main(); + process.exit(0); +} catch (error) { + console.error(`[productivity-workflow] ${error instanceof Error ? error.message : String(error)}`); + process.exit(1); +} diff --git a/tests/productivity-workflow.test.ts b/tests/productivity-workflow.test.ts new file mode 100644 index 000000000..d1263e07a --- /dev/null +++ b/tests/productivity-workflow.test.ts @@ -0,0 +1,88 @@ +import { describe, expect, it } from "vitest"; + +import { + analyzeFailureText, + buildWorkflowPlan, + classifyRisks, + extractOperatorItemsFromText, + providerCommandPattern, +} from "../scripts/productivity-core.mjs"; +import { resolveExternalWorkflow, workflowRootCandidates } from "../scripts/external-workflow.mjs"; + +describe("productivity workflow planning", () => { + it("classifies UI, retrieval, clinical, and privacy risk from changed paths", () => { + const risks = classifyRisks([ + "src/components/ClinicalDashboard.tsx", + "src/lib/retrieval-selection.ts", + "src/lib/owner-scope.ts", + ]); + + expect(risks.ui).toBe(true); + expect(risks.retrieval).toBe(true); + expect(risks.clinical).toBe(true); + expect(risks.privacy).toBe(true); + }); + + it("never places provider commands in the executable local plan", () => { + const plan = buildWorkflowPlan("flightplan", [ + "src/app/api/answer/route.ts", + "supabase/migrations/20260717000000_example.sql", + ]); + + expect(plan.localChecks.some((item: { command: string }) => providerCommandPattern.test(item.command))).toBe(false); + expect(plan.approvalRequired.map((item: { command: string }) => item.command)).toContain( + "npm run check:supabase-project", + ); + expect(plan.approvalRequired.map((item: { command: string }) => item.command)).toContain( + "npm run eval:retrieval:quality", + ); + }); + + it("keeps the design sweep local and covers the established breakpoint proof", () => { + const plan = buildWorkflowPlan("design-sweep", ["src/components/ClinicalDashboard.tsx"]); + + expect(plan.approvalRequired).toEqual([]); + expect(plan.localChecks.map((item: { command: string }) => item.command)).toContain("npm run verify:ui"); + expect(plan.proof.join(" ")).toContain("320, 390, 639, 768, 1440, and 1920"); + }); + + it("rejects unknown lifecycle phases", () => { + expect(() => buildWorkflowPlan("lifecycle", [], { phase: "publish-everything" })).toThrow( + "Unknown lifecycle phase", + ); + }); + + it("classifies common failure signatures", () => { + expect(analyzeFailureText("Error: Cannot find module 'workflow-status.mjs'").category).toBe("environment"); + expect(analyzeFailureText("OPENAI_API_KEY missing").category).toBe("provider-or-configuration"); + expect(analyzeFailureText("AssertionError: expected 2 received 3").category).toBe("probable-regression"); + }); + + it("extracts only actionable operator markers", () => { + const items = extractOperatorItemsFromText( + "# Heading\n| Live gate | ⏳ pending | run later |\n- Operator-only: rotate key\n- completed", + "docs/example.md", + ); + + expect(items).toHaveLength(2); + expect(items[0]).toMatchObject({ source: "docs/example.md", line: 2 }); + }); +}); + +describe("external workflow portability", () => { + it("derives the shared workflow directory from the Git common directory", () => { + const candidates = workflowRootCandidates( + "C:\\Users\\example\\.codex\\worktrees\\1234\\Database", + "C:\\Dev\\Apps\\Database\\.git", + { NODE_ENV: "test" }, + ); + + expect(candidates.some((candidate) => candidate.replaceAll("\\", "/").endsWith("/Dev/Apps/.local-dev"))).toBe(true); + }); + + it("rejects unknown shared workflow names", () => { + expect(() => resolveExternalWorkflow("unknown", process.cwd(), "", { NODE_ENV: "test" })).toThrow( + "Unknown external workflow", + ); + }); +}); From a0b14518b28820aadbc808a3891a3d70b1ffb0fb Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Fri, 17 Jul 2026 19:02:12 +0800 Subject: [PATCH 2/7] test(workflow): make resolver assertion cross-platform --- tests/productivity-workflow.test.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/productivity-workflow.test.ts b/tests/productivity-workflow.test.ts index d1263e07a..36e355f9d 100644 --- a/tests/productivity-workflow.test.ts +++ b/tests/productivity-workflow.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from "vitest"; +import path from "node:path"; import { analyzeFailureText, @@ -71,13 +72,13 @@ describe("productivity workflow planning", () => { describe("external workflow portability", () => { it("derives the shared workflow directory from the Git common directory", () => { - const candidates = workflowRootCandidates( - "C:\\Users\\example\\.codex\\worktrees\\1234\\Database", - "C:\\Dev\\Apps\\Database\\.git", - { NODE_ENV: "test" }, - ); + const root = path.parse(process.cwd()).root; + const cwd = path.join(root, "tmp", "codex", "worktrees", "1234", "Database"); + const gitCommonDir = path.join(root, "workspace", "Apps", "Database", ".git"); + const candidates = workflowRootCandidates(cwd, gitCommonDir, { NODE_ENV: "test" }); + const expected = path.join(root, "workspace", "Apps", ".local-dev"); - expect(candidates.some((candidate) => candidate.replaceAll("\\", "/").endsWith("/Dev/Apps/.local-dev"))).toBe(true); + expect(candidates).toContain(path.normalize(expected)); }); it("rejects unknown shared workflow names", () => { From 00fbc3803c6796d466095b3d6a8f7c9be4fdef2d Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Fri, 17 Jul 2026 19:03:57 +0800 Subject: [PATCH 3/7] docs(workflow): clarify task-start preflight source --- .agents/skills/session-lifecycle/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.agents/skills/session-lifecycle/SKILL.md b/.agents/skills/session-lifecycle/SKILL.md index 2366083dc..8d89067ab 100644 --- a/.agents/skills/session-lifecycle/SKILL.md +++ b/.agents/skills/session-lifecycle/SKILL.md @@ -7,7 +7,7 @@ description: Manage Database repository work safely from task start through veri 1. Select the phase and generate its plan: `npm run workflow:lifecycle -- --phase ` -2. At task start, run the mandatory `start-codex-task.ps1` preflight, read applicable `AGENTS.md`, and preserve all existing work. Do not use the shared stash across worktrees. +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. From b52112df6aa36311d7420189064acd79dcf2c3f5 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Fri, 17 Jul 2026 19:11:22 +0800 Subject: [PATCH 4/7] fix(workflow): classify answer routes as database-scoped --- scripts/productivity-core.mjs | 6 +++++- tests/productivity-workflow.test.ts | 10 ++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/scripts/productivity-core.mjs b/scripts/productivity-core.mjs index 872bf5602..a6845e95b 100644 --- a/scripts/productivity-core.mjs +++ b/scripts/productivity-core.mjs @@ -14,7 +14,11 @@ const RISK_PATTERNS = { /^tests\/ui-.*\.spec\.ts$/, /^playwright.*\.ts$/, ], - database: ["supabase", "src/lib/supabase", /^src\/app\/api\/(?:documents|images|ingestion|jobs|search|upload)/], + database: [ + "supabase", + "src/lib/supabase", + /^src\/app\/api\/(?:answer|documents|images|ingestion|jobs|search|upload)/, + ], retrieval: [ /^src\/lib\/(?:rag|retrieval|ranking|clinical-search|smart-rag|citations|evidence|source|chunking)/, /^src\/app\/api\/(?:answer|search)/, diff --git a/tests/productivity-workflow.test.ts b/tests/productivity-workflow.test.ts index 36e355f9d..0cb20a744 100644 --- a/tests/productivity-workflow.test.ts +++ b/tests/productivity-workflow.test.ts @@ -39,6 +39,16 @@ describe("productivity workflow planning", () => { ); }); + it("treats answer routes as database-scoped without needing a migration path", () => { + const files = ["src/app/api/answer/route.ts"]; + const plan = buildWorkflowPlan("flightplan", files); + + expect(classifyRisks(files).database).toBe(true); + expect(plan.approvalRequired.map((item: { command: string }) => item.command)).toContain( + "npm run check:supabase-project", + ); + }); + it("keeps the design sweep local and covers the established breakpoint proof", () => { const plan = buildWorkflowPlan("design-sweep", ["src/components/ClinicalDashboard.tsx"]); From d52a2273d427e6ba9bbd964b7b0ba923dcefeb37 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Fri, 17 Jul 2026 19:20:47 +0800 Subject: [PATCH 5/7] fix(workflow): close review safety gaps --- .agents/skills/clinical-change-proof/SKILL.md | 2 +- .gitignore | 1 + docs/branch-review-ledger.md | 1 + scripts/ci-change-scope.mjs | 4 +- scripts/productivity-core.mjs | 7 ++- scripts/productivity-workflow.mjs | 25 +++++--- tests/productivity-workflow.test.ts | 60 +++++++++++++++++++ 7 files changed, 87 insertions(+), 13 deletions(-) diff --git a/.agents/skills/clinical-change-proof/SKILL.md b/.agents/skills/clinical-change-proof/SKILL.md index 613f73335..d5634bd7b 100644 --- a/.agents/skills/clinical-change-proof/SKILL.md +++ b/.agents/skills/clinical-change-proof/SKILL.md @@ -16,5 +16,5 @@ description: Build evidence for Database changes affecting clinical output, inge - 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. Present every live Supabase, OpenAI, hosted CI, or production-readiness command as a separate approval request. Never bundle approval implicitly into a local gate. +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. diff --git a/.gitignore b/.gitignore index b17f695ae..76c42fe03 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ /.codex/ /.agents/* !/.agents/plugins/ +/.agents/plugins/** !/.agents/plugins/marketplace.json !/.agents/plugins/api_marketplace.json !/.agents/skills/ diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index c63f034d3..0104a19fe 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -569,3 +569,4 @@ Use this ledger to prevent repeated branch and PR reviews when the reviewed HEAD | 2026-07-17 | codex/historical-branch-cleanup-20260717 | e36ac0c6628264c7ed6c494a597a62d0214b68f6 | branch-cleanup and historical-content recovery | Completed the pending historical cleanup: deleted 55 exact-SHA remote refs and 20 redundant local refs, removed nine clean merged worktrees, preserved every dirty, active, open-PR, or patch-unique worktree, and recovered the still-useful governance incident runbook from `codex/domain-1-governance-remediation`. Historical code changes were either tied to merged PRs or reviewed as superseded by current implementations; open PRs #699, #700, #702, and #704 remain protected. | Fresh `git fetch --prune`; GitHub PR inventory and exact commit-to-merged-PR associations; exact remote SHA rechecks before deletion; cherry-pick-aware logs; two-dot tree and branch-only-file review; Codex task-to-worktree cross-check; focused documentation validation recorded in the cleanup PR. No OpenAI, Supabase, production-data, or live clinical workflow was run. | | 2026-07-17 | PR #704 / codex/scroll-geometry-stability-20260717 | 35e74ddbd61bacc5b34f06efbd58091f092665fd | nested scroll-source review follow-up | Confirmed the outside-diff CodeRabbit finding: the standalone shell shared one intent history across main and descendant scroll containers, so a switch from a deep main offset to a near-zero nested offset could falsely reveal chrome. Scroll metrics now identify their source, source changes rebase direction and travel while preserving visibility, and unit/UI regressions cover the switch. No unresolved actionable review finding remains. | Focused Vitest 9/9; TypeScript; scoped ESLint; Prettier; `git diff --check`. Exact-head hosted CI and UI remain required after push. No Supabase/OpenAI/live-provider checks run. | | 2026-07-17 | codex/scrolling-cleanup-20260717 | ff77cd06c + latest origin/main sync | cross-page scrolling and interaction stability review | Fixed two confirmed P2 defects: desktop action-popup placement performed synchronous geometry work for every captured scroll event, and submitted differential searches with zero document matches fell back to the home state. Placement is now coalesced per animation frame with passive scroll listeners, and submitted empty-evidence results remain visible. Hardened three popup/navigation browser helpers that reproduced hydration timing failures. No other high-confidence defect remains in the scoped diff. | Scroll-focused Chromium 28/28; final affected Chromium 5/5; source regressions 2/2; scoped ESLint; Prettier; TypeScript and production build passed before the final upstream-only sync; `git diff --check`. The aggregate local Vitest/UI runs were affected by concurrent-worktree resource contention, so exact-head hosted CI remains required before merge. No Supabase/OpenAI/live-provider checks run. | +| 2026-07-17 | PR #713 / codex/chat-workflow-ideas-0916 | b52112df6aa36311d7420189064acd79dcf2c3f5 + reviewed follow-up diff | workflow toolkit review follow-up | Fixed all 12 actionable Codex and CodeRabbit threads: cross-platform path fixtures, installation-managed preflight guidance, answer-route database scoping, per-command approval boundaries, plugin-ignore narrowing, isolated CI-scope proof, remote-Git command guarding, repository-skill verification classification, `TypeError` diagnosis, strict CLI option values, and machine-parseable JSON evidence output. No unresolved actionable finding remains in the reviewed scope. | `npm run verify:cheap` passed with 273 files and 2,599 tests; focused toolkit Vitest 13/13; CI-scope self-test; plugin-ignore proof; `git diff --check`. Exact-head hosted CI remains required after the follow-up push. No Supabase, OpenAI, or other live product-provider command was run. | diff --git a/scripts/ci-change-scope.mjs b/scripts/ci-change-scope.mjs index e5eb96505..10cee0418 100644 --- a/scripts/ci-change-scope.mjs +++ b/scripts/ci-change-scope.mjs @@ -463,9 +463,9 @@ function selfTest() { docs_only: false, build_changed: false, }); - assertScope("repo-skill", [".agents/skills/database-flightplan/SKILL.md", "scripts/productivity-core.mjs"], { + assertScope("repo-skill", [".agents/skills/database-flightplan/SKILL.md"], { workflow_changed: true, - source_changed: true, + source_changed: false, docs_only: false, build_changed: false, }); diff --git a/scripts/productivity-core.mjs b/scripts/productivity-core.mjs index a6845e95b..4fb255aa7 100644 --- a/scripts/productivity-core.mjs +++ b/scripts/productivity-core.mjs @@ -3,7 +3,7 @@ import fs from "node:fs"; import path from "node:path"; const PROVIDER_COMMAND_PATTERN = - /(?:check:supabase-project|check:production-readiness|verify:release(?:\s|$)|eval:retrieval:quality|eval:rag(?:\s|$)|eval:quality(?:\s|$)|gh\s|railway\s|supabase\s)/i; + /(?:check:supabase-project|check:production-readiness|verify:release(?:\s|$)|eval:retrieval:quality|eval:rag(?:\s|$)|eval:quality(?:\s|$)|git\s+(?:fetch|ls-remote|pull|push)(?:\s|$)|gh\s|glab\s|railway\s|supabase\s)/i; const RISK_PATTERNS = { ui: [ @@ -91,7 +91,8 @@ export function classifyRisks(files = []) { normalized.some((file) => patterns.some((pattern) => matches(file, pattern))), ]), ); - risks.docsOnly = normalized.length > 0 && normalized.every((file) => /(?:^docs\/|\.mdx?$)/i.test(file)); + risks.docsOnly = + normalized.length > 0 && normalized.every((file) => /(?:^docs\/|\.mdx?$)/i.test(file)) && !risks.workflow; return risks; } @@ -297,7 +298,7 @@ export function analyzeFailureText(text = "", knownFlakes = []) { reason: "Timeout or runtime process failure; inspect active processes and artifacts.", }; } - if (/assertionerror|expected .* received|tests? failed|type error|eslint.*error|syntaxerror/.test(lower)) { + if (/assertionerror|expected .* received|tests? failed|type ?error|eslint.*error|syntaxerror/.test(lower)) { return { category: "probable-regression", confidence: "medium", diff --git a/scripts/productivity-workflow.mjs b/scripts/productivity-workflow.mjs index 4330e4d02..9ef4ce60c 100644 --- a/scripts/productivity-workflow.mjs +++ b/scripts/productivity-workflow.mjs @@ -49,17 +49,23 @@ function parseArgs(argv) { }; for (let index = 0; index < tokens.length; index += 1) { const token = tokens[index]; + const nextValue = () => { + const value = tokens[index + 1]; + if (!value || value.startsWith("-")) throw new Error(`Missing value for ${token}`); + index += 1; + return value; + }; if (token === "--help" || token === "-h") options.help = true; else if (token === "--run") options.run = true; else if (token === "--json") options.json = true; else if (token === "--write-evidence") options.writeEvidence = true; else if (token === "--files") - options.files = (tokens[++index] || "") + options.files = nextValue() .split(",") .map((item) => item.trim()) .filter(Boolean); - else if (token === "--phase") options.phase = tokens[++index] || ""; - else if (token === "--log") options.log = tokens[++index] || ""; + else if (token === "--phase") options.phase = nextValue(); + else if (token === "--log") options.log = nextValue(); else throw new Error(`Unknown option: ${token}`); } return options; @@ -118,6 +124,9 @@ function main() { "operator-closeout is plan-only; execute approved provider actions individually after confirmation.", ); } + if (options.json && options.run) { + throw new Error("--json cannot be combined with --run because executed checks write non-JSON output."); + } const repoRoot = process.cwd(); const scope = readChangeScope(options.files, repoRoot); @@ -128,10 +137,12 @@ function main() { } if (options.workflow === "operator-closeout") plan.operatorItems = scanOperatorBacklog(repoRoot); - if (options.json) console.log(JSON.stringify(plan, null, 2)); - else render(plan); - - if (options.writeEvidence) console.log(`\nEvidence: ${writeWorkflowEvidence(plan, repoRoot)}`); + const evidencePath = options.writeEvidence ? writeWorkflowEvidence(plan, repoRoot) : undefined; + if (options.json) console.log(JSON.stringify(evidencePath ? { ...plan, evidencePath } : plan, null, 2)); + else { + render(plan); + if (evidencePath) console.log(`\nEvidence: ${evidencePath}`); + } if (options.run) { const result = runLocalChecks(plan.localChecks, repoRoot); if (result.status !== 0) { diff --git a/tests/productivity-workflow.test.ts b/tests/productivity-workflow.test.ts index 0cb20a744..6e681ce8c 100644 --- a/tests/productivity-workflow.test.ts +++ b/tests/productivity-workflow.test.ts @@ -1,4 +1,6 @@ import { describe, expect, it } from "vitest"; +import { spawnSync } from "node:child_process"; +import fs from "node:fs"; import path from "node:path"; import { @@ -10,6 +12,15 @@ import { } from "../scripts/productivity-core.mjs"; import { resolveExternalWorkflow, workflowRootCandidates } from "../scripts/external-workflow.mjs"; +const workflowScript = path.resolve(process.cwd(), "scripts", "productivity-workflow.mjs"); + +function runWorkflow(args: string[]) { + return spawnSync(process.execPath, [workflowScript, ...args], { + cwd: process.cwd(), + encoding: "utf8", + }); +} + describe("productivity workflow planning", () => { it("classifies UI, retrieval, clinical, and privacy risk from changed paths", () => { const risks = classifyRisks([ @@ -37,6 +48,8 @@ describe("productivity workflow planning", () => { expect(plan.approvalRequired.map((item: { command: string }) => item.command)).toContain( "npm run eval:retrieval:quality", ); + expect(providerCommandPattern.test("git push origin feature")).toBe(true); + expect(providerCommandPattern.test("glab mr merge 123")).toBe(true); }); it("treats answer routes as database-scoped without needing a migration path", () => { @@ -57,6 +70,13 @@ describe("productivity workflow planning", () => { expect(plan.proof.join(" ")).toContain("320, 390, 639, 768, 1440, and 1920"); }); + it("treats repository skill instructions as workflow changes rather than docs-only", () => { + const plan = buildWorkflowPlan("flightplan", [".agents/skills/database-flightplan/SKILL.md"]); + + expect(plan.risks).toMatchObject({ workflow: true, docsOnly: false }); + expect(plan.localChecks.map((item: { command: string }) => item.command)).toContain("npm run verify:pr-local"); + }); + it("rejects unknown lifecycle phases", () => { expect(() => buildWorkflowPlan("lifecycle", [], { phase: "publish-everything" })).toThrow( "Unknown lifecycle phase", @@ -67,6 +87,7 @@ describe("productivity workflow planning", () => { expect(analyzeFailureText("Error: Cannot find module 'workflow-status.mjs'").category).toBe("environment"); expect(analyzeFailureText("OPENAI_API_KEY missing").category).toBe("provider-or-configuration"); expect(analyzeFailureText("AssertionError: expected 2 received 3").category).toBe("probable-regression"); + expect(analyzeFailureText("TypeError: value is not iterable").category).toBe("probable-regression"); }); it("extracts only actionable operator markers", () => { @@ -80,6 +101,45 @@ describe("productivity workflow planning", () => { }); }); +describe("productivity workflow CLI", () => { + it("rejects option flags where a required value is missing", () => { + const result = runWorkflow(["flightplan", "--files", "--json"]); + + expect(result.status).toBe(1); + expect(result.stdout).toBe(""); + expect(result.stderr).toContain("Missing value for --files"); + }); + + it("rejects JSON execution before any local checks can write mixed output", () => { + const result = runWorkflow(["flightplan", "--files", "docs/example.md", "--json", "--run"]); + + expect(result.status).toBe(1); + expect(result.stdout).toBe(""); + expect(result.stderr).toContain("--json cannot be combined with --run"); + }); + + it("keeps JSON evidence output parseable and cleans its test artifact", () => { + const evidenceDirectory = path.join(process.cwd(), ".local", "workflow-evidence"); + const before = new Set(fs.existsSync(evidenceDirectory) ? fs.readdirSync(evidenceDirectory) : []); + + try { + const result = runWorkflow(["flightplan", "--files", "docs/example.md", "--json", "--write-evidence"]); + + expect(result.status).toBe(0); + expect(result.stderr).toBe(""); + const payload = JSON.parse(result.stdout); + expect(payload.evidencePath).toMatch(/workflow-evidence/); + expect(fs.existsSync(payload.evidencePath)).toBe(true); + } finally { + if (fs.existsSync(evidenceDirectory)) { + for (const entry of fs.readdirSync(evidenceDirectory)) { + if (!before.has(entry)) fs.rmSync(path.join(evidenceDirectory, entry), { force: true }); + } + } + } + }); +}); + describe("external workflow portability", () => { it("derives the shared workflow directory from the Git common directory", () => { const root = path.parse(process.cwd()).root; From a1732cf9938a8ee3a6ffe204e4f1cba169d1364a Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Fri, 17 Jul 2026 19:25:01 +0800 Subject: [PATCH 6/7] fix(workflow): preserve RAG lab approval gates --- docs/branch-review-ledger.md | 2 +- scripts/productivity-core.mjs | 1 + tests/productivity-workflow.test.ts | 9 +++++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index 0104a19fe..9f6e8acbc 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -569,4 +569,4 @@ Use this ledger to prevent repeated branch and PR reviews when the reviewed HEAD | 2026-07-17 | codex/historical-branch-cleanup-20260717 | e36ac0c6628264c7ed6c494a597a62d0214b68f6 | branch-cleanup and historical-content recovery | Completed the pending historical cleanup: deleted 55 exact-SHA remote refs and 20 redundant local refs, removed nine clean merged worktrees, preserved every dirty, active, open-PR, or patch-unique worktree, and recovered the still-useful governance incident runbook from `codex/domain-1-governance-remediation`. Historical code changes were either tied to merged PRs or reviewed as superseded by current implementations; open PRs #699, #700, #702, and #704 remain protected. | Fresh `git fetch --prune`; GitHub PR inventory and exact commit-to-merged-PR associations; exact remote SHA rechecks before deletion; cherry-pick-aware logs; two-dot tree and branch-only-file review; Codex task-to-worktree cross-check; focused documentation validation recorded in the cleanup PR. No OpenAI, Supabase, production-data, or live clinical workflow was run. | | 2026-07-17 | PR #704 / codex/scroll-geometry-stability-20260717 | 35e74ddbd61bacc5b34f06efbd58091f092665fd | nested scroll-source review follow-up | Confirmed the outside-diff CodeRabbit finding: the standalone shell shared one intent history across main and descendant scroll containers, so a switch from a deep main offset to a near-zero nested offset could falsely reveal chrome. Scroll metrics now identify their source, source changes rebase direction and travel while preserving visibility, and unit/UI regressions cover the switch. No unresolved actionable review finding remains. | Focused Vitest 9/9; TypeScript; scoped ESLint; Prettier; `git diff --check`. Exact-head hosted CI and UI remain required after push. No Supabase/OpenAI/live-provider checks run. | | 2026-07-17 | codex/scrolling-cleanup-20260717 | ff77cd06c + latest origin/main sync | cross-page scrolling and interaction stability review | Fixed two confirmed P2 defects: desktop action-popup placement performed synchronous geometry work for every captured scroll event, and submitted differential searches with zero document matches fell back to the home state. Placement is now coalesced per animation frame with passive scroll listeners, and submitted empty-evidence results remain visible. Hardened three popup/navigation browser helpers that reproduced hydration timing failures. No other high-confidence defect remains in the scoped diff. | Scroll-focused Chromium 28/28; final affected Chromium 5/5; source regressions 2/2; scoped ESLint; Prettier; TypeScript and production build passed before the final upstream-only sync; `git diff --check`. The aggregate local Vitest/UI runs were affected by concurrent-worktree resource contention, so exact-head hosted CI remains required before merge. No Supabase/OpenAI/live-provider checks run. | -| 2026-07-17 | PR #713 / codex/chat-workflow-ideas-0916 | b52112df6aa36311d7420189064acd79dcf2c3f5 + reviewed follow-up diff | workflow toolkit review follow-up | Fixed all 12 actionable Codex and CodeRabbit threads: cross-platform path fixtures, installation-managed preflight guidance, answer-route database scoping, per-command approval boundaries, plugin-ignore narrowing, isolated CI-scope proof, remote-Git command guarding, repository-skill verification classification, `TypeError` diagnosis, strict CLI option values, and machine-parseable JSON evidence output. No unresolved actionable finding remains in the reviewed scope. | `npm run verify:cheap` passed with 273 files and 2,599 tests; focused toolkit Vitest 13/13; CI-scope self-test; plugin-ignore proof; `git diff --check`. Exact-head hosted CI remains required after the follow-up push. No Supabase, OpenAI, or other live product-provider command was run. | +| 2026-07-17 | PR #713 / codex/chat-workflow-ideas-0916 | b52112df6aa36311d7420189064acd79dcf2c3f5 + reviewed follow-up diff | workflow toolkit review follow-up | Fixed all 13 actionable Codex and CodeRabbit threads: cross-platform path fixtures, installation-managed preflight guidance, answer-route database scoping, per-command approval boundaries, plugin-ignore narrowing, isolated CI-scope proof, remote-Git command guarding, repository-skill verification classification, `TypeError` diagnosis, strict CLI option values, machine-parseable JSON evidence output, and preservation of baseline database/clinical approval gates in the RAG lab. No unresolved actionable finding remains in the reviewed scope. | `npm run verify:cheap` passed with 273 files and 2,599 tests; focused toolkit Vitest 14/14; CI-scope self-test; plugin-ignore proof; `git diff --check`. Exact-head hosted CI remains required after the follow-up push. No Supabase, OpenAI, or other live product-provider command was run. | diff --git a/scripts/productivity-core.mjs b/scripts/productivity-core.mjs index 4fb255aa7..70c45c21b 100644 --- a/scripts/productivity-core.mjs +++ b/scripts/productivity-core.mjs @@ -215,6 +215,7 @@ export function buildWorkflowPlan(workflow, files = [], options = {}) { ]; approvalRequired = uniqueBy( [ + ...approvalRequired, check("npm run eval:retrieval:quality", "Require 36/36 against the live corpus after explicit approval."), check("npm run eval:rag -- --limit 15", "Sample live answer generation after explicit approval."), check("npm run eval:quality -- --rag-only", "Compare grounded-answer invariants after explicit approval."), diff --git a/tests/productivity-workflow.test.ts b/tests/productivity-workflow.test.ts index 6e681ce8c..2880de1ec 100644 --- a/tests/productivity-workflow.test.ts +++ b/tests/productivity-workflow.test.ts @@ -62,6 +62,15 @@ describe("productivity workflow planning", () => { ); }); + it("preserves database and clinical approval gates in the RAG lab", () => { + const plan = buildWorkflowPlan("rag-lab", ["src/app/api/answer/route.ts"]); + const commands = plan.approvalRequired.map((item: { command: string }) => item.command); + + expect(commands).toContain("npm run check:supabase-project"); + expect(commands).toContain("npm run check:production-readiness"); + expect(commands).toContain("npm run eval:retrieval:quality"); + }); + it("keeps the design sweep local and covers the established breakpoint proof", () => { const plan = buildWorkflowPlan("design-sweep", ["src/components/ClinicalDashboard.tsx"]); From 07b4af045e9cf976df8a304012e65ce92d4c7195 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Fri, 17 Jul 2026 19:29:21 +0800 Subject: [PATCH 7/7] fix(workflow): align database route classification --- docs/branch-review-ledger.md | 2 +- scripts/productivity-core.mjs | 2 +- tests/productivity-workflow.test.ts | 12 ++++++++++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index 9f6e8acbc..b18697ac0 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -569,4 +569,4 @@ Use this ledger to prevent repeated branch and PR reviews when the reviewed HEAD | 2026-07-17 | codex/historical-branch-cleanup-20260717 | e36ac0c6628264c7ed6c494a597a62d0214b68f6 | branch-cleanup and historical-content recovery | Completed the pending historical cleanup: deleted 55 exact-SHA remote refs and 20 redundant local refs, removed nine clean merged worktrees, preserved every dirty, active, open-PR, or patch-unique worktree, and recovered the still-useful governance incident runbook from `codex/domain-1-governance-remediation`. Historical code changes were either tied to merged PRs or reviewed as superseded by current implementations; open PRs #699, #700, #702, and #704 remain protected. | Fresh `git fetch --prune`; GitHub PR inventory and exact commit-to-merged-PR associations; exact remote SHA rechecks before deletion; cherry-pick-aware logs; two-dot tree and branch-only-file review; Codex task-to-worktree cross-check; focused documentation validation recorded in the cleanup PR. No OpenAI, Supabase, production-data, or live clinical workflow was run. | | 2026-07-17 | PR #704 / codex/scroll-geometry-stability-20260717 | 35e74ddbd61bacc5b34f06efbd58091f092665fd | nested scroll-source review follow-up | Confirmed the outside-diff CodeRabbit finding: the standalone shell shared one intent history across main and descendant scroll containers, so a switch from a deep main offset to a near-zero nested offset could falsely reveal chrome. Scroll metrics now identify their source, source changes rebase direction and travel while preserving visibility, and unit/UI regressions cover the switch. No unresolved actionable review finding remains. | Focused Vitest 9/9; TypeScript; scoped ESLint; Prettier; `git diff --check`. Exact-head hosted CI and UI remain required after push. No Supabase/OpenAI/live-provider checks run. | | 2026-07-17 | codex/scrolling-cleanup-20260717 | ff77cd06c + latest origin/main sync | cross-page scrolling and interaction stability review | Fixed two confirmed P2 defects: desktop action-popup placement performed synchronous geometry work for every captured scroll event, and submitted differential searches with zero document matches fell back to the home state. Placement is now coalesced per animation frame with passive scroll listeners, and submitted empty-evidence results remain visible. Hardened three popup/navigation browser helpers that reproduced hydration timing failures. No other high-confidence defect remains in the scoped diff. | Scroll-focused Chromium 28/28; final affected Chromium 5/5; source regressions 2/2; scoped ESLint; Prettier; TypeScript and production build passed before the final upstream-only sync; `git diff --check`. The aggregate local Vitest/UI runs were affected by concurrent-worktree resource contention, so exact-head hosted CI remains required before merge. No Supabase/OpenAI/live-provider checks run. | -| 2026-07-17 | PR #713 / codex/chat-workflow-ideas-0916 | b52112df6aa36311d7420189064acd79dcf2c3f5 + reviewed follow-up diff | workflow toolkit review follow-up | Fixed all 13 actionable Codex and CodeRabbit threads: cross-platform path fixtures, installation-managed preflight guidance, answer-route database scoping, per-command approval boundaries, plugin-ignore narrowing, isolated CI-scope proof, remote-Git command guarding, repository-skill verification classification, `TypeError` diagnosis, strict CLI option values, machine-parseable JSON evidence output, and preservation of baseline database/clinical approval gates in the RAG lab. No unresolved actionable finding remains in the reviewed scope. | `npm run verify:cheap` passed with 273 files and 2,599 tests; focused toolkit Vitest 14/14; CI-scope self-test; plugin-ignore proof; `git diff --check`. Exact-head hosted CI remains required after the follow-up push. No Supabase, OpenAI, or other live product-provider command was run. | +| 2026-07-17 | PR #713 / codex/chat-workflow-ideas-0916 | b52112df6aa36311d7420189064acd79dcf2c3f5 + reviewed follow-up diff | workflow toolkit review follow-up | Fixed all 14 actionable Codex and CodeRabbit threads: cross-platform path fixtures, installation-managed preflight guidance, complete Supabase-backed API database scoping, per-command approval boundaries, plugin-ignore narrowing, isolated CI-scope proof, remote-Git command guarding, repository-skill verification classification, `TypeError` diagnosis, strict CLI option values, machine-parseable JSON evidence output, and preservation of baseline database/clinical approval gates in the RAG lab. No unresolved actionable finding remains in the reviewed scope. | `npm run verify:cheap` passed with 273 files and 2,599 tests; focused toolkit Vitest 20/20; CI-scope self-test; plugin-ignore proof; `git diff --check`. Exact-head hosted CI remains required after the follow-up push. No Supabase, OpenAI, or other live product-provider command was run. | diff --git a/scripts/productivity-core.mjs b/scripts/productivity-core.mjs index 70c45c21b..43f634bdc 100644 --- a/scripts/productivity-core.mjs +++ b/scripts/productivity-core.mjs @@ -17,7 +17,7 @@ const RISK_PATTERNS = { database: [ "supabase", "src/lib/supabase", - /^src\/app\/api\/(?:answer|documents|images|ingestion|jobs|search|upload)/, + /^src\/app\/api\/(?:answer|differentials|documents|eval-cases|health|images|ingestion|jobs|medications|registry|search|setup-status|upload)/, ], retrieval: [ /^src\/lib\/(?:rag|retrieval|ranking|clinical-search|smart-rag|citations|evidence|source|chunking)/, diff --git a/tests/productivity-workflow.test.ts b/tests/productivity-workflow.test.ts index 2880de1ec..219a1c5c4 100644 --- a/tests/productivity-workflow.test.ts +++ b/tests/productivity-workflow.test.ts @@ -62,6 +62,18 @@ describe("productivity workflow planning", () => { ); }); + it.each(["differentials", "eval-cases", "health", "medications", "registry", "setup-status"])( + "treats the %s API route as database-scoped", + (route) => { + const plan = buildWorkflowPlan("flightplan", [`src/app/api/${route}/route.ts`]); + + expect(plan.risks.database).toBe(true); + expect(plan.approvalRequired.map((item: { command: string }) => item.command)).toContain( + "npm run check:supabase-project", + ); + }, + ); + it("preserves database and clinical approval gates in the RAG lab", () => { const plan = buildWorkflowPlan("rag-lab", ["src/app/api/answer/route.ts"]); const commands = plan.approvalRequired.map((item: { command: string }) => item.command);