Skip to content
Merged
12 changes: 12 additions & 0 deletions .claude/hooks/session-start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ if [ "${CLAUDE_CODE_REMOTE:-}" != "true" ]; then
exit 0
fi

payload="$(cat 2>/dev/null || true)"

NODE_VERSION="24.19.0"
# Keep in step with the floor in package.json engines.node. A matching major is
# not enough: dev dependencies (jsdom) carry a minor-level floor, so a 24.13 on
Expand Down Expand Up @@ -102,12 +104,22 @@ if [ ! -d node_modules ]; then
mkdir -p "$(dirname "$LOCK_STAMP")"
echo "$lock_hash" > "$LOCK_STAMP"
echo "[session-start] Dependencies installed"
status_msg="Dependencies installed"
elif [ ! -f "$LOCK_STAMP" ] || [ "$(cat "$LOCK_STAMP")" != "$lock_hash" ]; then
echo "[session-start] node_modules is stale for the current lockfile, reinstalling"
npm ci --no-audit --no-fund
mkdir -p "$(dirname "$LOCK_STAMP")"
echo "$lock_hash" > "$LOCK_STAMP"
echo "[session-start] Dependencies reinstalled"
status_msg="Dependencies reinstalled"
else
echo "[session-start] node_modules matches the lockfile, skipping install"
status_msg="node_modules matches the lockfile, skipping install"
fi

if [ -n "${CLAUDE_ENV_FILE:-}" ] || printf '%s' "$payload" | grep -Eq '"(hook_event_name|hookEventName)"[[:space:]]*:[[:space:]]*"SessionStart"'; then
node_v="$(node -v 2>/dev/null || echo 'unknown')"
npm_v="$(npm -v 2>/dev/null || echo 'unknown')"
context="[session-start] Node ${node_v} / npm ${npm_v} ready. ${status_msg}."
printf '{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"%s"}}\n' "$context"
fi
13 changes: 9 additions & 4 deletions data/repo-awareness-snapshot.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"version": "repo-awareness-snapshot-v1",
"captured_revision": {
"sha": "e7c67a0d1984c1af856123ee041e27ec17e15022",
"committed_at": "2026-08-25T21:59:40+08:00"
"sha": "a5d918057c1fb8103f8aa3b57b8c4056aea533c6",
"committed_at": "2026-08-26T00:44:09+08:00"
},
"routes": {
"modes": [
Expand Down Expand Up @@ -2113,6 +2113,11 @@
"section": "root",
"catalogued": true
},
{
"path": "docs/continuous-integration.md",
"section": "root",
"catalogued": true
},
{
"path": "docs/current-clinical-work-brief.md",
"section": "root",
Expand Down Expand Up @@ -3576,8 +3581,8 @@
}
],
"counts": {
"documents": 430,
"catalogued": 101,
"documents": 431,
"catalogued": 102,
"uncatalogued": 329,
"sections": 18
}
Expand Down
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ npm run docs:check-links
## Process and review

- [process-hardening.md](process-hardening.md) — verification gates, CI expectations, known debts
- [continuous-integration.md](continuous-integration.md) — workflow concurrency keys, push exemption, and Guard 2 in-flight CI push guard
- [testing.md](testing.md) — test execution, focused/live commands, Playwright ownership, flake policy
- [phone-chrome-physical-acceptance.md](phone-chrome-physical-acceptance.md) — labelled Safari and cold-launch PWA acceptance matrix
- [productivity-workflows.md](productivity-workflows.md) — repo workflow planners (flightplan, triage, rag-lab, …)
Expand Down
2 changes: 2 additions & 0 deletions docs/ci-operations.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# CI Operations and Runner Usage Assessment

See also [continuous-integration.md](continuous-integration.md) for pre-push safety controls and Guard 2 in-flight CI push guard details.

## Overview and Concurrency Architecture

In PR #2209 (merged `af2075a`), GitHub Actions workflow concurrency for base-branch (`main`, `release/**`) pushes was changed to key on `github.run_id`:
Expand Down
2 changes: 2 additions & 0 deletions docs/clinical-hazard-analysis.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ _Pathway: coverage/confidence/trust gating. Note: the **coverage gate** (`evalua
| **H5d** | Hardcoded clozapine / patient-property branches in the coverage gate use looser accept criteria than the generic path ([`rag.ts:3726`](../src/lib/rag/rag.ts)) — inconsistent strictness per drug. | Low–Medium | Medium | downstream gates apply uniformly | none asserts cross-drug parity | Per-drug literals in a safety-adjacent gate. |
| **H5e** | Out-of-order response painting the wrong answer under a new question — **controlled**: a monotonic `searchRequestSeqRef` guards every state write incl. streamed progress ([`ClinicalDashboard.tsx`](../src/components/ClinicalDashboard.tsx), audit M10). | High (if it regressed) | Low | request-id guard | none (client React logic) | Minor: superseded stream has no `AbortController` (resource hygiene only). |

### H5: Provenance tags and synthetic summaries <a id="h5-provenance-tags"></a><a id="h5-provenance-tags-and-synthetic-summaries"></a>

**H5a update (2026-08-17, coordinator review):** the fast-path half of H5a is now partially
mitigated — `deriveConfidence` (`src/lib/rag/rag-answer-support.ts`) computes
`strongestNonSynthetic` by excluding rows tagged `similarity_origin: "synthetic_text"` and
Expand Down
48 changes: 48 additions & 0 deletions docs/continuous-integration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Continuous Integration and Workflow Concurrency

## Overview and Concurrency Architecture

The repository enforces robust concurrency controls across GitHub Actions workflows to guarantee complete verification of merged commits and prevent cancellation storms on active pull requests.

### 1. Base-Branch Push Concurrency (`ci.yml`)

In `.github/workflows/ci.yml`, workflow concurrency is configured with a per-run group for pushes, schedules, and dispatches, while pull requests use ref-based deduplication:

```yaml
concurrency:
group: ${{ github.workflow }}-${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'schedule' || github.event_name == 'push') && github.run_id || github.ref }}
cancel-in-progress: ${{ github.event_name != 'push' }}
```

- **Per-Run Concurrency Key for Base Branches:** Keying base-branch pushes on `github.run_id` ensures that every merged commit landing on `main` or `release/**` receives independent, isolated CI verification.
- **Queue Eviction Prevention:** GitHub Actions natively limits concurrency groups to at most one pending run. Without the per-run key, rapid merges evict waiting runs from the queue, destroying verification on intermediate commits.
- **Push Exemption from `cancel-in-progress`:** `cancel-in-progress: ${{ github.event_name != 'push' }}` explicitly guarantees that in-flight base-branch validation runs are never terminated by subsequent commits.

### 2. Eval Canary Concurrency (`eval-canary.yml`)

The weekly and on-demand evaluation canary runs with a dedicated single-flight group:

```yaml
concurrency:
group: eval-canary
cancel-in-progress: false
```

This prevents multiple live evaluation workflows from overlapping or colliding against the shared live Supabase/OpenAI evaluation test harnesses.

---

## Pre-Push Safety: Guard 2 (In-Flight CI Push Guard)

To eliminate the anti-pattern where frequent branch syncs (e.g. repeated `git merge origin/main` loops) restart CI and cancel in-flight runs via `cancel-in-progress` (#TF6TPJ, #HSSHRG), `scripts/guard-push.mjs` enforces **Guard 2: in-flight CI push guard**.

### Mechanism

1. **Active Run Detection:** When pushing to an open PR branch, `findInFlightCiRuns()` inspects the branch's workflow runs for required CI (`ci.yml`) in active states:
- `pending`, `queued`, `in_progress`, `requested`, `waiting`.
2. **Push Interception:** If a required CI run is already in progress, `inFlightCiVerdict()` blocks the push before local refs are transmitted, logging the active run ID and PR number.
3. **Prevention of False-Red CI:** By preventing superfluous pushes while CI evaluates, Guard 2 ensures test suites complete and prevents `pr-required` from reporting false-red aggregate status caused by self-inflicted cancellations.
4. **Override:** In exceptional circumstances where an immediate force-update is required, set:
```bash
SKIP_IN_FLIGHT_CI_GUARD=1 git push
```
7 changes: 5 additions & 2 deletions docs/launch-operator-runbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,12 @@ Legend: **⏸ PAUSE** = provider action, needs your approval · **✅ verify** =
## 0. Pre-flight (read-only)

```bash
npm run check:supabase-project # must report Clinical KB Database / sjrfecxgysukkwxsowpy
node -v # must report >= 24.15.0 < 25 (Node 24 engine floor)
npm -v # must report >= 11.0.0 < 12 (npm 11)
npm run check:runtime # validates Node 24 and npm 11 engines
npm run check:supabase-project # must report Clinical KB Database / sjrfecxgysukkwxsowpy
npx supabase migration list --linked
npm run reindex:health # note jobs_pending / jobs_processing (needed for step 1 R17)
npm run reindex:health # note jobs_pending / jobs_processing (needed for step 1 R17)
```

## 1. Confirm migration state; apply only unresolved controls 🧑 Supabase
Expand Down
2 changes: 1 addition & 1 deletion docs/site-map.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ This file is generated by `npm run docs:update` (or `npm run sitemap:update` dir
- `/tools` - Clinical tools and applications launcher directory. Source: `src/app/(search-app)/tools/page.tsx`.
- `/ward-management` - Statewide psychiatry ward demand, bed capacity, and patient flow console. Source: `src/app/ward-management/page.tsx`.
- `/ward-management/capacity` - Ward bed availability, unit occupancy, and staffing capacity. Source: `src/app/ward-management/capacity/page.tsx`.
- `/ward-management/constellation` - Compatibility redirect to `/ward-management/network`. Phase 2 retired the constellation command view. Source: `src/app/ward-management/constellation/page.tsx`.
- `/ward-management/constellation` - Intentional, unlinked backwards-compatibility redirect to `/ward-management/network` (pointing to the current Ward Management home). Phase 2 retired the constellation command view. Source: `src/app/ward-management/constellation/page.tsx`.
- `/ward-management/ed/[edId]` - Synthetic emergency-department role screen for one origin department. Source: `src/app/ward-management/ed/[edId]/page.tsx`.
- `/ward-management/exceptions` - Patient flow exceptions, delays, and escalation alerts. Source: `src/app/ward-management/exceptions/page.tsx`.
- `/ward-management/governance` - Ward coordination governance, compliance, and audit log. Source: `src/app/ward-management/governance/page.tsx`.
Expand Down
16 changes: 16 additions & 0 deletions docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,22 @@ downloadable expected/actual/diff artifact instead of a failed check. Missing ba
runtime/assertion, and artifact-publication failures remain visible as job failures because those
runs produced no trustworthy comparison evidence.

**Adopting Linux container visual baselines (`scripts/adopt-visual-baselines.mjs`).**
When document viewer layout changes (e.g. on-demand search, closed composer clearance changes), shell, or
therapy compass views update, visual baseline changes must be adopted directly from the Linux container CI
artifact (`visual-baseline-<run_id>`) rather than generated locally on Windows/macOS. Run:

```bash
node scripts/adopt-visual-baselines.mjs \
--from <extracted-artifact-dir> \
--run-id <id> \
--head <40-char-sha> \
--reviewed-by "<display name>" \
--write
```

This updates the authoritative Linux baselines under `tests/__screenshots__/linux/` and regenerates `tests/__screenshots__/linux/provenance.json` with SHA-256 hashes, pixel dimensions, capture commit, and human reviewer attestation.

## Performance budget

`npm run verify:lighthouse` builds and serves an isolated production app in demo mode, measures the
Expand Down
3 changes: 3 additions & 0 deletions docs/worker-deploy-runbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ _"apply before worker redeploy"_) for the ordered apply plan.
Confirm the gate before continuing:

```bash
node -v # must report >= 24.15.0 < 25 (Node 24 engine floor)
npm -v # must report >= 11.0.0 < 12 (npm 11)
npm run check:runtime # validates Node 24 and npm 11 engines
npm run reindex:health # ok:true, and the RPC signatures accept p_worker_id
```

Expand Down
2 changes: 1 addition & 1 deletion scripts/generate-site-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ const routeDescriptions: Record<string, string> = {
"/ward-management": "Statewide psychiatry ward demand, bed capacity, and patient flow console.",
"/ward-management/capacity": "Ward bed availability, unit occupancy, and staffing capacity.",
"/ward-management/constellation":
"Compatibility redirect to `/ward-management/network`. Phase 2 retired the constellation command view.",
"Intentional, unlinked backwards-compatibility redirect to `/ward-management/network` (pointing to the current Ward Management home). Phase 2 retired the constellation command view.",
"/ward-management/ed/[edId]": "Synthetic emergency-department role screen for one origin department.",
"/ward-management/exceptions": "Patient flow exceptions, delays, and escalation alerts.",
"/ward-management/governance": "Ward coordination governance, compliance, and audit log.",
Expand Down
37 changes: 37 additions & 0 deletions tests/session-start-hook.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,43 @@ describe("session-start hook", () => {
expect(result.status).toBe(0);
expect(result.stdout.trim()).toBe("");
});

it("emits valid JSON on stdout matching SessionStart schema when running in hook mode", () => {
const { home, project, hook } = stubEnvironment();
const envFile = join(project, "claude-env");
writeFileSync(envFile, "");

const payload = JSON.stringify({ hook_event_name: "SessionStart" });
const result = spawnSync(bashCommand, [hook.replace(/\\/g, "/")], {
cwd: project,
env: {
...process.env,
HOME: home.replace(/\\/g, "/"),
CLAUDE_CODE_REMOTE: "true",
CLAUDE_ENV_FILE: envFile.replace(/\\/g, "/"),
CLAUDE_PROJECT_DIR: project.replace(/\\/g, "/"),
} as NodeJS.ProcessEnv,
encoding: "utf8",
input: payload,
});

expect(result.status, `hook exited ${result.status}: ${result.stderr}`).toBe(0);
expect(result.stderr).toBe("");

const jsonLine = result.stdout
.split(/\r?\n/)
.map((line) => line.trim())
.find((line) => line.startsWith('{"hookSpecificOutput"'));

expect(jsonLine, "expected hook to emit hookSpecificOutput JSON on stdout").toBeDefined();
const parsed = JSON.parse(jsonLine!);
expect(parsed).toEqual({
hookSpecificOutput: {
hookEventName: "SessionStart",
additionalContext: expect.stringContaining("[session-start]"),
},
});
});
});

describe("precompact observability hook", () => {
Expand Down
39 changes: 39 additions & 0 deletions tests/verify-phone-chrome.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,31 @@ const ids = (files: string[], fullMode: "auto" | "always" | "never" = "auto") =>

const stage = (files: string[], id: string) => phoneChromePlan(files).stages.find((candidate) => candidate.id === id);

const PHONE_CHROME_EXECUTED_CONTRACT_COUNT = 135;

function countExecutedVitestCases(source: string): number {
const code = source.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "");
const pattern = /(?:^|\n)\s*(?:it|test)(?:\.each\s*\((?<table>[\s\S]*?)\))?\s*\(/g;
let count = 0;
for (const match of code.matchAll(pattern)) {
const table = match.groups?.table?.trim();
if (!table) {
count += 1;
continue;
}
if (!table.startsWith("[")) {
count += 1;
continue;
}
count += table
.slice(1, table.endsWith("]") ? -1 : undefined)
.split(",")
.map((part) => part.trim())
.filter(Boolean).length;
}
return count;
}

describe("phoneChromePlan", () => {
it("keeps documentation-only work out of browser suites", () => {
expect(ids(["docs/phone-chrome-physical-acceptance.md"])).toEqual(["docs-index", "docs-links"]);
Expand Down Expand Up @@ -119,6 +144,20 @@ describe("phoneChromePlan", () => {
expect(plan.notes.join(" ")).not.toContain("No phone-chrome-affecting file was detected");
},
);

it("maintains the complete 9-suite phone-chrome contract baseline with 135 executed contracts", () => {
const plan = phoneChromePlan(["tests/header-scroll-hide-contract.test.ts"]);
const contractStage = plan.stages.find((candidate) => candidate.id === "contracts");
expect(contractStage).toBeDefined();
const contractFiles = (contractStage?.command.args as string[]).filter((arg) => arg.startsWith("tests/"));
expect(contractFiles).toHaveLength(9);
const executedContracts = contractFiles.reduce((total, file) => {
const cases = countExecutedVitestCases(readFileSync(resolve(process.cwd(), file), "utf8"));
expect(cases, `${file} must declare at least one executed contract`).toBeGreaterThan(0);
return total + cases;
}, 0);
expect(executedContracts).toBe(PHONE_CHROME_EXECUTED_CONTRACT_COUNT);
});
Comment thread
BigSimmo marked this conversation as resolved.
});

describe("runPhoneChromeStages", () => {
Expand Down
Loading