Janus, the Roman god of gates, has two faces looking in opposite directions — and so does this pipeline. One face looks downstream, where an OpenShift consultant works: the exact RHEL/OCP builds customers run, their crashes, upgrades, and CVEs. The other looks upstream, where the fix belongs: the kernel, Kubernetes, and KubeVirt communities. Dig deep enough into Linux and the two views meet at the same root cause.
An OpenShift / RHEL / CNV research & investigation pipeline for Claude Code — case types range from kernel crash forensics to upgrade/compatibility analysis, CVE impact assessment, and operator/component behavior investigation. JANUS uses Claude Code itself as the orchestrator: the lead session composes a pipeline of small agent stages per case type, gates the dynamic ones, and hands a ranked-hypothesis report to a human. It lays evidence and hypotheses on the table — the human makes the final call.
Every case feeds both faces: synthesize hands the consultant a
ranked-hypothesis report grounded in the exact builds involved, and when
the root cause turns out to be everyone's problem, upstream-adviser
drafts the contribution proposal to carry it home.
Invoke /janus with a question or an artifact. The lead classifies the
case, shows you the pipeline it intends to run, fans the stages out on
approval, and hands you a ranked-hypothesis report at
cases/<id>/results/report.md.
CVE impact assessment (needs okp-mcp):
/janus Does CVE-2024-1086 affect OpenShift 4.16 worker nodes?
→ { doc-search, source-trace } | synthesize — errata/KB sweep plus the
actual code path, cross-referenced into ranked hypotheses. A
well-supported "not affected, and here is why" is a valid outcome.
Kernel crash forensics (needs drgn):
/janus Analyze the vmcore under cases/2026-07-11-node-panic/artifacts/,
kernel 5.14.0-570.el9. The node panicked during a VM live migration.
→ adds crash-analyze: drgn triage (crashed thread, dmesg, task states),
then up to 5 observe → hypothesize → probe rounds. Every probe and its
output lands in cases/<id>/audit/ — the report's claims point at them.
Upgrade / cross-version compatibility:
/janus What changed between OCP 4.18 and 4.20 that could break VMs
using SCSI-3 persistent reservations over multipath?
→ version-diff investigation across layers (kernel, RHEL userspace,
CNV). When a stage surfaces a KubeVirt PR or an RHEL-NNNNN ticket it
cannot open, the lead launches github-trace / jira-trace follow-ups
at fan-in. For ARO cases, mslearn covers the Azure layer; for ROSA cases,
the AWS MCP servers cover the AWS layer.
Japanese prompts work the same way — the skill triggers on phrases like 「vmcoreを解析」「OOM調査」「アップグレード互換性を調査」「CVEの影響評価」.
Reproducible lab as Infrastructure-as-Code (needs terraform / ansible):
/janus Build the ARO 4.16 lab needed to reproduce this multipath
hypothesis — Terraform + Ansible, ready for review.
→ adds iac-author: it looks every provider argument and version pin up
in the Terraform registry rather than recalling it, checks the sharp-edged
values before writing them (instance-type allowlist, GPU AZ availability,
node disk ≥ 3× model size, image tags that actually exist), and writes
fmt-ed, validate-d, ansible-lint-clean code into cases/<id>/iac/
with documented variables and no credentials. Anything it could not
confirm is left un-defaulted with a TODO(iac-author) at the exact line —
a hole you can see beats a plausible guess that survives review.
Every finding in the report carries Confidence + Basis
(VERIFIED / REASONED / ASSUMED) + a reference a human can open — a
CVE/errata URL, a source permalink, or a drgn audit log. Live-cluster
verification (lab-verify) is only ever proposed: it runs on a
disposable lab, and only after you approve
review-queue/APPROVE_<id>.md.
Authoring that IaC and applying it are deliberately different stages.
iac-author is static and autonomous, because writing a .tf changes no
infrastructure; lab-verify is the only stage that runs it, behind the
approval gate, as explicit shell commands recorded in cases/<id>/audit/.
The ansible MCP's executing tools (ansible_navigator,
ade_setup_environment) are granted to no agent, and the terraform
grants are enumerated rather than wildcarded so that enabling that
server's enterprise tools can never quietly hand an agent apply_run.
scripts/validate.py fails the build if either rule is broken.
How a case flows through the team — the lead session orchestrates, small agent stages investigate in parallel, and the human holds every gate that matters:
flowchart TD
human(["Human"]) -->|"question / artifact"| intake
intake["1 · Intake — classify the case, write case.yaml"] --> fanout
fanout["2 · Fan-out — launch stages in parallel"]
subgraph stages["Static stages — each writes cases/<id>/findings/<stage>.md"]
ds["doc-search<br/>okp · mslearn · aws"]
st["source-trace<br/>casket (optional)"]
ca["crash-analyze<br/>drgn · gdb"]
ia["iac-author<br/>terraform · ansible<br/>writes IaC, runs nothing"]
end
lv["lab-verify — DYNAMIC<br/>disposable lab, never production"]
fanout --> ds & st & ca & ia
ia -->|"cases/<id>/iac/"| lv
fanout -.->|"only after the human approves<br/>APPROVE_<id>.md"| lv
ds & st & ca & ia --> fanin
lv -.-> fanin
fanin["3 · Fan-in — read findings, spot gaps"]
follow["github-trace · jira-trace<br/>(conditional, read-only)"]
fanin -.->|"a stage surfaced a<br/>PR / Jira key"| follow
follow -.->|"findings/<stage>.md"| fanin
fanin --> syn["synthesize — cross-reference all findings"]
syn --> report[("results/report.md<br/>ranked hypotheses · Confidence + Basis + refs")]
report --> qc["4 · Quality check<br/>chain verify · urlcheck · quotecheck · versioncheck · linkcheck · prosecheck (ja) · two judgment gates (C1/C2)"]
qc -.->|"send-back, by sub-code"| syn
qc -->|"handoff"| human2(["Human — final call, writes verdict.md"])
And how JANUS improves itself — two periodic agents sit outside the pipeline and feed a human-gated review queue:
flowchart LR
case["Completed case"] -->|"new failure pattern,<br/>human-approved"| lessons
lessons[("janus-lessons<br/>project-local staging")] -->|"injected into<br/>stage briefs"| next["Next case"]
verdicts[("verdict.md × 10<br/>or weekly")] --> si["self-improver<br/>metrics: hit rate, escalation<br/>precision/recall, calibration"]
lessons -->|"recurs across ≥2 cases"| si
report[("results/report.md<br/>high-confidence upstream defect")] --> ua["upstream-adviser<br/>advisory only — never<br/>opens issues/PRs itself"]
si -->|"IMPROVE_<date>.md"| rq[("review-queue/")]
ua -->|"contribution drafts"| rq
rq -->|"human approves"| plugin["Plugin agent catalogs<br/>failure & reusable patterns,<br/>process changes"]
The pipeline has nine composable stages connected by a universal
findings/*.md format, plus two periodic agents:
{ doc-search, source-trace, crash-analyze, iac-author | [approve] lab-verify }
| synthesize [| localize]
github-traceandjira-tracejoin conditionally when another stage surfaces an upstream PR/issue or a Jira ticket.localizeruns only whenreport_language≠en.- Reusable investigation patterns (drgn triage, CVE tracing, refuting an a-priori hypothesis, goroutine-leak repro, etc.) are inlined into each agent so they travel with the plugin.
| Requirement | Purpose | Required? |
|---|---|---|
| Claude Code | Plugin host (CLI, desktop, or web) | Yes |
| Python 3.9+ | validate.py, selftest.py, integrity scripts (chain.py, urlcheck.py, etc.) |
Yes |
| Bash 4+ | Shell-based tests and hook scripts | Yes |
None of the MCP servers are bundled — paths are machine-specific. Register each one before running an investigation (see MCP dependencies below for setup commands).
| Server | Used by | Required? |
|---|---|---|
| okp-mcp | doc-search (CVE / errata / KB) | Recommended |
| casket-mcp | source-trace (versioned source) | Optional |
| drgn-mcp | crash-analyze (vmcore) | For crash cases |
| GitHub MCP | github-trace, upstream-adviser | Optional |
| mcp-atlassian | jira-trace | Optional |
| mslearn | doc-search (ARO/Azure layer) | For ARO cases |
| aws-knowledge | doc-search (ROSA/AWS layer) | For ROSA cases |
| terraform-mcp-server | iac-author | For lab IaC |
| ansible-dev-tools | iac-author | For lab IaC |
| linux-mcp-server | lab-verify | For live verification |
| slack-mcp | doc-search (team context) | Optional |
| Tool | Purpose |
|---|---|
| textlint + ja-technical-writing | Japanese report prose check (gate C2/prose) |
| ax | Token-aware web fetching |
| mdq | Markdown querying (jq for Markdown) |
This repo is itself a Claude Code plugin marketplace
(.claude-plugin/marketplace.json). Inside a Claude Code session:
/plugin marketplace add nogunix/janus # register straight from GitHub
/plugin install janus@janus # install the plugin (plugin@marketplace)
Or from a local clone (useful when editing the plugin):
git clone https://github.com/nogunix/janus.git ~/janus/plugin marketplace add ~/janus # register the local marketplace
/plugin install janus@janus # install the plugin (plugin@marketplace)
When editing the plugin, run python3 scripts/validate.py before
committing — it checks manifest schema, skill/agent frontmatter,
hook-script paths, and SKILL.md ↔ agents/ stage sync.
Submitted to Anthropic's community marketplace for review; once approved
it will also be installable via /plugin marketplace add anthropics/claude-plugins-community → janus@claude-community. Until
then, use the direct-from-GitHub install above — it already tracks the
latest release.
Restart Claude Code so the skills and agents load, then verify:
/plugin—janusshows as installed and enabled/janusappears in the skill list; the tenjanus:*agents appear in the Agent tool list
Day-to-day maintenance:
/plugin marketplace update janus # re-read the local clone after edits
/plugin uninstall janus@janus # remove the plugin
/plugin marketplace remove janus # remove the marketplace entry
.claude-plugin/marketplace.json # marketplace listing → plugins/janus
plugins/janus/
.claude-plugin/plugin.json # plugin manifest
skills/janus/SKILL.md # /janus — pipeline driver
skills/janus/scripts/chain.py # per-case evidence hash ledger (seal/verify/lock)
skills/janus/scripts/urlcheck.py # reference-URL liveness check (backs gate C1/url)
skills/janus/scripts/quotecheck.py # verbatim-quote fidelity check (backs gate C2/quote)
skills/janus/scripts/versioncheck.py # version-provenance check (backs gate C2/version)
skills/janus/scripts/linkcheck.py # evidence links resolve to a real file/anchor (backs C1/link)
skills/janus/scripts/prosecheck.py # ja report prose via textlint (backs gate C2/prose)
skills/janus/scripts/textlintrc.json # ja-technical-writing config for the above
skills/janus/scripts/anchors.py # findings heading → GitHub slug map (evidence links)
skills/deck/ # report → branded .pptx/PDF
skills/gslides/ # report → Google Slides via the gws CLI
skills/md2pdf/ # Markdown → PDF (pandoc + weasyprint, CJK)
skills/okp-doc-search/ # okp-mcp research know-how (queries, doc_id rules)
skills/ocp-triage-heuristics/ # experiential OCP live-cluster triage reference
hooks/ # secret-safety + evidence-lock (PreToolUse denies) + evidence-chain (PostToolUse auto-seal)
agents/ # 11 agents (patterns inlined into each)
doc-search source-trace github-trace jira-trace crash-analyze
iac-author lab-verify synthesize localize self-improver
upstream-adviser
scripts/validate.py # repo consistency checks (CI-friendly, stdlib-only)
scripts/selftest.py # offline self-tests for chain.py / urlcheck.py / quotecheck.py / versioncheck.py / linkcheck.py / prosecheck.py / hooks
.github/workflows/ci.yml # runs both on every push / PR
The plugin does not bundle MCP config — server paths are machine-specific.
Register each server yourself (claude mcp add …) before running an
investigation, then confirm claude mcp list shows ✔ Connected — a tool
being advertised isn't the same as the server being reachable.
Bridges to the official Offline Knowledge Portal (OKP) Solr index.
Requires a Red Hat account (registry.redhat.io access + an OKP access key
from https://access.redhat.com/offline/access/). The bridge server itself
is public OSS: https://github.com/rhel-lightspeed/okp-mcp.
podman login registry.redhat.io # needs a Red Hat account
# build the okp-mcp bridge image per github.com/rhel-lightspeed/okp-mcp
podman play kube okp-pod.yaml # manifest below
claude mcp add --transport http okp-mcp http://localhost:8000/mcp --scope userokp-pod.yaml — Solr + bridge in one pod:
apiVersion: v1
kind: Pod
metadata:
name: okp-mcp
spec:
containers:
- name: redhat-okp
image: registry.redhat.io/offline-knowledge-portal/rhokp-rhel9:latest
ports:
- containerPort: 8983
hostPort: 8983
env:
- name: ACCESS_KEY
value: "<your-okp-access-key>"
- name: SOLR_JETTY_HOST
value: "0.0.0.0"
volumeMounts:
- name: redhat-okp-data
mountPath: /opt/solr/server/solr/portal/data
- name: okp-mcp
image: localhost/okp-mcp:latest
ports:
- containerPort: 8000
hostPort: 8000
env:
- name: MCP_SOLR_URL
value: "http://localhost:8983"
volumes:
- name: redhat-okp-data
persistentVolumeClaim:
claimName: redhat-okp-data
restartPolicy: AlwaysUsed by source-trace to search and read the exact source behind a specific OCP release — component git trees, operator bundles, SRPMs, and layered-product operands. Optional: without it, source-trace is skipped and the report notes a gap.
casket-mcp is part of ocp-source-collector. Full setup (venv, registration, LAN access): mcp/README.md.
# Local (stdio) — run from the ocp-source-collector checkout
claude mcp add casket -- "$CASKET_WORK"/mcp/.venv/bin/python \
"$CASKET_WORK"/mcp/casket_mcp.py
# Or from another machine on the LAN (HTTP, casket-host runs the server)
claude mcp add --transport http casket http://<CASKET_HOST_IP>:8765/mcpPublic remote server, no auth, used by doc-search. Official server docs: https://github.com/MicrosoftDocs/mcp.
claude mcp add --transport http mslearn https://learn.microsoft.com/api/mcpThe AWS mirror of mslearn, used by doc-search for ROSA (Red Hat OpenShift Service on AWS) and the AWS services beneath it. All optional, from awslabs/mcp; doc-search uses whichever are connected and skips the rest.
- aws-knowledge — hosted, read-only, no auth; cross-searches AWS docs /
blogs / What's New / API references:
claude mcp add --transport http aws-knowledge https://knowledge-mcp.global.api.aws
- aws-docs — read-only, no credentials; runs via
uvx:claude mcp add aws-docs -- uvx awslabs.aws-documentation-mcp-server@latest
- aws-support — needs AWS credentials + a Business/Enterprise support
plan. Only its read-only
describe_*tools are granted to doc-search (the case create / reply / resolve tools are deliberately withheld — JANUS never mutates a support case):claude mcp add aws-support --env AWS_PROFILE=<profile> --env AWS_REGION=us-east-1 \ -- uvx awslabs.aws-support-mcp-server@latest
doc-search can supplement official docs with your team's Slack
discussions. It calls the tools of
redhat-community-ai-tools/slack-mcp
(search_messages, get_thread, get_channel_history,
list_joined_channels, …), which runs locally via Podman/Docker
(quay.io/redhat-ai-tools/slack-mcp) or its one-shot setup script.
Register it with SLACK_MCP_READ_ONLY=1 so state-mutating tools
(post/react/join) error out — JANUS only reads. Point it at a workspace
you are authorized to search. Slack hits are supplementary evidence
only — findings attribute them as [slack] #channel, YYYY-MM-DD and
never rest a conclusion on them alone. Without it, doc-search simply
skips the Slack angle.
Used by the conditional jira-trace stage when another stage surfaces a
Jira ticket key (e.g. Red Hat Jira RHEL-NNNNN / OCPBUGS-NNNNN).
Public OSS: https://github.com/sooperset/mcp-atlassian. Register it
with READ_ONLY_MODE=true — that disables every write tool at server
level and is the safety boundary that keeps jira-trace observation-only
(JANUS never creates, edits, comments on, or transitions tickets):
claude mcp add mcp-atlassian -s user \
--env JIRA_URL=https://issues.redhat.com \
--env JIRA_PERSONAL_TOKEN=<your-PAT> \
--env READ_ONLY_MODE=true \
-- uvx mcp-atlassianFor Jira Cloud, use JIRA_USERNAME + JIRA_API_TOKEN instead of the
personal access token. Without this server, jira-trace is skipped and
ticket references stay in the report as gaps.
Public OSS: https://github.com/walac/drgn-mcp, an MCP wrapper around the drgn programmable kernel debugger. Run it sandboxed — network-cut, read-only, unprivileged — since upstream has no built-in sandboxing:
git clone https://github.com/walac/drgn-mcp.git
cd drgn-mcp && python3 -m venv .venv && .venv/bin/pip install -e .
claude mcp add drgn -s user -- "$(pwd)/.venv/bin/python" -m drgn_mcp.serverOfficial hosted read-only endpoint of
github/github-mcp-server
— no server to run yourself. The /readonly path serves only read
tools, which is the safety boundary here. Used by github-trace and
upstream-adviser:
claude mcp add --transport http github https://api.githubcopilot.com/mcp/readonlyPublic OSS: https://github.com/rhel-lightspeed/linux-mcp-server,
local or over SSH. Used by lab-verify. Register with LINUX_MCP_TOOLSET=fixed (lowercase) so the
arbitrary-script run_script toolset stays disabled — that's the safety
boundary that keeps lab-verify read-only:
pip install git+https://github.com/rhel-lightspeed/linux-mcp-server.git
claude mcp add linux -s user --env LINUX_MCP_TOOLSET=fixed -- linux-mcp-serverHashiCorp's official terraform-mcp-server, used by iac-author to write HCL against the current registry schema instead of from memory. Its default tool set is read-only registry lookup:
podman run -d -p 8090:8080 --name terraform-mcp \
hashicorp/terraform-mcp-server:latest
claude mcp add --transport http terraform http://localhost:8090/mcpOnly the enumerated registry tools are granted. Enabling the server's
enterprise tools with a Terraform token adds create_run / apply_run,
which apply real infrastructure — JANUS never grants
mcp__terraform__* as a wildcard for exactly that reason.
The ansible-dev-tools MCP server, used by iac-author:
pip install ansible-dev-tools
adt mcp --sse --port 8081 # see the ansible-dev-tools docs for current flags
claude mcp add --transport sse ansible http://localhost:8081/sseOnly the authoring subset is granted — ansible_lint,
ansible_content_best_practices, zen_of_ansible,
create_ansible_projects, define_and_build_execution_env,
adt_check_env, ade_environment_info. Two of this server's tools are
deliberately granted to no agent: ansible_navigator runs playbooks
against real targets, and ade_setup_environment runs the host package
manager. Playbook execution belongs to lab-verify, post-approval, as an
explicit ansible-playbook command in the audit trail.
ax is a token-aware CLI for web
fetching, page discovery, and structured data extraction — used in place
of curl + throwaway parsing scripts.
brew install oven-sh/bun/bun
git clone --depth 1 https://github.com/yusukebe/ax.git /tmp/ax-src
cd /tmp/ax-src && bun install --ignore-scripts
bun build src/index.ts --compile --outfile ~/bin/ax
npx skills add yusukebe/axmdq queries Markdown documents the
way jq queries JSON — extract sections, lists, tables, links, and code
blocks by selector. Useful for filtering findings, reports, and ax --md
output.
brew install mdqNot an MCP server, and not required: scripts/prosecheck.py shells out to
textlint only for report_language: ja cases, and degrades to a notice
whenever it is absent. Install it if you hand Japanese reports to
customers:
npm install -g textlint \
textlint-rule-preset-ja-technical-writing \
textlint-filter-rule-node-typesThe config ships with the plugin at
skills/janus/scripts/textlintrc.json and is passed explicitly with
-c, so it never collides with a .textlintrc of your own. prosecheck.py
itself stays stdlib-only, CI needs no npm install, and npx is invoked
with --no-install so nothing is ever downloaded mid-investigation.
Investigation quality is enforced by explicit discipline, not by the model in the seat:
- Model provenance — the claim above is only worth as much as your
ability to check it, so every findings file records
model:— the model that actually ran that stage — and the report's Execution Metadata carries the roll-up. The Model strategy table states what was assigned; the cost and refusal ladders substitute a different model without announcing it, so the table cannot be read backwards. A stage that cannot tell writesunrecordedrather than guessing.validate.pykeeps the assigned side honest: every agent's declared model must match SKILL.md's roster, and the Model strategy table must not contradict it. - Evidence-basis labels — every finding carries
Basis: VERIFIED | REASONED | ASSUMED(tool output observed vs. inferred from reading vs. carried in) alongside its confidence, and a label is only promoted by new evidence. - Named acceptance gates — the lead checks each report against two
judgment gates: C1 GROUNDING (references, public URLs, no
speculation language, basis integrity) and C2 COMPLETENESS &
FIDELITY (completeness, verbatim artifact names, verbatim evidence
quotes). Failures go back to synthesize by sub-code (
C1/basis,C2/quote-absent, …); a HIGH hypothesis needs at least one VERIFIED finding behind it. Three of these checks are mechanical — reference liveness, quote fidelity, and the evidence chain (see Integrity checks below). - Causation gate — crash-analyze may not record a crash cause without "X causes Y because Z" where X and Y are observations from this vmcore; correlation without a mechanism caps at MEDIUM.
- Failure-pattern catalogs — agents carry
symptom → wrong move → correct moveentries seeded from real cases (e.g. a search timeout means "reduce scope", never "report negative"). - Lessons loop — project-specific lessons are banked (with human
approval) in
.claude/skills/janus-lessons/SKILL.md, which plugin updates never overwrite; the lead injects relevant entries into stage briefs, and recurring ones get promoted into the plugin's own catalogs via the self-improver review queue. - Declared fail direction — for each check, what it does when it proves a defect and what it does when it cannot tell are both written down, in SKILL.md's Fail direction table. Fail-closed where a defect is provable, fail-open where it is not (an air-gapped install has to stay usable), and across every row: a notice means not checked, never passed.
Four stdlib-only scripts turn "trust the report" into "check the report." All run at handoff; a failure sends the report back rather than shipping it.
Evidence chain — scripts/chain.py. Each case carries an
append-only hash ledger, cases/<id>/chain.jsonl. Every record holds
the sha256 of one evidence file plus the previous record's hash — the
same linked-hash idea as a blockchain. It makes edits visible, never
impossible: a legitimate revision (a report sent back to synthesize,
an updated finding) appends a new record and the ledger keeps the full
history; an edit that bypasses sealing breaks verification.
$ python3 scripts/chain.py verify cases/<id>
FAIL: TAMPER: results/report.md changed after last seal
Sealing is mostly automatic — a PostToolUse hook
(hooks/evidence-chain.py) seals every write into the evidence set
(case.yaml, findings, report, audit logs, verdict) — and the lead
also seals explicitly before synthesis and at close (covering
shell-written files the hook can't see). Deleting a record to cover
tracks fails too: the broken hash link exposes the gap. The upshot is
that the audit trail behind a claim can't be quietly rewritten after
the fact, and the human verdicts self-improver's metrics stand on stay
ground truth.
The chain detects rewrites; chain.py lock prevents the accident in
the first place. When the lead closes fan-in it drops the write bits on
the fact base (case.yaml, findings/*.md, audit/*), and a
PreToolUse hook (hooks/evidence-lock.py) denies tracked writes to
locked files with an explanation instead of a bare permission error —
so a stage can no longer clobber another stage's findings mid-flight.
chain.py unlock is the lead's explicit escape hatch for a legitimate
revision (unlock → edit → re-seal → lock).
Quote fidelity — scripts/quotecheck.py. The telephone-game
failure the lock can't catch: findings survive intact on disk while a
fact mutates as synthesize copies it into the report — "reproduced"
softens into "may reproduce", a version number drifts. The report is a
legitimately new file, so no hash ledger notices. Instead, the report
carries its load-bearing facts as attributed verbatim quotes
(> … / > — findings/<stage>.md), and quotecheck verifies each one
appears word-for-word in the file it cites — backing gate C2/quote:
$ python3 scripts/quotecheck.py cases/<id>/results/report.md
FAIL: report.md:12: quote not found verbatim in findings/doc-search.md: "…"
Reference liveness — scripts/urlcheck.py. Backs gate C1/url by
curl-checking every reference URL in the report. A fabricated citation
(the classic LLM failure) dies as a 404 instead of a footnote nobody
clicked:
$ python3 scripts/urlcheck.py cases/<id>/results/report.md
FAIL: https://access.redhat.com/errata/RHSA-2099:9999/ (404)
The check is deliberately honest about what it can't prove. A portal that 302-redirects a missing path into an SSO login flow (returns 200) is classified gated, not live — existence unconfirmable without authenticating, so it's flagged for a human rather than passed or failed. 401/403/429 fold into the same class. A fully-unreachable network downgrades to a notice and passes, so air-gapped okp-mcp installs stay usable.
Version provenance — scripts/versioncheck.py. The drift the quote
check can't see: a fact observed at one product version reworded into a
claim about another. Backs gate C2/version. The one hard FAIL is a
source location cited with no version anywhere in its Ref (no NVR,
casket path, or commit) — which version was read is unrecoverable. The
rest are warnings the lead judges: a Detail/Ref pair crossed within one
product family (Detail says 4.16, Ref pins 4.18), or — against the
version_scope a case may declare — a finding or report version in that
family but off-scope. Family-anchoring keeps kernel 5.14, image tags,
and RPM releases from drowning the OCP-minor signal:
$ python3 scripts/versioncheck.py cases/<id>
FAIL: findings/source-trace.md F2: source location cited with no version pin
warning: results/report.md: version 4.19 asserted but backed by no finding
Evidence links — scripts/linkcheck.py. The report is meant to be
read by clicking: each claim links straight to the finding behind it, so
a reviewer lands on the evidence instead of grepping for it.
H1 rests on [F3](../findings/crash-analyze.md#f3-sigsegv-in-qemu-kvm)
and the [lab trace](../audit/lab-1.log).That affordance creates a new way to lie, which is why the check exists.
urlcheck.py only sees http(s)://, so a relative link to a finding
that was never written — or to an #anchor no heading produces —
renders as an ordinary blue link and resolves to nothing. Backs gate
C1/link:
$ python3 scripts/linkcheck.py cases/<id>/results/report.md
FAIL: report.md:8: no such anchor in findings/crash-analyze.md: #f9-does-not-exist
FAIL: report.md:9: evidence file does not exist: findings/source-trace.md
Anchors are matched the way GitHub and VS Code generate them (lowercase,
punctuation dropped rather than hyphenated, duplicates suffixed -1),
plus explicit <a id="…">. Links inside fenced code blocks are ignored,
and a target outside the case directory is a FAIL. Unlike urlcheck this
never fails open — local resolution is deterministic and offline, so
a broken link is always a real defect.
Japanese prose quality — scripts/prosecheck.py. The other four ask
whether the report is true; none ask whether it is readable. For a
report_language: ja case this wraps
textlint with the
ja-technical-writing
preset and backs gate C2/prose:
$ python3 scripts/prosecheck.py cases/<id>
FAIL: report.md:7:47 [ja-technical-writing/no-mix-dearu-desumasu] "である"調 であるべき箇所に "ですます"調
FAIL: report.md:11:3 [ja-technical-writing/no-hankaku-kana] Disallow to use 半角カタカナ: "カタナ"
Two boundaries make it safe. It never rewrites — no --fix, because
that would mutate prose chain.py has sealed and quotecheck.py
cross-checks, and would break the rule that the lead never patches the
report itself; violations go back to synthesize like any other gate.
And ja-no-weak-phrase is switched off on purpose: it flags hedging,
but in JANUS a LOW-confidence hypothesis is supposed to read as
uncertain. Forcing assertive prose would make the report overclaim —
precisely what the Confidence/Basis labels exist to prevent.
This is the only check that shells out to a non-stdlib tool, so it fails
open in every direction — English case, textlint not installed, preset
missing, no report yet — with a notice and exit 0. A notice means not
checked, never passed; the script refuses to print OK for a run that
did not happen. Quoted evidence, code blocks, tables and headings are
excluded from linting (textlint-filter-rule-node-types), though note
that the preset's own rules already skip those node types — the filter is
a backstop for rules added later, such as prh for terminology, which do
match inside blockquotes.
All six scripts and the lock hook have offline self-tests
(scripts/selftest.py) exercising tamper detection, ledger-edit
detection, lock/deny/unlock, quote-mutation detection, the
gated-vs-dead URL split, version-provenance drift, evidence-link
resolution, and prosecheck's
fail-open paths (including the regression that once printed OK when
textlint was absent); .github/workflows/ci.yml runs them with
validate.py on every push and PR — no npm install needed, because the
prose check degrades to a notice there.
Read-only. Dead-artifact analysis (vmcore) is autonomous-safe; live-target work (lab provisioning, dynamic tracing) requires explicit human approval and a disposable lab, never production. Never spoofs guardrails; the final root-cause call is the human's.
MIT License - see the LICENSE file for details.
See CHANGELOG.md for version history.