Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .claude/agents/fhir-conformance-reviewer.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,5 +65,7 @@ Anything you notice that is wrong, misplaced, or suspicious OUTSIDE your
assigned scope (code in the wrong crate, a duplicated definition, a stale
claim, a missing test, a dependency smell) goes in your final report under an
explicit "En-route findings" heading, each with file:line and one sentence of
evidence, so the orchestrator files a tracker issue for it. "Not in my task
list" is never a reason to stay silent. Report; do not fix.
evidence and the type (Bug, Feature or Task) and priority you would give it, so
the orchestrator files a tracker issue for it with `scripts/gh/fields.sh new`.
You file no issue yourself. "Not in my task list" is never a reason to stay
silent. Report; do not fix.
7 changes: 4 additions & 3 deletions .claude/agents/implementer.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ Anything you notice that is wrong, misplaced, or suspicious OUTSIDE your
assigned scope (code in the wrong crate, a duplicated definition, a stale
claim, a missing test, a dependency smell) goes in your final report under an
explicit "En-route findings" heading, each with file:line and one sentence of
evidence, so the orchestrator files a tracker issue for it. "It was already
there" is never a reason to stay silent. Do not fix out-of-scope findings
yourself; report them.
evidence and the type (Bug, Feature or Task) and priority you would give it, so
the orchestrator files a tracker issue for it with `scripts/gh/fields.sh new`.
You file no issue yourself. "It was already there" is never a reason to stay
silent. Do not fix out-of-scope findings yourself; report them.
7 changes: 4 additions & 3 deletions .claude/agents/leptos-reviewer.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ Anything you notice that is wrong, misplaced, or suspicious OUTSIDE your
assigned scope (code in the wrong crate, a duplicated definition, a stale
claim, a missing test, a dependency smell) goes in your final report under an
explicit "En-route findings" heading, each with file:line and one sentence of
evidence, so the orchestrator files a tracker issue for it. "It was already
there" is never a reason to stay silent. Do not fix out-of-scope findings
yourself; report them.
evidence and the type (Bug, Feature or Task) and priority you would give it, so
the orchestrator files a tracker issue for it with `scripts/gh/fields.sh new`.
You file no issue yourself. "It was already there" is never a reason to stay
silent. Do not fix out-of-scope findings yourself; report them.
8 changes: 5 additions & 3 deletions .claude/agents/spec-researcher.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ Anything you notice that is wrong, misplaced, or suspicious OUTSIDE your
assigned scope (a stale claim in a doc, a per-version parameter the code will
get wrong, a spec contradiction, a missing test) goes in your final report
under an explicit "En-route findings" heading, each with a location and one
sentence of evidence, so the orchestrator files a tracker issue for it. "Not
in my task list" is never a reason to stay silent. Do not fix out-of-scope
findings yourself; report them.
sentence of evidence and the type (Bug, Feature or Task) and priority you would
give it, so the orchestrator files a tracker issue for it with
`scripts/gh/fields.sh new`. You file no issue yourself. "Not in my task list"
is never a reason to stay silent. Do not fix out-of-scope findings yourself;
report them.
7 changes: 4 additions & 3 deletions .claude/agents/ui-implementer.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ Anything you notice that is wrong, misplaced, or suspicious OUTSIDE your
assigned scope (code in the wrong crate, a duplicated definition, a stale
claim, a missing test, a dependency smell) goes in your final report under an
explicit "En-route findings" heading, each with file:line and one sentence of
evidence, so the orchestrator files a tracker issue for it. "It was already
there" is never a reason to stay silent. Do not fix out-of-scope findings
yourself; report them.
evidence and the type (Bug, Feature or Task) and priority you would give it, so
the orchestrator files a tracker issue for it with `scripts/gh/fields.sh new`.
You file no issue yourself. "It was already there" is never a reason to stay
silent. Do not fix out-of-scope findings yourself; report them.
107 changes: 107 additions & 0 deletions .claude/hooks/block_dangerous.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: BUSL-1.1
# .claude/hooks/block_dangerous.sh
#
# Claude Code PreToolUse hook (matcher: Bash). Blocks destructive commands:
# - a recursive forced delete (delete specific files, use git rm, or work
# under /tmp)
# - force-pushes touching main or master, and bare force-pushes
# - deleting main or master on a remote, and mirror pushes
# - deletion of LICENSE or CLAUDE.md (the licence and the working discipline)
#
# Reads the tool-call JSON on stdin. Exit 2 blocks; exit 0 allows.

set -euo pipefail

payload="$(cat)"

if command -v jq >/dev/null 2>&1; then
cmd="$(printf '%s' "$payload" | jq -r '.tool_input.command // empty' 2>/dev/null || true)"
else
cmd="$payload"
fi
[[ -n "${cmd:-}" ]] || exit 0

# A delete carrying a recursive flag (-r, -R, --recursive) and a force flag
# (-f, --force), in any spelling or order, unless every path it names sits
# under /tmp. Each command of a compound line is judged on its own, so one
# /tmp delete never clears another delete on the same line.
recursive_forced_delete() {
local segment word recursive force path outside
while IFS= read -r segment; do
# Word splitting is the tokenizer here, with globbing off.
set -f
# shellcheck disable=SC2086
set -- $segment
set +f
# Find rm wherever it stands (after sudo, xargs, then, `bash -c "`, a
# backslash), with quotes and backslashes stripped from the word.
word=""
while [[ $# -gt 0 ]]; do
word="${1//[\"\'\\]/}"
shift
[[ "$word" == rm || "$word" == */rm ]] && break
done
[[ "${word:-}" == rm || "${word:-}" == */rm ]] || continue
recursive=0 force=0 outside=0 path=0
for word in "$@"; do
case "$word" in
--recursive) recursive=1 ;;
--force) force=1 ;;
--*) ;;
-*)
[[ "$word" == *[rR]* ]] && recursive=1
[[ "$word" == *f* ]] && force=1
;;
*)
path=1
word="${word//[\"\'\\]/}"
if [[ ! ( "$word" == /tmp/* || "$word" == /private/tmp/* ) || "$word" == *..* ]]; then
outside=1
fi
;;
esac
done
if [[ "$recursive" -eq 1 && "$force" -eq 1 && ( "$outside" -eq 1 || "$path" -eq 0 ) ]]; then
return 0
fi
done <<<"${cmd//[;&|()\`]/$'\n'}"
return 1
}

if recursive_forced_delete; then
echo "BLOCKED: a recursive forced delete is not allowed (block_dangerous hook). Delete specific files with 'git rm' or a plain 'rm <file>', or operate under /tmp." >&2
exit 2
fi

# Deleting main or master on a remote is never allowed: `git push origin
# :main`, `--delete main`, or `-d main`, and a mirror push, which can delete
# every remote branch.
if printf '%s' "$cmd" | grep -qE 'git[[:space:]]+push[^;|&]*([[:space:]]:(refs/heads/)?(main|master)([[:space:]]|$)|--delete[^;|&]*[[:space:]](main|master)([[:space:]]|$)|[[:space:]]-d[[:space:]][^;|&]*(main|master)([[:space:]]|$)|--mirror)'; then
echo "BLOCKED: deleting main or master on a remote, or a mirror push, is forbidden (CLAUDE.md hard rule)." >&2
exit 2
fi

# Force pushes: never to main or master; a bare force-push is refused too.
# The protected names match only as WHOLE REF WORDS (delimiter-bounded, so
# `refs/heads/main`, `origin main`, and `HEAD:main` all hit) and never as raw
# substrings of the command line, which would falsely block a feature branch
# whose name merely CONTAINS a protected name.
if printf '%s' "$cmd" | grep -qE 'git[[:space:]]+push[^;|&]*(--force([^-]|$)|--force-with-lease|[[:space:]]-f([[:space:]]|$)|[[:space:]]\+[[:alnum:]])'; then
if printf '%s' "$cmd" | grep -qE '(^|[[:space:]:/+])(main|master)([[:space:]]|$|["'"'"';&|])'; then
echo "BLOCKED: force-push touching main or master is forbidden (CLAUDE.md hard rule)." >&2
exit 2
fi
if ! printf '%s' "$cmd" | grep -qE '(feat|fix|chore|docs|refactor|perf|test|ci|build|release)/'; then
echo "BLOCKED: bare force-push refused. Force-push (prefer --force-with-lease) only an explicit conventional-type branch (feat/, fix/, chore/, docs/, refactor/, perf/, test/, ci/, build/, release/)." >&2
exit 2
fi
fi

# Never delete the licence or the working discipline.
if printf '%s' "$cmd" | grep -qE '(^|[;&|[:space:]])(git[[:space:]]+rm|rm)[^;|&]*(LICENSE|CLAUDE\.md)([[:space:]]|$|["'"'"';&|])'; then
echo "BLOCKED: LICENSE (the Business Source License 1.1, the project licence) and CLAUDE.md (the working discipline) must not be deleted." >&2
exit 2
fi

exit 0
11 changes: 6 additions & 5 deletions .claude/hooks/session_start_issues.sh
Original file line number Diff line number Diff line change
Expand Up @@ -31,26 +31,27 @@ fi

if [[ "$gh_ok" = "1" ]]; then
repo_nwo="$(gh repo view --json nameWithOwner --jq .nameWithOwner 2>/dev/null || true)"
echo "=== tracker: open GitHub issues (gh issue view <n> --comments for the contract + discussion) ==="
echo "=== tracker: open GitHub issues (read one with: gh issue view <n> --json title,body,comments) ==="
echo "--- pinned (current focus) ---"
# shellcheck disable=SC2016 # $owner/$name are GraphQL variables, expanded by the server
gh api graphql \
-f query='query($owner: String!, $name: String!) { repository(owner: $owner, name: $name) { pinnedIssues(first: 3) { nodes { issue { number title } } } } }' \
-f owner="${repo_nwo%%/*}" -f name="${repo_nwo##*/}" \
--jq '.data.repository.pinnedIssues.nodes[].issue | "#\(.number) \(.title)"' 2>/dev/null || echo " (none)"
echo "--- open (child-of = sub-issue; {k/n} = sub-issue progress; BLOCKED-by = has an open blocker, not a /next-task candidate; relationships: .claude/rules/issue-relationships.md) ---"
echo "--- open (<Type/Priority>; child-of = sub-issue; {k/n} = sub-issue progress; BLOCKED-by = has an open blocker, not a /next-task candidate; relationships: .claude/rules/issue-relationships.md) ---"
# One batched GraphQL call yields each open issue's labels, milestone, parent,
# sub-issue progress, and open blockers/blocks, so the tracker shows work
# structure, not just a flat list.
# shellcheck disable=SC2016 # $owner/$name are GraphQL variables and $labels/$b/$k are jq bindings
# shellcheck disable=SC2016 # $owner/$name are GraphQL variables and $tp/$labels/$b/$k are jq bindings
issues="$(gh api graphql \
-f query='query($owner: String!, $name: String!) { repository(owner: $owner, name: $name) { issues(first: 100, states: OPEN, orderBy: {field: CREATED_AT, direction: DESC}) { nodes { number title labels(first: 20) { nodes { name } } milestone { title } parent { number } subIssuesSummary { total completed } blockedBy(first: 30) { nodes { number state } } blocking(first: 30) { nodes { number state } } } } } }' \
-f query='query($owner: String!, $name: String!) { repository(owner: $owner, name: $name) { issues(first: 100, states: OPEN, orderBy: {field: CREATED_AT, direction: DESC}) { nodes { number title issueType { name } issueFieldValues(first: 10) { nodes { ... on IssueFieldSingleSelectValue { field { ... on IssueFieldSingleSelect { name } } value } } } labels(first: 20) { nodes { name } } milestone { title } parent { number } subIssuesSummary { total completed } blockedBy(first: 30) { nodes { number state } } blocking(first: 30) { nodes { number state } } } } } }' \
-f owner="${repo_nwo%%/*}" -f name="${repo_nwo##*/}" \
--jq '.data.repository.issues.nodes[]
| ((.issueType.name // "?") + "/" + ([.issueFieldValues.nodes[] | select(.field.name == "Priority") | .value] | first // "?")) as $tp
| ([.labels.nodes[].name] | join(", ")) as $labels
| ([.blockedBy.nodes[] | select(.state == "OPEN") | "#\(.number)"]) as $b
| ([.blocking.nodes[] | select(.state == "OPEN") | "#\(.number)"]) as $k
| "#\(.number) \(.title) [\($labels)]"
| "#\(.number) \(.title) <\($tp)> [\($labels)]"
+ (if .milestone then " (\(.milestone.title))" else "" end)
+ (if .parent then " child-of #\(.parent.number)" else "" end)
+ (if .subIssuesSummary.total > 0 then " {\(.subIssuesSummary.completed)/\(.subIssuesSummary.total)}" else "" end)
Expand Down
50 changes: 50 additions & 0 deletions .claude/hooks/versions_guard.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: BUSL-1.1
# .claude/hooks/versions_guard.sh
#
# Claude Code PostToolUse hook (matcher: Write|Edit).
#
# When an edited file is the pin matrix or a file that repeats a pin (the
# product version, the licence parameters, a vendored corpus's PROVENANCE.md
# or the vendor script that writes it), run scripts/checks/versions.sh. Exit 2
# feeds its findings back as a correction; every other path is a quiet exit 0.

set -uo pipefail

payload="$(cat)" || true

if command -v jq >/dev/null 2>&1; then
file_path="$(printf '%s' "$payload" | jq -r '.tool_input.file_path // empty' 2>/dev/null)" || true
else
file_path="$(printf '%s' "$payload" | sed -n 's/.*"file_path"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)"
fi

[[ -n "${file_path:-}" ]] || exit 0

repo_root="${CLAUDE_PROJECT_DIR:-$(cd "$(dirname "$0")/../.." && pwd)}"
rel="${file_path#"$repo_root"/}"
base="$(basename "$file_path")"

watched=0
case "$rel" in
docs/VERSIONS.md | LICENSE | NOTICE | Cargo.toml | rust-toolchain.toml | CITATION.cff | compose.yaml | README.md | website/landing/index.html) watched=1 ;;
*) ;;
esac
case "$base" in
VERSIONS.md | LICENSE | NOTICE | rust-toolchain.toml | CITATION.cff | compose.yaml | README.md | Dockerfile | PROVENANCE.md) watched=1 ;;
*) ;;
esac
case "$rel" in
scripts/vendor/*.sh) watched=1 ;;
*) ;;
esac

[[ "$watched" -eq 1 ]] || exit 0
[[ -x "$repo_root/scripts/checks/versions.sh" ]] || exit 0

findings="$("$repo_root/scripts/checks/versions.sh" 2>&1)" || {
printf '%s\n' "$findings" >&2
exit 2
}

exit 0
5 changes: 3 additions & 2 deletions .claude/memory/MEMORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
- [Owner work style](owner-work-style.md), research-first and evidence-based; from-first-principles, skeptical of the legacy way others build CT servers; confirm foundational decisions before scaffolding; the graph model is right, but implemented as a materialized index, not a graph database or live traversal
- [Official name FerroTERM](official-name-ferroterm.md), FerroTERM (ferroterm.eu) is the official name since 2026-09-02, Notio was the codename; FerroTERM in prose, `ferroterm` in identifiers
- [Local SNOMED release](local-snomed-release.md), the owner's licensed data under data/ (NL edition 20260630, International edition 20260901, LOINC 2.83) and the built artifacts under artifacts/; never committed, fixtures stay synthetic
- [Repo merge gates](repo-merge-gates.md), main needs one approving + code-owner review, signed commits, and the `conclusion` check; Claude stacks PRs and the owner merges; the Roadmap board did not exist as of 2026-09-02
- [Repo merge gates](repo-merge-gates.md), main needs signed commits and the `conclusion` check and merges through the merge queue since 2026-10-05; no rebase train; the Roadmap board is a FerroHEALTH org project
- [Release cut cadence](release-cut-cadence.md), cut the release PR the moment a milestone empties, bump the version EVERYWHERE (README, landing page, book, not only the manifests), run scripts/checks/versions.sh, create the next milestone for stragglers and close the emptied one by hand, and sign and push the tag yourself when the owner asks (v0.1.3, 2026-09-12); verify as a consumer afterwards
- [Container image decisions](container-image-decisions.md), distroless static base pinned by digest, numeric user 65532, docker/Dockerfile with a root .dockerignore, reusable L3 image lane with syft SBOMs per platform and actions/attest, Linux-only release targets, GHCR quirks
- [Post-release verification pause](post-release-verification-pause.md), after the v0.0.3 cut, stop and verify binaries, image, L3 provenance, SBOMs, signatures as a consumer before new work; the owner wants the supply chain proven
Expand All @@ -24,7 +24,7 @@
- [Clean up agent worktrees](clean-up-agent-worktrees.md), remove a subagent's worktree and its branches the moment its PR merges, unprompted; check `gh` for MERGED then `git branch -D` (squash merges defeat `-d`), never touch a live agent's worktree
- [Enum width sets every value](enum-width-sets-every-value.md), a Rust enum costs its largest variant for every value; box the wide rare variants (Cloudflare's 1.1.1.1 DNS cache post is the reference case); this took FerroTERM's `$lookup` from ~630 us to 232 us and is the first thing to check on any layout work
- [e2e is its own workspace](e2e-own-workspace.md), root `cargo fmt --all` and clippy never touch `e2e/tests/`; run both inside `e2e/` or CI's ui-e2e job fails on formatting a round trip later
- [Auto-merge every PR](auto-merge-every-pr.md), owner directive 2026-09-09: enable `gh pr merge <n> --squash --auto --delete-branch` at open time and keep working; never stall waiting for the owner to merge
- [Auto-merge every PR](auto-merge-every-pr.md), owner directive 2026-09-09: arm `gh pr merge <n> --auto` at open time (the merge queue refuses `--squash` and `--delete-branch`) and keep working; never stall waiting for the owner to merge
- [openEHR archetype canonical thread](openehr-archetype-canonical-thread.md), the owner's 2026-09-12 discourse thread on the canonical URL of an archetype-local value set; Seref's answer backs the opaque-canonical stance of #547, Ian mints under openehr.org
- [Forum replies short and plain](forum-replies-short-plain.md), anything the owner posts as themself stays near 100 to 150 words with none of the AI-tell words or hedges; a 408-word draft was rejected hard on 2026-09-12
- [NTS sync program](nts-sync-program.md), owner decisions 2026-09-22: separate ferroterm-sync service, addons/ named after the service (nts first), native scheduler no cron, one way, v0.1.4 reserved for it and the old items moved to v0.1.5
Expand All @@ -33,3 +33,4 @@
- [Git -C absolute paths](git-c-absolute-paths.md), a `cd` in a compound command does not reliably set cwd for git; `git -C /abs/path` everywhere, and a force push checks the branch name in the same command (2026-09-24, PRs #674/#676 collided)
- [NTS account facts](nts-account-facts.md), owner has an NTS account since 2026-09-25 (creds in ~/.ferroterm-nts.env); feed is SNOMED binary-only, FHIR content behind the API (#692), LOINC and NHG hidden until Nictiz grants those licences
- [FerroHEALTH org move](ferrohealth-org-move.md), repo is FerroHEALTH/FerroTERM since 2026-10-01; attestations and the old GHCR package stay under rubentalstra, image path lowercase ghcr.io/ferrohealth, crates.io Trusted Publishing must name FerroHEALTH
- [Native issue types and priority](native-issue-types-and-priority.md), type, priority and effort are the org's native issue type and Priority/Effort fields via scripts/gh/fields.sh since 2026-10-05 (#704); the owner runs migrate-fields.sh apply and labels.sh by hand
Loading
Loading