Template sync audit. Compares this repo against
liulab-repo-template at 815f179
(2026-08-31), auditing this repo at 744ba10. Produced by running the template's
scripts/conformance.py --root against a clean clone, then a file-by-file comparison.
This is a tracking issue, not a work order. Findings are split into Gaps (adopt),
Justified divergences (this repo is right, leave it) and Upstream candidates (this
repo is ahead of the template). Several gaps need a maintainer decision before anyone
starts — those are called out in Notes for the maintainer at the end. Split it into
per-area issues if that suits better.
Coordinator note: G1 verified independently — .gitignore:83 is /.claude/, and
git ls-files shows the eight .agents/skills/* symlinks tracked with no
.claude/skills/* counterpart.
seqforge — template sync gap report
Shape: published — src/seqforge/ + tests/ + CHANGELOG.md + .github/workflows/release.yml
publishing to PyPI by Trusted Publishing; README.md documents pip install seqforge.
Summary: seqforge is the most template-like repo in the lab and in several places it is ahead of
the template — its scripts/check.sh is a strict superset, and it has tests holding that runner to
its own invariants. The drift is concentrated in the writing and conformance half of the
standard: .vale.ini, styles/, scripts/conformance.py and the [tool.liulab] declarations are
all absent, markdownlint is not a step of check, and the tracked .claude/skills/ symlinks are
missing because .gitignore carries the blanket /.claude/ the template forbids. The single most
important fix is that last one: a fresh clone of seqforge gives Claude Code zero of the eight skills
this repo ships.
Gaps
G1. Stop ignoring /.claude/ and track the eight .claude/skills/ symlinks
- Priority: high
- Area: skills
- Now:
.gitignore (last line) has a blanket /.claude/, added for agent worktrees. .claude/
does not exist in the clone at all; git ls-files shows the eight .agents/skills/* symlinks and
no .claude/skills/*.
- Template:
.gitignore names .claude/settings.local.json and .claude/worktrees/ and
nothing else, with the comment: "Never a blanket /.claude/: the skill symlinks under
.claude/skills/ and .agents/skills/ are TRACKED, because init-repo has to be discoverable
the instant you clone — before anyone could run the thing that runs installers." Its
.claude/skills/init-repo and .claude/skills/template-dev are mode-120000 tracked entries.
- Change: in
.gitignore, replace /.claude/ with .claude/settings.local.json and
.claude/worktrees/. Then python skills/install.py --target claude and commit the eight
resulting relative symlinks.
- Why it matters: every clone and every CI checkout of seqforge discovers no skills for Claude
Code until somebody remembers to run the installer. The .agents/skills/ links are correct
(relative, non-dangling, verified) — only the Claude path is lost, and it is lost by one line.
G2. Add scripts/conformance.py and the [tool.liulab] declarations
- Priority: high
- Area: gates
- Now: no
scripts/conformance.py, no conformance pixi task, and no [tool.liulab.agent-docs]
or [tool.liulab.waived] table in pyproject.toml. Running the template's checker against the
clone reports rules 2 and 4 as "not checked" purely because the declarations are absent.
- Template:
scripts/conformance.py (8 rules + 2 warnings), invoked by the conformance task
and named in check-static; declarations at pyproject.toml:166-179.
- Change: copy
scripts/conformance.py verbatim (it is written to be shared), add
conformance = "python scripts/conformance.py" to [tool.pixi.tasks], add pyyaml to the
default environment's dependencies (it is already a runtime dep, so this is free), and declare
[tool.liulab.agent-docs] with seqforge's real paths — AGENTS.md, CONTEXT.md,
docs/adr/, skills/, docs/research/ = false.
- Why it matters: none of the shared rules are mechanised here. G1 is exactly the class of
failure rule 7 exists to catch, and it has been live on main undetected.
G3. Adopt .vale.ini and styles/Lab/
- Priority: high
- Area: writing
- Now: neither file exists; there is no
vale task and no CI job runs one. The lab's writing
rules are entirely unenforced in this repo.
- Template:
.vale.ini (four sections, every rule named in every section) and four tracked
rules under styles/Lab/: Jargon.yml, LengthDoc.yml (1000 words), LengthAdr.yml (400),
Readability.yml.
- Change: copy
.vale.ini and styles/Lab/, add vale = "3.19.0.*" to the default
environment and vale = "vale --glob='!{.pixi,site,dist,build}/**' ." to [tool.pixi.tasks].
Confirm styles/ is not gitignored (it currently is not).
- Why it matters: measured against today's tree, the cost of adopting it is bounded and small.
Six agent-facing files exceed the 1000-word LengthDoc cap — skills/seqforge-kb-author/SKILL.md
(1708), AGENTS.md (1705), src/seqforge/compose/CONTEXT.md (1612),
skills/seqforge-orchestrate/SKILL.md (1392), skills/seqforge-manifest/SKILL.md (1324),
src/seqforge/resolve/CONTEXT.md (1033) — and three records exceed the 400-word LengthAdr cap
(0053-a-mate-that-did-not-align... 757, 0049-stars-command-line... 746,
0051-a-composed-pipeline-runs-on-one-machine 555). CONTEXT-MAP.md at 957 already passes.
G4. Port --check into skills/install.py
- Priority: high
- Area: skills
- Now:
skills/install.py is byte-for-byte the template's file minus the check half. discover,
TARGETS and install_one are identical, including comments. Missing: the CHECKED_TARGETS
constant, _problem(), check(), report_check(), the --check argparse flag and the early
if args.check: return. --check therefore exits 2 with
install.py: error: unrecognized arguments: --check.
- Template:
skills/install.py lines 43-46 (CHECKED_TARGETS), 88-205 (_problem, check,
report_check), 226-230 (the flag) and 234-239 (the early return); the parser also uses
description=__doc__ with RawDescriptionHelpFormatter rather than
description=__doc__.splitlines()[0].
- Change: copy those five blocks in, switch the parser to
__doc__ +
RawDescriptionHelpFormatter, and add the --check paragraph to the module docstring. Keep
seqforge's trailing seqforge hook install line in main(). Nothing else in the file changes.
- Why it matters:
--check is what conformance rule 7 calls, and it is the check that would
have caught G1 — it holds .claude/skills and .agents/skills to three invariants (a link in
both, nothing dangling, every link relative) and prints the repairing command on each problem.
G5. Make markdownlint a step of the gate, and delete the second exclusion list
- Priority: medium
- Area: gates
- Now:
markdownlint is in no pixi task. It runs in two other places: as a pre-commit hook
(.pre-commit-config.yaml, markdownlint-cli2 rev v0.23.1) and as its own CI job
(.github/workflows/ci.yml, npx --yes markdownlint-cli2@0.23.1 on a Node runner).
.markdownlint-cli2.yaml has narrow globs: (docs/**/*.md, the KB specs, README.md) plus an
eleven-entry ignores: list and no gitignore: true.
- Template:
markdownlint = "markdownlint-cli2" is a task and a check-static step, with
markdownlint-cli2 installed from conda-forge. .markdownlint-cli2.yaml is gitignore: true +
globs: ["**/*.md"] + config: {default: true, MD013: false}, with "deliberately no ignores:
list. It used to exist only to mirror a second list of excluded paths kept elsewhere, and two
lists that must agree are a drift waiting to happen."
- Change: add
markdownlint-cli2 to the default environment and a markdownlint task; add it
to the check line in [tool.pixi.tasks]. Then set gitignore: true, widen globs to
["**/*.md"], and drop ignores: — replacing the mkdocs.yml exclude_docs: mirror with
per-file <!-- markdownlint-disable --> where a KB wrapper genuinely has nothing to lint.
- Why it matters: seqforge's own config comment concedes the template's argument — "The
ignores: list below and mkdocs.yml's exclude_docs: are the SAME list… they drifted once
already, turning CI red on every open PR." Two pins of the same tool (npx 0.23.1, pre-commit
0.23.1) plus a third toolchain (Node) to lint markdown is the cost of not having it in pixi.
G6. Move the version to hatch-vcs
- Priority: medium
- Area: toolchain
- Now:
pyproject.toml:24 has version = "2026.7.1" hand-written, [build-system] requires
is ["hatchling"] only, and there is no [tool.hatch.version]. release.yml carries a whole
extra step ("Check tag matches pyproject version") that reads the version back out with tomllib
and compares it to GITHUB_REF_NAME. CHANGELOG.md says the version "tracks [project].version
in pyproject.toml".
- Template:
dynamic = ["version"], requires = ["hatchling", "hatch-vcs"],
[tool.hatch.version] source = "vcs", with "No fallback-version: that is a hand-written
version string."
- Change: add
hatch-vcs to requires, replace the version key with dynamic = ["version"],
add [tool.hatch.version] source = "vcs", delete the tag-check step from release.yml, and make
sure the build job keeps fetch-depth: 0.
- Why it matters: the tag-vs-file check exists only because there are two copies of the version.
The template deletes the second copy instead of guarding it, and the tag stays the single source.
G7. Build the released wheel with pixi, not a separate pip toolchain
- Priority: medium
- Area: ci
- Now:
release.yml's build job uses actions/setup-python@v5 with python-version: "3.12"
and python -m pip install --upgrade build twine, then python -m build. ci.yml's build job
uses pixi run -e default build. The wheel that ships is therefore built by a toolchain no pull
request ever exercises. Neither release job uses fetch-depth: 0.
- Template:
release.yml's build job is setup-pixi with locked: true and pixi run build —
"The same task the build job in ci.yml runs, so what is published was built the way every
pull request already proved."
- Change: swap
setup-python + pip for prefix-dev/setup-pixi with locked: true and
pixi run build; add fetch-depth: 0 to the checkout (required once G6 lands). Keep the
twine check step — see Upstream candidates.
- Why it matters: two build paths that diverge silently, and the divergent one is the only one
whose output reaches users.
G8. Add docs/agents/, starting with writing.md
- Priority: medium
- Area: agent-docs
- Now: no
docs/agents/ directory. .github/ISSUE-CONVENTIONS.md covers what the template
splits across issue-tracker.md and triage-labels.md (and covers it better — see Upstream
candidates). Nothing covers writing.md or domain.md.
- Template:
docs/agents/domain.md, issue-tracker.md, triage-labels.md, writing.md, each
with search: exclude: true front matter and none of them in nav:.
- Change: add
docs/agents/writing.md (the three rules and the cap table) at minimum; either
move .github/ISSUE-CONVENTIONS.md to docs/agents/issue-tracker.md + triage-labels.md or
declare .github/ISSUE-CONVENTIONS.md under [tool.liulab.agent-docs] and leave it where it is.
- Why it matters:
writing.md is where the word caps and the reason for them are written. G3
installs the enforcement; without this document a contributor meets a red vale run with no
explanation of what 1000 means or where the dial lives.
G9. Declare the Python version in two places, not five
- Priority: medium
- Area: toolchain
- Now: five declarations, and they disagree —
requires-python = ">=3.12" (line 9),
classifiers listing both 3.12 and 3.13 (lines 18-19), [tool.pixi.feature.py313.dependencies] python = "3.13.*" (line 172), [tool.ruff] target-version = "py312" (line 299),
[tool.mypy] python_version = "3.12" (line 315), plus python-version: "3.12" in release.yml.
- Template: exactly two —
requires-python and [tool.pixi.dependencies] python, with "No
target-version: ruff infers it from requires-python" and "No pythonVersion: taken from the
interpreter in the environment above."
- Change: delete
[tool.ruff] target-version (ruff infers it). Keep [tool.mypy] python_version — mypy defaults to the running interpreter (3.13) and this repo genuinely
supports 3.12, so this one is load-bearing; add a one-line comment saying so. release.yml's pin
disappears with G7.
- Why it matters: the environment runs 3.13 while the checkers are told 3.12. That is the
correct conservative choice for a package declaring >=3.12, but it is three numbers that must
move together and only one of them has to.
G10. Widen the ruff rule set
- Priority: medium
- Area: config
- Now:
[tool.ruff.lint] select = ["E", "W", "F", "I", "UP", "B"],
ignore = ["E501", "UP046", "UP047"]. No [tool.ruff.lint.pydocstyle], no per-file-ignores.
[tool.ruff] src = ["src", "tests"] — scripts is absent though scripts/ holds three files.
- Template:
select = ["E","W","F","I","UP","B","C4","SIM","PT","PTH","N","D","RUF"],
ignore = ["E501","D203","D213"], convention = "numpy", "tests/**" = ["D"],
src = ["src", "tests", "scripts"].
- Change: add
C4, SIM, PT, PTH, N, RUF first (cheap, mostly autofixable); add D
with the numpy convention and the tests/** exemption as a follow-up, since it will flag a lot on
a package this size. Add "scripts" to src. Keep UP046/UP047 ignored — the pydantic-v2
reason given at line 306 is sound.
- Why it matters:
D is the one that costs something today: seqforge ships to PyPI and has no
docstring gate at all, while mkdocstrings-style API rendering (G16) depends on docstrings existing.
G11. Replace the two pre-commit tool copies with the pixi ones
- Priority: medium
- Area: toolchain
- Now:
.pre-commit-config.yaml installs ruff-pre-commit v0.15.16 and
markdownlint-cli2 v0.23.1 into pre-commit's own environments. pixi separately declares
ruff = "*" in the dev feature. mypy is already routed through pixi
(entry: pixi run --no-progress typecheck), and pytest is deliberately excluded.
- Template: ships no
.pre-commit-config.yaml: "the gate is pixi run check… pre-commit is a
second toolchain installing its own pinned copies of ruff outside pixi."
- Change: either drop the file, or keep it and route
ruff-check/ruff-format/markdownlint
through pixi the way the mypy hook already is (entry: pixi run --no-progress lint, etc.). Keep
the generic pre-commit-hooks and shellcheck entries either way — those install nothing pixi
also owns.
- Why it matters: pinned ruff
v0.15.16 in pre-commit and floating ruff = "*" in pixi will
eventually disagree, and the disagreement surfaces as a commit that pre-commit formatted one way
and CI fails the other. The mypy hook already shows the fix.
G12. Keep the gate's step list in one place, and let CI use it
- Priority: medium
- Area: gates
- Now:
check = "bash scripts/check.sh lint fmt-check typecheck test test-corpus test-external"
— one task, no check-static. ci.yml never calls check.sh; its lint job spells lint,
fmt-check, typecheck and docs-build as four separate steps, and markdownlint lives in a
fifth job with its own toolchain.
- Template:
check-static = "bash scripts/check.sh lint fmt-check typecheck vale markdownlint conformance" and check = { depends-on = ["check-static", "test"] }; ci.yml's check job is a
single pixi run check-static, "so this file and a laptop cannot drift: adding a step is one more
word there and nothing here."
- Change: split
check into check-static (lint, fmt-check, typecheck, vale, markdownlint,
conformance) and check = { depends-on = ["check-static", "test", "test-corpus"] }; make
ci.yml's lint job pixi run check-static.
- Why it matters: seqforge's own
check.sh header documents this drift happening — "the prose
here used to claim the gate ran CI's selections outright, which drifted the moment those jobs were
added and then read as a bug." The template's fix is structural rather than a comment.
G13. Add search: exclude: true front matter to docs/adr/ and docs/research/
- Priority: low
- Area: docs
- Now: no page under
docs/adr/ or docs/research/ carries front matter (checked
docs/adr/0002-*.md, docs/adr/README.md, docs/research/star-on-a-chimera.md). Exclusion
relies entirely on mkdocs.yml's exclude_docs: block plus the explicit nav:.
- Template: every page under
docs/agents/, docs/adr/ and docs/research/ opens with
---\nsearch:\n exclude: true\n---, and "exclude_docs: is silently ignored [by zensical], so
both are needed."
- Change: prepend the three-line front matter block to every file under
docs/adr/,
docs/research/ and src/seqforge/*/docs/adr/.
- Why it matters: correct today — real mkdocs honours
exclude_docs:, so this is not a live
leak. It is cheap insurance against a future zensical move, and it is what conformance rule 2
checks once G2 lands.
G14. Reconcile CONTEXT-MAP.md with the template's CONTEXT.md
- Priority: low
- Area: agent-docs
- Now:
CONTEXT-MAP.md at the root (957 words) is a router to five per-context glossaries under
src/seqforge/*/CONTEXT.md. AGENTS.md links it by that name.
- Template:
CONTEXT.md at the root, declared under [tool.liulab.agent-docs] and capped both
by Lab.LengthDoc and by conformance rule 4's per-entry 200-word limit. The spec calls
CONTEXT-MAP.md "an older name for this [that] should be reconciled".
- Change: the cheapest reconciliation is to rename
CONTEXT-MAP.md to CONTEXT.md (updating
the two links in AGENTS.md) and declare "CONTEXT.md" = "LengthDoc" plus
"src/seqforge/" = "LengthDoc" — or leave the name and declare "CONTEXT-MAP.md". Do not
reshape the multi-context layout itself; see Upstream candidates.
- Why it matters: naming only, but the conformance and vale rules key on the path, so one of the
two files has to move or the declaration has to name the local spelling.
G15. Refresh the GitHub Action pins
- Priority: low
- Area: ci
- Now: across
ci.yml, docs.yml, release.yml, benchmark.yml: actions/checkout@v5,
prefix-dev/setup-pixi@v0.10.0, actions/upload-artifact@v4, actions/download-artifact@v4.
locked: true is set on every setup-pixi — that part is already right.
- Template:
actions/checkout@v7, prefix-dev/setup-pixi@v0.10.2, actions/upload-artifact@v7,
actions/download-artifact@v8, peaceiris/actions-gh-pages@v4,
pypa/gh-action-pypi-publish@release/v1.
- Change: bump the four in one commit.
- Why it matters: cosmetic today; keeping the pins aligned across lab repos is what makes a
future bump one sweep rather than five investigations.
G16. Decide on an API reference page
- Priority: low
- Area: docs
- Now:
mkdocs.yml has plugins: [search] only — no mkdocstrings, no docs/api.md, and no
mkdocstrings-python dependency. The site documents the CLI and the concepts.
- Template: package shapes get
mkdocstrings with the python handler, paths: [src], numpy
docstring style, and docs/api.md in nav:.
- Change: either add mkdocstrings +
docs/api.md (pairs naturally with the D rules in G10),
or record the decision — a compiler whose interface is the CLI (ADR-0013) may legitimately not
want a rendered Python API.
- Why it matters: low. This is a genuine judgement call, not obviously a defect; flagging it so
the answer is written down rather than implied.
Justified divergences
benchmark.yml — the template ships no benchmark workflow and asks a repo that has one to say
why. seqforge says why at length in the file header, and the reasoning holds: it is release +
workflow_dispatch only (never push/PR/schedule), runs --no-llm so no API key or provider secret
is ever exposed to it, keeps permissions: contents: read, and an unreachable package skips rather
than fails. This is the networked tier of a validation corpus, which is a claim per-commit CI
cannot make. No change wanted, and the template need not learn from it — most repos have no corpus.
- mypy
--strict instead of pyright standard — the template's stated goal is one verdict from
editor and CI, and seqforge achieves it the other way round: [tool.pyright] typeCheckingMode = "off" in pyproject.toml (so any pyright-based server honours it, not just VS Code) plus
ms-python.mypy-type-checker pointed at the pixi interpreter. [tool.mypy] files owns the scope
so the typecheck task is bare. Strictly stronger than the template's setting; leave it.
- Three test lanes and the
test-star environment — the unit / hermetic-corpus / external
partition, no-default-feature + a separate solve group for the aligner, and the divided
--maxprocesses caps are all measured decisions documented in pyproject.toml and ci.yml. The
template's single test task is right for a repo with no external binaries; this is right here.
- mkdocs-material rather than zensical;
exclude_docs: in mkdocs.yml — the site predates
ADR-0001 and leans on material-specific features (pymdownx.snippets with check_paths, mermaid
fences, the palette, the shared GA4 property). exclude_docs: is only silently ignored by
zensical; real mkdocs honours it, so the exclusion works today. Revisit if the site ever moves.
channels = ["conda-forge", "bioconda"], platforms including osx-64 — bioconda is where
STAR/samtools/htslib live; osx-64 widens the lab-machine coverage. Both are additive.
- Git
pypi-dependencies for liulab-genome / liulab-data — kept out of
[project] dependencies because PyPI rejects git+https metadata, with the deliberate absence of
[tool.hatch.metadata] allow-direct-references as a tripwire. Well argued; the template has no
equivalent situation.
Upstream candidates
scripts/check.sh — the process-group cleanup. seqforge's runner sets set -muo pipefail, so
each step is its own process group, and its cleanup() kills every leftover group before it
removes the scratch directory, with trap 'exit 130' INT / trap 'exit 143' TERM turning the two
signals that reach an abandoned gate into an ordinary exit. The template's version has none of
this: Ctrl-C on a template gate kills the shell without running the EXIT trap, leaving live pytest
runs writing into a directory that is about to disappear. It also runs steps as
pixi run --no-progress and tails 3 lines of a green step instead of dumping it whole, and prints
elapsed seconds in the summary. The template should adopt all of this.
- The three gate-runner tests in
tests/test_repo_invariants.py. The template's check.sh
header records a real bash-3.2 bug — an associative array that failed on macOS, set -e absent so
the script ran on, and "the gate reported green on every macOS host" — and then tests nothing.
seqforge has test_the_gate_runner_stays_within_the_bash_macos_ships (a regex guard on declare -A / mapfile, which fires on a Linux runner too),
test_the_gate_exits_non_zero_and_says_which_step_failed, and
test_an_interrupted_gate_leaves_no_step_running (starts two lingering steps, sends SIGINT,
asserts no grandchild survives and the scratch directory is gone). The template should adopt
these; the bug they guard is one the template has already had.
scripts/require_binaries.sh and the "a skip is green" principle. A depends-on that proves
each external binary answers (--version, not which) before pytest selects, so a lane cannot
report green having run nothing. The template has no external-binary lane today, but the principle
and the shape are generic enough to belong in docs/agents/ or a template ADR.
.vscode/settings.json + .vscode/extensions.json. The template ships neither, and seqforge's
fixes a failure every pixi repo has: python-envs.workspaceSearchPaths pointed at
.pixi/envs/default because the extension searches [".venv", "*/.venv"] and "the interpreter
silently fell back to a system python 3.10 and every third-party import went unresolved while the
pixi env sitting in the tree had them all." defaultInterpreterPath alone does not fix it. The
template should ship the pixi-interpreter half (adjusting the checker keys to pyright).
.github/ISSUE-CONVENTIONS.md's wayfinding section. Beyond the template's
issue-tracker.md + triage-labels.md, it documents GitHub's native issue dependencies with the
exact gh api call and the trap that costs an hour — the blocker's numeric database id
(gh api repos/<o>/<r>/issues/<n> --jq .id), not the #number or node_id — plus the frontier
query and the fallback when dependencies are unavailable. The template's issue docs should absorb
this.
- Per-context
CONTEXT.md and colocated docs/adr/ under src/<pkg>/<context>/. The template's
docs/agents/domain.md opens "Single-context: one CONTEXT.md and one docs/adr/ at the repo
root" and offers nothing else. seqforge runs the multi-context shape — five per-context
glossaries routed from CONTEXT-MAP.md, records beside the code they govern, cited by number
never by path so a record can move, and deliberately shipped in the wheel
(pyproject.toml:70-76). The template should describe this as the second supported shape, with the
conformance/vale declarations that go with it.
twine check dist/* in the release build. Validates the rendered metadata the way PyPI will,
and here it doubles as the tripwire for a regressed git+https direct reference. Cheap; the
template's release build has no equivalent.
Notes for the maintainer
- G3 and G10 are the two that will produce work rather than a diff. G3's exact bill is measured
above: six agent docs and three records over cap. G10's D rules are the unknown — I did not run
ruff against the tree, so I cannot say how many docstrings are missing. Land the other select
codes first and treat D as its own issue.
- G8 vs.
.github/ISSUE-CONVENTIONS.md is a real choice, not a defect to fix mechanically.
seqforge's file is better than what it would replace, and it is where AGENTS.md points. Moving
it to docs/agents/ buys alignment and costs a good location; declaring it in
[tool.liulab.agent-docs] where it stands buys the caps without the move. My recommendation is
the second, plus a new docs/agents/writing.md.
- G14 touches
AGENTS.md, which is at 1705 words and already over the cap G3 introduces. Doing
the rename and the AGENTS.md trim in one commit is probably cheaper than two passes over the same
file.
- I could not determine whether the
docs feature belongs in the default environment.
ci.yml folds docs-build into the lint job and justifies it by measurement (1.7s versus ~28s for
another setup-pixi), which is sound for mkdocs-material. The template's separate docs job
exists because zensical's environment is heavy. If seqforge stays on material, leave it; revisit if
the site ever moves to zensical. Not filed as a gap.
- Nothing in this report was verified by running seqforge's own gate. The clone was read only —
no pixi install, no check, no ruff or mypy run. Every claim above is from file contents,
git ls-files, symlink inspection and wc -w.
seqforge — template sync gap report
Shape: published —
src/seqforge/+tests/+CHANGELOG.md+.github/workflows/release.ymlpublishing to PyPI by Trusted Publishing;
README.mddocumentspip install seqforge.Summary: seqforge is the most template-like repo in the lab and in several places it is ahead of
the template — its
scripts/check.shis a strict superset, and it has tests holding that runner toits own invariants. The drift is concentrated in the writing and conformance half of the
standard:
.vale.ini,styles/,scripts/conformance.pyand the[tool.liulab]declarations areall absent,
markdownlintis not a step ofcheck, and the tracked.claude/skills/symlinks aremissing because
.gitignorecarries the blanket/.claude/the template forbids. The single mostimportant fix is that last one: a fresh clone of seqforge gives Claude Code zero of the eight skills
this repo ships.
Gaps
G1. Stop ignoring
/.claude/and track the eight.claude/skills/symlinks.gitignore(last line) has a blanket/.claude/, added for agent worktrees..claude/does not exist in the clone at all;
git ls-filesshows the eight.agents/skills/*symlinks andno
.claude/skills/*..gitignorenames.claude/settings.local.jsonand.claude/worktrees/andnothing else, with the comment: "Never a blanket
/.claude/: the skill symlinks under.claude/skills/and.agents/skills/are TRACKED, becauseinit-repohas to be discoverablethe instant you clone — before anyone could run the thing that runs installers." Its
.claude/skills/init-repoand.claude/skills/template-devare mode-120000 tracked entries..gitignore, replace/.claude/with.claude/settings.local.jsonand.claude/worktrees/. Thenpython skills/install.py --target claudeand commit the eightresulting relative symlinks.
Code until somebody remembers to run the installer. The
.agents/skills/links are correct(relative, non-dangling, verified) — only the Claude path is lost, and it is lost by one line.
G2. Add
scripts/conformance.pyand the[tool.liulab]declarationsscripts/conformance.py, noconformancepixi task, and no[tool.liulab.agent-docs]or
[tool.liulab.waived]table inpyproject.toml. Running the template's checker against theclone reports rules 2 and 4 as "not checked" purely because the declarations are absent.
scripts/conformance.py(8 rules + 2 warnings), invoked by theconformancetaskand named in
check-static; declarations atpyproject.toml:166-179.scripts/conformance.pyverbatim (it is written to be shared), addconformance = "python scripts/conformance.py"to[tool.pixi.tasks], addpyyamlto thedefault environment's dependencies (it is already a runtime dep, so this is free), and declare
[tool.liulab.agent-docs]with seqforge's real paths —AGENTS.md,CONTEXT.md,docs/adr/,skills/,docs/research/ = false.failure rule 7 exists to catch, and it has been live on
mainundetected.G3. Adopt
.vale.iniandstyles/Lab/valetask and no CI job runs one. The lab's writingrules are entirely unenforced in this repo.
.vale.ini(four sections, every rule named in every section) and four trackedrules under
styles/Lab/:Jargon.yml,LengthDoc.yml(1000 words),LengthAdr.yml(400),Readability.yml..vale.iniandstyles/Lab/, addvale = "3.19.0.*"to the defaultenvironment and
vale = "vale --glob='!{.pixi,site,dist,build}/**' ."to[tool.pixi.tasks].Confirm
styles/is not gitignored (it currently is not).Six agent-facing files exceed the 1000-word
LengthDoccap —skills/seqforge-kb-author/SKILL.md(1708),
AGENTS.md(1705),src/seqforge/compose/CONTEXT.md(1612),skills/seqforge-orchestrate/SKILL.md(1392),skills/seqforge-manifest/SKILL.md(1324),src/seqforge/resolve/CONTEXT.md(1033) — and three records exceed the 400-wordLengthAdrcap(
0053-a-mate-that-did-not-align...757,0049-stars-command-line...746,0051-a-composed-pipeline-runs-on-one-machine555).CONTEXT-MAP.mdat 957 already passes.G4. Port
--checkintoskills/install.pyskills/install.pyis byte-for-byte the template's file minus the check half.discover,TARGETSandinstall_oneare identical, including comments. Missing: theCHECKED_TARGETSconstant,
_problem(),check(),report_check(), the--checkargparse flag and the earlyif args.check:return.--checktherefore exits 2 withinstall.py: error: unrecognized arguments: --check.skills/install.pylines 43-46 (CHECKED_TARGETS), 88-205 (_problem,check,report_check), 226-230 (the flag) and 234-239 (the early return); the parser also usesdescription=__doc__withRawDescriptionHelpFormatterrather thandescription=__doc__.splitlines()[0].__doc__+RawDescriptionHelpFormatter, and add the--checkparagraph to the module docstring. Keepseqforge's trailing
seqforge hook installline inmain(). Nothing else in the file changes.--checkis what conformance rule 7 calls, and it is the check that wouldhave caught G1 — it holds
.claude/skillsand.agents/skillsto three invariants (a link inboth, nothing dangling, every link relative) and prints the repairing command on each problem.
G5. Make
markdownlinta step of the gate, and delete the second exclusion listmarkdownlintis in no pixi task. It runs in two other places: as a pre-commit hook(
.pre-commit-config.yaml,markdownlint-cli2 rev v0.23.1) and as its own CI job(
.github/workflows/ci.yml,npx --yes markdownlint-cli2@0.23.1on a Node runner)..markdownlint-cli2.yamlhas narrowglobs:(docs/**/*.md, the KB specs,README.md) plus aneleven-entry
ignores:list and nogitignore: true.markdownlint = "markdownlint-cli2"is a task and acheck-staticstep, withmarkdownlint-cli2installed from conda-forge..markdownlint-cli2.yamlisgitignore: true+globs: ["**/*.md"]+config: {default: true, MD013: false}, with "deliberately noignores:list. It used to exist only to mirror a second list of excluded paths kept elsewhere, and two
lists that must agree are a drift waiting to happen."
markdownlint-cli2to the default environment and amarkdownlinttask; add itto the
checkline in[tool.pixi.tasks]. Then setgitignore: true, widenglobsto["**/*.md"], and dropignores:— replacing themkdocs.ymlexclude_docs:mirror withper-file
<!-- markdownlint-disable -->where a KB wrapper genuinely has nothing to lint.ignores:list below andmkdocs.yml'sexclude_docs:are the SAME list… they drifted oncealready, turning CI red on every open PR." Two pins of the same tool (npx 0.23.1, pre-commit
0.23.1) plus a third toolchain (Node) to lint markdown is the cost of not having it in pixi.
G6. Move the version to hatch-vcs
pyproject.toml:24hasversion = "2026.7.1"hand-written,[build-system] requiresis
["hatchling"]only, and there is no[tool.hatch.version].release.ymlcarries a wholeextra step ("Check tag matches pyproject version") that reads the version back out with
tomlliband compares it to
GITHUB_REF_NAME.CHANGELOG.mdsays the version "tracks[project].versionin
pyproject.toml".dynamic = ["version"],requires = ["hatchling", "hatch-vcs"],[tool.hatch.version] source = "vcs", with "Nofallback-version: that is a hand-writtenversion string."
hatch-vcstorequires, replace theversionkey withdynamic = ["version"],add
[tool.hatch.version] source = "vcs", delete the tag-check step fromrelease.yml, and makesure the
buildjob keepsfetch-depth: 0.The template deletes the second copy instead of guarding it, and the tag stays the single source.
G7. Build the released wheel with pixi, not a separate pip toolchain
release.yml'sbuildjob usesactions/setup-python@v5withpython-version: "3.12"and
python -m pip install --upgrade build twine, thenpython -m build.ci.yml'sbuildjobuses
pixi run -e default build. The wheel that ships is therefore built by a toolchain no pullrequest ever exercises. Neither release job uses
fetch-depth: 0.release.yml's build job issetup-pixiwithlocked: trueandpixi run build—"The same task the
buildjob inci.ymlruns, so what is published was built the way everypull request already proved."
setup-python+ pip forprefix-dev/setup-pixiwithlocked: trueandpixi run build; addfetch-depth: 0to the checkout (required once G6 lands). Keep thetwine checkstep — see Upstream candidates.whose output reaches users.
G8. Add
docs/agents/, starting withwriting.mddocs/agents/directory..github/ISSUE-CONVENTIONS.mdcovers what the templatesplits across
issue-tracker.mdandtriage-labels.md(and covers it better — see Upstreamcandidates). Nothing covers
writing.mdordomain.md.docs/agents/domain.md,issue-tracker.md,triage-labels.md,writing.md, eachwith
search: exclude: truefront matter and none of them innav:.docs/agents/writing.md(the three rules and the cap table) at minimum; eithermove
.github/ISSUE-CONVENTIONS.mdtodocs/agents/issue-tracker.md+triage-labels.mdordeclare
.github/ISSUE-CONVENTIONS.mdunder[tool.liulab.agent-docs]and leave it where it is.writing.mdis where the word caps and the reason for them are written. G3installs the enforcement; without this document a contributor meets a red vale run with no
explanation of what 1000 means or where the dial lives.
G9. Declare the Python version in two places, not five
requires-python = ">=3.12"(line 9),classifiers listing both 3.12 and 3.13 (lines 18-19),
[tool.pixi.feature.py313.dependencies] python = "3.13.*"(line 172),[tool.ruff] target-version = "py312"(line 299),[tool.mypy] python_version = "3.12"(line 315), pluspython-version: "3.12"inrelease.yml.requires-pythonand[tool.pixi.dependencies] python, with "Notarget-version: ruff infers it fromrequires-python" and "NopythonVersion: taken from theinterpreter in the environment above."
[tool.ruff] target-version(ruff infers it). Keep[tool.mypy] python_version— mypy defaults to the running interpreter (3.13) and this repo genuinelysupports 3.12, so this one is load-bearing; add a one-line comment saying so.
release.yml's pindisappears with G7.
correct conservative choice for a package declaring
>=3.12, but it is three numbers that mustmove together and only one of them has to.
G10. Widen the ruff rule set
[tool.ruff.lint] select = ["E", "W", "F", "I", "UP", "B"],ignore = ["E501", "UP046", "UP047"]. No[tool.ruff.lint.pydocstyle], no per-file-ignores.[tool.ruff] src = ["src", "tests"]—scriptsis absent thoughscripts/holds three files.select = ["E","W","F","I","UP","B","C4","SIM","PT","PTH","N","D","RUF"],ignore = ["E501","D203","D213"],convention = "numpy","tests/**" = ["D"],src = ["src", "tests", "scripts"].C4,SIM,PT,PTH,N,RUFfirst (cheap, mostly autofixable); addDwith the numpy convention and the
tests/**exemption as a follow-up, since it will flag a lot ona package this size. Add
"scripts"tosrc. KeepUP046/UP047ignored — the pydantic-v2reason given at line 306 is sound.
Dis the one that costs something today: seqforge ships to PyPI and has nodocstring gate at all, while mkdocstrings-style API rendering (G16) depends on docstrings existing.
G11. Replace the two pre-commit tool copies with the pixi ones
.pre-commit-config.yamlinstallsruff-pre-commit v0.15.16andmarkdownlint-cli2 v0.23.1into pre-commit's own environments. pixi separately declaresruff = "*"in thedevfeature. mypy is already routed through pixi(
entry: pixi run --no-progress typecheck), and pytest is deliberately excluded..pre-commit-config.yaml: "the gate ispixi run check… pre-commit is asecond toolchain installing its own pinned copies of ruff outside pixi."
ruff-check/ruff-format/markdownlintthrough pixi the way the mypy hook already is (
entry: pixi run --no-progress lint, etc.). Keepthe generic
pre-commit-hooksandshellcheckentries either way — those install nothing pixialso owns.
v0.15.16in pre-commit and floatingruff = "*"in pixi willeventually disagree, and the disagreement surfaces as a commit that pre-commit formatted one way
and CI fails the other. The mypy hook already shows the fix.
G12. Keep the gate's step list in one place, and let CI use it
check = "bash scripts/check.sh lint fmt-check typecheck test test-corpus test-external"— one task, no
check-static.ci.ymlnever callscheck.sh; itslintjob spellslint,fmt-check,typecheckanddocs-buildas four separate steps, andmarkdownlintlives in afifth job with its own toolchain.
check-static = "bash scripts/check.sh lint fmt-check typecheck vale markdownlint conformance"andcheck = { depends-on = ["check-static", "test"] };ci.yml'scheckjob is asingle
pixi run check-static, "so this file and a laptop cannot drift: adding a step is one moreword there and nothing here."
checkintocheck-static(lint, fmt-check, typecheck, vale, markdownlint,conformance) and
check = { depends-on = ["check-static", "test", "test-corpus"] }; makeci.yml's lint jobpixi run check-static.check.shheader documents this drift happening — "the prosehere used to claim the gate ran CI's selections outright, which drifted the moment those jobs were
added and then read as a bug." The template's fix is structural rather than a comment.
G13. Add
search: exclude: truefront matter todocs/adr/anddocs/research/docs/adr/ordocs/research/carries front matter (checkeddocs/adr/0002-*.md,docs/adr/README.md,docs/research/star-on-a-chimera.md). Exclusionrelies entirely on
mkdocs.yml'sexclude_docs:block plus the explicitnav:.docs/agents/,docs/adr/anddocs/research/opens with---\nsearch:\n exclude: true\n---, and "exclude_docs:is silently ignored [by zensical], soboth are needed."
docs/adr/,docs/research/andsrc/seqforge/*/docs/adr/.exclude_docs:, so this is not a liveleak. It is cheap insurance against a future zensical move, and it is what conformance rule 2
checks once G2 lands.
G14. Reconcile
CONTEXT-MAP.mdwith the template'sCONTEXT.mdCONTEXT-MAP.mdat the root (957 words) is a router to five per-context glossaries undersrc/seqforge/*/CONTEXT.md.AGENTS.mdlinks it by that name.CONTEXT.mdat the root, declared under[tool.liulab.agent-docs]and capped bothby
Lab.LengthDocand by conformance rule 4's per-entry 200-word limit. The spec callsCONTEXT-MAP.md"an older name for this [that] should be reconciled".CONTEXT-MAP.mdtoCONTEXT.md(updatingthe two links in
AGENTS.md) and declare"CONTEXT.md" = "LengthDoc"plus"src/seqforge/" = "LengthDoc"— or leave the name and declare"CONTEXT-MAP.md". Do notreshape the multi-context layout itself; see Upstream candidates.
two files has to move or the declaration has to name the local spelling.
G15. Refresh the GitHub Action pins
ci.yml,docs.yml,release.yml,benchmark.yml:actions/checkout@v5,prefix-dev/setup-pixi@v0.10.0,actions/upload-artifact@v4,actions/download-artifact@v4.locked: trueis set on everysetup-pixi— that part is already right.actions/checkout@v7,prefix-dev/setup-pixi@v0.10.2,actions/upload-artifact@v7,actions/download-artifact@v8,peaceiris/actions-gh-pages@v4,pypa/gh-action-pypi-publish@release/v1.future bump one sweep rather than five investigations.
G16. Decide on an API reference page
mkdocs.ymlhasplugins: [search]only — no mkdocstrings, nodocs/api.md, and nomkdocstrings-pythondependency. The site documents the CLI and the concepts.mkdocstringswith the python handler,paths: [src], numpydocstring style, and
docs/api.mdinnav:.docs/api.md(pairs naturally with theDrules in G10),or record the decision — a compiler whose interface is the CLI (ADR-0013) may legitimately not
want a rendered Python API.
the answer is written down rather than implied.
Justified divergences
benchmark.yml— the template ships no benchmark workflow and asks a repo that has one to saywhy. seqforge says why at length in the file header, and the reasoning holds: it is
release+workflow_dispatchonly (never push/PR/schedule), runs--no-llmso no API key or provider secretis ever exposed to it, keeps
permissions: contents: read, and an unreachable package skips ratherthan fails. This is the networked tier of a validation corpus, which is a claim per-commit CI
cannot make. No change wanted, and the template need not learn from it — most repos have no corpus.
--strictinstead of pyrightstandard— the template's stated goal is one verdict fromeditor and CI, and seqforge achieves it the other way round:
[tool.pyright] typeCheckingMode = "off"inpyproject.toml(so any pyright-based server honours it, not just VS Code) plusms-python.mypy-type-checkerpointed at the pixi interpreter.[tool.mypy] filesowns the scopeso the
typechecktask is bare. Strictly stronger than the template's setting; leave it.test-starenvironment — the unit / hermetic-corpus / externalpartition,
no-default-feature+ a separate solve group for the aligner, and the divided--maxprocessescaps are all measured decisions documented inpyproject.tomlandci.yml. Thetemplate's single
testtask is right for a repo with no external binaries; this is right here.exclude_docs:inmkdocs.yml— the site predatesADR-0001 and leans on material-specific features (pymdownx.snippets with
check_paths, mermaidfences, the palette, the shared GA4 property).
exclude_docs:is only silently ignored byzensical; real mkdocs honours it, so the exclusion works today. Revisit if the site ever moves.
channels = ["conda-forge", "bioconda"],platformsincludingosx-64— bioconda is whereSTAR/samtools/htslib live;
osx-64widens the lab-machine coverage. Both are additive.pypi-dependenciesforliulab-genome/liulab-data— kept out of[project] dependenciesbecause PyPI rejectsgit+httpsmetadata, with the deliberate absence of[tool.hatch.metadata] allow-direct-referencesas a tripwire. Well argued; the template has noequivalent situation.
Upstream candidates
scripts/check.sh— the process-group cleanup. seqforge's runner setsset -muo pipefail, soeach step is its own process group, and its
cleanup()kills every leftover group before itremoves the scratch directory, with
trap 'exit 130' INT/trap 'exit 143' TERMturning the twosignals that reach an abandoned gate into an ordinary exit. The template's version has none of
this: Ctrl-C on a template gate kills the shell without running the EXIT trap, leaving live pytest
runs writing into a directory that is about to disappear. It also runs steps as
pixi run --no-progressand tails 3 lines of a green step instead of dumping it whole, and printselapsed seconds in the summary. The template should adopt all of this.
tests/test_repo_invariants.py. The template'scheck.shheader records a real bash-3.2 bug — an associative array that failed on macOS,
set -eabsent sothe script ran on, and "the gate reported green on every macOS host" — and then tests nothing.
seqforge has
test_the_gate_runner_stays_within_the_bash_macos_ships(a regex guard ondeclare -A/mapfile, which fires on a Linux runner too),test_the_gate_exits_non_zero_and_says_which_step_failed, andtest_an_interrupted_gate_leaves_no_step_running(starts two lingering steps, sends SIGINT,asserts no grandchild survives and the scratch directory is gone). The template should adopt
these; the bug they guard is one the template has already had.
scripts/require_binaries.shand the "a skip is green" principle. Adepends-onthat proveseach external binary answers (
--version, notwhich) before pytest selects, so a lane cannotreport green having run nothing. The template has no external-binary lane today, but the principle
and the shape are generic enough to belong in
docs/agents/or a template ADR..vscode/settings.json+.vscode/extensions.json. The template ships neither, and seqforge'sfixes a failure every pixi repo has:
python-envs.workspaceSearchPathspointed at.pixi/envs/defaultbecause the extension searches[".venv", "*/.venv"]and "the interpretersilently fell back to a system python 3.10 and every third-party import went unresolved while the
pixi env sitting in the tree had them all."
defaultInterpreterPathalone does not fix it. Thetemplate should ship the pixi-interpreter half (adjusting the checker keys to pyright).
.github/ISSUE-CONVENTIONS.md's wayfinding section. Beyond the template'sissue-tracker.md+triage-labels.md, it documents GitHub's native issue dependencies with theexact
gh apicall and the trap that costs an hour — the blocker's numeric database id(
gh api repos/<o>/<r>/issues/<n> --jq .id), not the#numberornode_id— plus the frontierquery and the fallback when dependencies are unavailable. The template's issue docs should absorb
this.
CONTEXT.mdand colocateddocs/adr/undersrc/<pkg>/<context>/. The template'sdocs/agents/domain.mdopens "Single-context: oneCONTEXT.mdand onedocs/adr/at the reporoot" and offers nothing else. seqforge runs the multi-context shape — five per-context
glossaries routed from
CONTEXT-MAP.md, records beside the code they govern, cited by numbernever by path so a record can move, and deliberately shipped in the wheel
(
pyproject.toml:70-76). The template should describe this as the second supported shape, with theconformance/vale declarations that go with it.
twine check dist/*in the release build. Validates the rendered metadata the way PyPI will,and here it doubles as the tripwire for a regressed
git+httpsdirect reference. Cheap; thetemplate's release build has no equivalent.
Notes for the maintainer
above: six agent docs and three records over cap. G10's
Drules are the unknown — I did not runruff against the tree, so I cannot say how many docstrings are missing. Land the other
selectcodes first and treat
Das its own issue..github/ISSUE-CONVENTIONS.mdis a real choice, not a defect to fix mechanically.seqforge's file is better than what it would replace, and it is where
AGENTS.mdpoints. Movingit to
docs/agents/buys alignment and costs a good location; declaring it in[tool.liulab.agent-docs]where it stands buys the caps without the move. My recommendation isthe second, plus a new
docs/agents/writing.md.AGENTS.md, which is at 1705 words and already over the cap G3 introduces. Doingthe rename and the AGENTS.md trim in one commit is probably cheaper than two passes over the same
file.
docsfeature belongs in thedefaultenvironment.ci.ymlfoldsdocs-buildinto the lint job and justifies it by measurement (1.7s versus ~28s foranother
setup-pixi), which is sound for mkdocs-material. The template's separatedocsjobexists because zensical's environment is heavy. If seqforge stays on material, leave it; revisit if
the site ever moves to zensical. Not filed as a gap.
no
pixi install, nocheck, no ruff or mypy run. Every claim above is from file contents,git ls-files, symlink inspection andwc -w.