This document is both reference material and a prompt template. Give it to an
engineer or an AI coding agent along with a filled-in Your task section, and
the result should be a new lumen <name> pipeline that looks and behaves exactly like every
other one — same file layout, same naming, same make/Docker/native wiring, same
verification steps. The goal is predictability: two different people implementing two
different pipelines from this document should produce structurally identical diffs.
The reference implementation for everything described here is the security-audit
pipeline, added in this exact way. When in doubt, go read its files instead of guessing:
pipeline/codedoc/pipelines/security_audit.pypipeline/codedoc/stages/security_audit_agent.pypipeline/codedoc/stages/parallel.pypipeline/codedoc/prompts/security-analyst-access.md,security-analyst-dependencies.md,security-threat-model.md,security-synthesis.mdpipeline/codedoc/cli.py(thecommon_pipeline_optionsdecorator and thesecurity_auditcommand)Makefile(lumen-security-audit,lumen-docker-security-audittargets)scripts/lumen-docker-security-audit.sh
Source repo → [preflight + indexer] ← SHARED, NEVER MODIFIED for a new pipeline
│
▼
KuzuDB graph
│
▼
[your new agent stage] ← THIS is the only pluggable part
fan-out (N parallel roles, each querying the graph + writing 1 artifact)
│
▼
fan-in (1 synthesis pass reading the fan-out artifacts off disk)
- Preflight and indexer are shared and permanent. Every pipeline reuses
run_preflightsandrun_indexercompletely unchanged. Never editpreflight/*orstages/indexer.pyto add a pipeline. - Only the agent stage (stage 2) is pluggable. A new pipeline gets its own
run_agent(state) -> statefunction in a newstages/<name>_agent.pymodule.stages/agent.py(the docs pipeline'srun_supervisor_agent, its archetype system, its artifact planner) is never touched or generalized for a new pipeline — it has its own contract that isn't worth coupling to. - The required shape is fan-out then fan-in: 2+ roles run in parallel, each querying the graph independently and writing its own findings artifact, then exactly one synthesis step reads those artifacts off disk and writes the final output. This mirrors the docs pipeline's Phase 2 (parallel analysts) → Phase 3 (architect) shape, just without the archetype/artifact-plan machinery.
- No shared plugin registry, no "PipelineSpec" abstraction. A new pipeline is a plain Python module built directly on the primitives below. Copying the reference implementation and renaming things is the intended workflow — don't build a framework on top of this document's pattern.
| Building block | Where | What it does |
|---|---|---|
KuzuBackend(kuzu_path) |
codedoc.kg_tools |
Opens the indexed graph (read-only by default). Instantiate a fresh one per thread/task — connections are not thread-safe. |
ReverseEngineerToolkit(backend, repo_path=...) |
codedoc.kg_tools |
The full graph query surface (40+ tools: get_schema, get_entry_points, get_api_endpoints, get_external_dependencies, get_hotspots, get_domains, get_workflows, get_unused_code, etc. — see kg_tools/toolkit.py for the complete list). |
create_provider(provider, model, base_url, num_ctx) |
codedoc.llm |
Builds the LLM provider (Claude/Ollama/OpenAI) from CLI/config values. Call it once per pipeline run, share the same instance across fan-out tasks. |
run_loop(provider, toolkit, system_prompt, user_request, output_root, ...) |
codedoc.stages.agent |
The generic agentic tool loop: gives the model graph tools + write_artifact, runs until it stops or hits max_turns. Key kwargs: allowed_artifact_paths (a set[str] — the ONLY filenames this call may write; anything else is rejected with a correction hint), phase_label (prefix for log lines, e.g. "<name>/<role>"), max_source_reads=0 (analysts should reason over graph structure, not raw source), include_write_artifact=True. Returns a dict: {"status", "error", "artifacts", "events", "tool_uses", "tool_call_counts", "input_tokens", "output_tokens"}. |
run_parallel_tasks(tasks: dict[str, Callable[[], dict]]) |
codedoc.stages.parallel |
The fan-out/fan-in concurrency helper. Give it a dict of no-arg callables (each one builds its own KuzuBackend/ReverseEngineerToolkit and calls run_loop), get back a dict of their result dicts. |
common_pipeline_options |
codedoc.cli |
Click decorator bundling the ~10 shared options (--output-dir, --model, --provider, --max-turns, --allow-xlarge, etc.) that every agent-stage pipeline command exposes identically. |
create_run_dir, init_state, log_pipeline_start, finalize_state, apply_repo_size_runtime_defaults(state, bump_max_turns=...), should_stop_for_xlarge_repo(state) |
codedoc.pipelines.common |
Shared run-dir/state/finalization plumbing every pipeline module calls in the same order. Mode-agnostic — no code here needs editing when you add a pipeline. |
start_agent_boxes(agent_names=[...], workflow_phases=[...]), update_agent_box(name, status=, tool=, artifacts=), update_workflow_phase(name, status=, tool=), print_researcher_done(name, n_artifacts), print_tool_usage_table(per_role_tool_counts), print_synthesizer_done(n_artifacts), stop_agent_boxes() |
codedoc.log |
The live fan-out/fan-in dashboard lumen run uses. Data-driven, not hardcoded — pass your own role names/phase names to start_agent_boxes() and every downstream call (including the per-turn progress line run_loop already emits via phase_label) routes into your boxes automatically. Skipping this wiring is the #1 way a new pipeline's console output ends up looking inconsistent with lumen run. |
pipeline/codedoc/stages/agent.py— the docs pipeline's supervisor, prompts, and artifact-plan logic. Reuserun_loop(a plain function it exports); don't modify anything else in this file.pipeline/codedoc/archetype_registry.py,pipeline/codedoc/artifact_planner.py— the docs pipeline's archetype/artifact-plan system. Your pipeline defines its own smallallowed_artifact_pathssets directly; it does not need or use this system.pipeline/codedoc/stages/builder.py— the MkDocs build step. Only call it from your pipeline module if you specifically want an MkDocs site; most non-docs pipelines skip it and leave artifacts as plain markdown (seesecurity_audit.py, which has no Stage 3).pipeline/codedoc/preflight/*,pipeline/codedoc/stages/indexer.py— shared and unmodified, as above.Dockerfile,scripts/build-native.sh,scripts/install-lumen.sh, the nativelumenlauncher template — all already subcommand-agnostic. A new pipeline needs zero changes here (see the Make/Docker/native section below).
Pick one <name> in kebab-case for your pipeline (e.g. security-audit,
test-coverage, dependency-graph). Derive everything else from it mechanically:
| Thing | Convention | Example |
|---|---|---|
| CLI command | <name> (kebab-case, as-is) |
lumen security-audit |
mode passed to init_state |
<name> (kebab-case, as-is) |
"security-audit" |
| Pipeline module | pipelines/<name_snake>.py |
pipelines/security_audit.py |
| Agent-stage module | stages/<name_snake>_agent.py |
stages/security_audit_agent.py |
| Prompt files | prompts/<name>-<role>.md, prompts/<name>-synthesis.md |
prompts/security-analyst-access.md, prompts/security-synthesis.md |
phase_label per run_loop call |
<short-name>/<role> |
"security/access", "security/synthesis" |
| Artifact paths | <short-name>/<artifact>.md, one folder per pipeline |
security/access-control-findings.md |
| Makefile targets | lumen-<name> (native), lumen-docker-<name> (Docker) |
lumen-security-audit, lumen-docker-security-audit |
| Docker wrapper script | scripts/lumen-docker-<name>.sh |
scripts/lumen-docker-security-audit.sh |
(<name_snake> is <name> with hyphens replaced by underscores, since it's a Python
module name.)
Before writing code, decide:
- How many parallel roles (2–4 is typical; it does not have to be 3 just because the docs pipeline uses 3 analysts).
- What each role's single findings artifact is named.
- What the one fan-in/synthesis step reads and produces.
- Whether you need a Stage 3 (MkDocs build) at all — most non-docs pipelines don't.
One markdown prompt per role, plus one synthesis prompt, under pipeline/codedoc/prompts/.
Follow the existing style (see security-analyst-access.md for a role prompt,
security-synthesis.md for the fan-in prompt):
- A
## Your Artifactsection with the exact filename and a fenced template of its expected structure. - An evidence model: role prompts tag findings
[Observed](from a tool call) vs.[Inferred](reasoned); the synthesis prompt tags everything[Synthesized]. - Explicit tool names to call (pull real names from
kg_tools/toolkit.py, don't invent ones) and a line count budget (≤ 80 lines total, etc.) to keep output focused. - The synthesis prompt must say "You do NOT call any graph query tools. You only call
write_artifact." — it receives role artifacts injected as text, it doesn't query the graph itself.
Wire in the pretty logging — this is not optional. run_loop alone does not make your
pipeline's console output look like lumen run's. You must explicitly call the
codedoc.log dashboard functions with your own role/phase names (see the table above and
the skeleton below): start_agent_boxes(agent_names=..., workflow_phases=...) before
fan-out, update_agent_box(...) when each role starts/finishes, print_researcher_done
print_tool_usage_tableafter fan-out,update_workflow_phase(...)+print_synthesizer_donearound the fan-in call, andstop_agent_boxes()on every exit path (success, failure, and exceptions — usetry/exceptaround the fan-out and fan-in calls as shown below so a crash doesn't leave a half-finished live dashboard on screen). Skipping this was a real regression once already: a pipeline built without it fell back to plain dim-text lines with no live boxes, no "researcher done" checkmarks, and no tool-usage table — visibly inconsistent withlumen run, and it had to be retrofitted afterward. Do it right the first time.
Skeleton (copy stages/security_audit_agent.py and rename):
"""Stage 2 (alternative) — <Name> agent."""
from __future__ import annotations
from pathlib import Path
from typing import Any
from codedoc.kg_tools import KuzuBackend, ReverseEngineerToolkit
from codedoc.llm import create_provider
from codedoc.stages.agent import run_loop
from codedoc.stages.parallel import run_parallel_tasks
_PROMPTS_DIR = Path(__file__).resolve().parent.parent / "prompts"
_ROLE_A_ARTIFACT = "<short-name>/<role-a>-findings.md"
_ROLE_B_ARTIFACT = "<short-name>/<role-b>-findings.md"
_REPORT_ARTIFACT = "<short-name>/report.md"
_ROLE_PROMPT_FILES: dict[str, str] = {
"<short-name>/<role-a>": "<name>-<role-a>.md",
"<short-name>/<role-b>": "<name>-<role-b>.md",
}
_ROLE_ARTIFACTS: dict[str, set[str]] = {
"<short-name>/<role-a>": {_ROLE_A_ARTIFACT},
"<short-name>/<role-b>": {_ROLE_B_ARTIFACT},
}
def _load_prompt(filename: str) -> str:
return (_PROMPTS_DIR / filename).read_text(encoding="utf-8")
def _orientation_summary(kuzu_path: str, repo_path: str) -> str:
backend = KuzuBackend(kuzu_path)
toolkit = ReverseEngineerToolkit(backend, repo_path=repo_path)
try:
return toolkit.call("get_architecture_summary")
except Exception as e:
return f"[Orientation data unavailable — graph query failed ({e}).]"
def _make_role_task(name, *, provider, kuzu_path, repo_path, repo_name,
orientation_summary, artifacts_dir, max_turns,
max_context_tokens, use_anthropic_format, verbose):
def _task() -> dict:
backend = KuzuBackend(kuzu_path)
toolkit = ReverseEngineerToolkit(backend, repo_path=repo_path)
system_prompt = (
_load_prompt(_ROLE_PROMPT_FILES[name])
+ f"\n\n---\n## Orientation Summary\n\n{orientation_summary}\n"
+ f"\n\n---\n## Runtime context\n\n- Repository name: `{repo_name}`\n"
)
return run_loop(
provider=provider, toolkit=toolkit, system_prompt=system_prompt,
user_request="Begin your review and write your findings artifact.",
output_root=artifacts_dir, max_turns=max_turns, verbose=verbose,
use_anthropic_format=use_anthropic_format,
max_context_tokens=max_context_tokens, max_source_reads=0,
include_write_artifact=True, phase_label=name,
allowed_artifact_paths=_ROLE_ARTIFACTS[name],
)
return _task
def _build_synthesis_prompt(artifacts_dir: str, repo_name: str) -> str:
prompt = _load_prompt("<name>-synthesis.md")
prompt += "\n\n---\n## Findings (written by reviewers)\n\n"
for rel_path in (_ROLE_A_ARTIFACT, _ROLE_B_ARTIFACT):
full_path = Path(artifacts_dir) / rel_path
content = full_path.read_text(encoding="utf-8") if full_path.exists() else "_Not produced._"
prompt += f"### {rel_path}\n\n{content}\n\n"
prompt += (
f"\n\n---\n## Runtime context\n\n- Repository name: `{repo_name}`\n"
f"- Write only `{_REPORT_ARTIFACT}` using the `write_artifact` tool.\n"
"- Do NOT call any graph query tools.\n"
)
return prompt
def run_agent(state) -> Any:
"""Pipeline stage 2 for the <name> pipeline. PipelineState in, PipelineState out."""
kuzu_path, repo_path, output_dir = state.kuzu_path, state.repo_path, state.output_dir
model, provider_name, base_url = state.model, state.provider, state.base_url
max_turns, max_context_tokens = state.max_turns, state.max_context_tokens
ollama_num_ctx = state.ollama_num_ctx or 131_072
verbose = state.verbose
repo_name = state.repo_name or (Path(repo_path).name if repo_path else "unknown")
try:
KuzuBackend(kuzu_path)
except FileNotFoundError as e:
state.status, state.error = "failed", str(e)
return state
provider = create_provider(provider=provider_name, model=model, base_url=base_url, num_ctx=ollama_num_ctx)
use_anthropic_format = provider_name in ("claude", "anthropic") or (
provider_name == "auto" and (model.startswith("claude") or model.startswith("anthropic"))
)
artifacts_dir = str(Path(output_dir) / "artifacts")
Path(artifacts_dir).mkdir(parents=True, exist_ok=True)
events, input_tokens, output_tokens, tool_uses = [], 0, 0, 0
from codedoc import log as _log
def log(msg: str) -> None:
events.append(msg)
_log.print_supervisor_line(msg)
log("[<name>] Phase 1 — orientation (direct toolkit query)")
orientation_summary = _orientation_summary(kuzu_path, repo_path)
role_turns = max(10, max_turns // 3)
synthesis_turns = max(6, max_turns // 4)
role_names = list(_ROLE_PROMPT_FILES)
log(f"[<name>] Phase 2 — spawning {len(role_names)} roles in parallel")
# start_agent_boxes() is what makes this pipeline's console output look like
# `lumen run`'s — pass YOUR role/phase names, don't skip this call.
_log.start_agent_boxes(agent_names=role_names, workflow_phases=["<short-name>/synthesis"])
for name in role_names:
_log.update_agent_box(name, status="running", tool="starting")
tasks = {
name: _make_role_task(
name, provider=provider, kuzu_path=kuzu_path, repo_path=repo_path,
repo_name=repo_name, orientation_summary=orientation_summary,
artifacts_dir=artifacts_dir, max_turns=role_turns,
max_context_tokens=max_context_tokens,
use_anthropic_format=use_anthropic_format, verbose=verbose,
)
for name in role_names
}
try:
results = run_parallel_tasks(tasks)
except Exception:
_log.stop_agent_boxes()
raise
all_artifacts = []
role_tool_counts = {}
for name, result in results.items():
events.extend(result["events"])
input_tokens += result["input_tokens"]
output_tokens += result["output_tokens"]
tool_uses += result["tool_uses"]
role_tool_counts[name] = result.get("tool_call_counts", {})
n_artifacts = len(result["artifacts"])
if result["status"] == "failed":
_log.update_agent_box(name, status="failed", tool="error")
log(f"[<name>] WARNING: {name} failed: {result['error']}")
else:
all_artifacts.extend(result["artifacts"])
_log.update_agent_box(name, status="done", tool="complete", artifacts=n_artifacts)
log(f"[<name>] {name} done — {n_artifacts} artifact(s)")
_log.print_researcher_done(name, n_artifacts)
_log.print_tool_usage_table(role_tool_counts)
log("[<name>] Phase 3 — running synthesis…")
_log.update_workflow_phase("<short-name>/synthesis", status="running", tool="synthesizing")
synth_backend = KuzuBackend(kuzu_path)
synth_toolkit = ReverseEngineerToolkit(synth_backend, repo_path=repo_path)
try:
synthesis_result = run_loop(
provider=provider, toolkit=synth_toolkit,
system_prompt=_build_synthesis_prompt(artifacts_dir, repo_name),
user_request="Write the final report now.", output_root=artifacts_dir,
max_turns=synthesis_turns, verbose=verbose,
use_anthropic_format=use_anthropic_format,
max_context_tokens=max_context_tokens, max_source_reads=0,
include_write_artifact=True, phase_label="<short-name>/synthesis",
allowed_artifact_paths={_REPORT_ARTIFACT},
)
except Exception:
_log.update_workflow_phase("<short-name>/synthesis", status="failed", tool="error")
_log.stop_agent_boxes()
raise
events.extend(synthesis_result["events"])
input_tokens += synthesis_result["input_tokens"]
output_tokens += synthesis_result["output_tokens"]
tool_uses += synthesis_result["tool_uses"]
state.events = list(state.events) + events
state.input_tokens, state.output_tokens, state.tool_uses = input_tokens, output_tokens, tool_uses
if synthesis_result["status"] == "failed":
_log.update_workflow_phase("<short-name>/synthesis", status="failed", tool="error")
_log.stop_agent_boxes()
state.status, state.error = "failed", synthesis_result["error"]
return state
_log.update_workflow_phase("<short-name>/synthesis", status="done", tool="report written")
_log.print_synthesizer_done(len(synthesis_result["artifacts"]))
_log.stop_agent_boxes()
all_artifacts.extend(synthesis_result["artifacts"])
if not all_artifacts:
state.status, state.error = "failed", "<name> agent completed but wrote no artifacts"
return state
state.artifacts_dir = artifacts_dir
state.log("agent", f"{len(all_artifacts)} artifact(s) → {artifacts_dir}")
return stateSkeleton (copy pipelines/security_audit.py and rename):
"""<Name> Lumen pipeline orchestration."""
from __future__ import annotations
import time
from pathlib import Path
from codedoc.preflight import run_preflights
from codedoc.pipelines.common import (
apply_repo_size_runtime_defaults,
create_run_dir,
finalize_state,
init_state,
log_pipeline_start,
should_stop_for_xlarge_repo,
)
from codedoc.stages.indexer import run_indexer
from codedoc.stages.<name_snake>_agent import run_agent
from codedoc.state import PipelineState
def run_pipeline(
repo_path: str, output_dir: str, model: str, provider: str, base_url: str,
max_turns: int, max_context_tokens: int = 120_000, ollama_num_ctx: int = 131_072,
timeout: int = 300, repo_size_check: str = "warn", allow_xlarge: bool = False,
timeout_explicit: bool = False, max_turns_explicit: bool = False,
verbose: bool = False, indexer_bin_dir: str = "", repo_name: str | None = None,
) -> PipelineState:
repo = Path(repo_path).resolve()
resolved_repo_name = repo_name or repo.name
run_dir = create_run_dir(output_dir, resolved_repo_name)
state = init_state(
repo_path=str(repo), run_dir=run_dir, mode="<name>",
model=model, provider=provider, base_url=base_url, max_turns=max_turns,
max_context_tokens=max_context_tokens, ollama_num_ctx=ollama_num_ctx,
timeout=timeout, repo_size_check=repo_size_check, allow_xlarge=allow_xlarge,
verbose=verbose, timeout_explicit=timeout_explicit,
max_turns_explicit=max_turns_explicit, indexer_bin_dir=indexer_bin_dir,
repo_name=resolved_repo_name,
)
log_pipeline_start(state, repo_path=str(repo), run_dir=run_dir, label="<name> pipeline")
from codedoc import log as _log
pipeline_t0 = time.time()
try:
state.log("pipeline", "=== Preflight: Repo Metrics ===")
state = run_preflights(state)
if state.status == "failed":
return state
state = apply_repo_size_runtime_defaults(state, bump_max_turns=True)
if should_stop_for_xlarge_repo(state):
state.status = "stopped"
state.error = "Repo classified as xlarge; rerun with --allow-xlarge to continue."
return state
_log.print_stage_header(1, "Indexer")
t0 = time.time()
state = run_indexer(state)
_log.print_stage_done(1, "Indexer", time.time() - t0)
if state.status == "failed":
return state
_log.print_stage_header(2, "Agent")
t0 = time.time()
state = run_agent(state)
_log.print_stage_done(2, "Agent", time.time() - t0)
if state.status == "failed":
return state
state.status = "done"
state.log("pipeline", f"Pipeline completed successfully in {time.time() - pipeline_t0:.1f}s")
except Exception as exc:
state.status, state.error = "failed", str(exc)
finally:
finalize_state(state, run_dir)
return stateSkip the Stage 3 builder call entirely unless your pipeline needs an MkDocs site — see "What NOT to touch" above.
- Import:
from codedoc.pipelines.<name_snake> import run_pipeline as run_<name_snake>_pipeline - Copy the
security_auditcommand function (cli.py), rename it, keep@main.command(name="<name>")+@common_pipeline_options+ therepo_pathargument. The body (config loading, callingrun_<name_snake>_pipeline, success/failure panel) is identical boilerplate — only the pipeline import and docstring change.
- Makefile: add
lumen-<name>(native) andlumen-docker-<name>(Docker) targets by copying thelumen-security-audit/lumen-docker-security-auditblocks and renaming. Add both names to.PHONY. Add aCMD=lumen-<name>/CMD=lumen-docker-<name>case to thelumen-helpcase statement, and one line each to the default (no-CMD) listing. - Docker wrapper script: copy
scripts/lumen-docker-security-audit.shtoscripts/lumen-docker-<name>.sh, change the subcommand in thecmd=(...)line fromsecurity-auditto<name>,chmod +xit. - Docker image, native bundle,
install-lumen.sh: no changes.Dockerfile'sENTRYPOINT ["lumen"]and the native bundle'slumenlauncher (exec ... "$@") both already resolve any registered subcommand — they only need the image/bundle to contain the updatedcodedocpackage.
README.md: add alumen <name> REPO_PATH [OPTIONS]block to the CLI Reference section (copy thesecurity-auditblock), and a short usage mention if this is a second/third alternative pipeline worth calling out in prose.CLAUDE.md: add the new files to thepipeline/key-files list, and add a Design Decisions row if you made any non-obvious choice (e.g. skipping the builder step, a different role count, a different orientation query set).
Once step 6 (Makefile target) is done, ./e2e-test/test-lumen.sh discovers your
new pipeline automatically — it greps the Makefile for lumen-<name>: targets and runs
each one against the checked-in fixture repos under e2e-test/fixtures/, verifying exit
code, pipeline.json status/mode, and that artifacts were actually written (see the
script itself for exactly what it checks). Run it after finishing the implementation; it
covers most of the manual checks below in one command. The manual checks are for when
test-lumen.sh fails and you need to narrow down why, or before the Makefile target
exists yet.
python3 -c "import ast; ast.parse(open('<file>').read())"on every new file, or just import them directly:uv run python -c "import codedoc.pipelines.<name_snake>".uv run lumen --helpshows the new command;uv run lumen <name> --helpshows the same option set asrun(provescommon_pipeline_optionswas applied correctly).- Smoke test against a tiny synthetic repo (3–5 files is enough — don't burn tokens/time on
a real large repo for this). Use
--max-turns 20-30 --verboseto bound cost and confirm:- Stage 1 (indexer) log output is identical in shape to
lumen run's. - The fan-out roles' log lines interleave (proves they ran concurrently, not
sequentially) — look for alternating
[<role-a>]/[<role-b>]tags. - Each role writes exactly its assigned artifact; the synthesis step's log shows it reading both role artifacts and writing the final report.
pipeline.jsonin the run's output dir has"mode": "<name>"and"status": "done".- The "researcher done" checkmark lines, the "Tool usage by researcher" table, and the
"synthesizer done" line all appear (these come from
print_researcher_done,print_tool_usage_table,print_synthesizer_done— if they're missing, you skipped thecodedoc.logwiring in step 3). In a real terminal (not piped/redirected), you should also see the live colored dashboard boxes update in place during Phase 2/3, matchinglumen run's look —console.is_terminalisFalsewhen output is piped, so redirecting to a file/tailwill only show the plain-line fallbacks, not the boxes.
- Stage 1 (indexer) log output is identical in shape to
- If you touched
pipelines/common.pyorcodedoc/log.py(you shouldn't normally need to), re-verifylumen runstill works — at minimum re-importcodedoc.pipelines.full,codedoc.pipelines.mcp,codedoc.pipelines.mcp_http, and confirmcodedoc.log.start_agent_boxes()(no args) still produces the exact sameanalyst/domain|flows|tech+synthesis/architect/summarystate as before. - Clean up smoke-test output directories before committing.
Fill in this section with what you want the new pipeline to do, then hand this whole document + this section to whoever (or whatever) is implementing it.
- Pipeline name (kebab-case):
<fill in> - What should it analyze or produce?
<fill in — e.g. "test coverage gaps: which public methods have no corresponding test, grounded in call-graph reachability from test files"> - Fan-out roles (2–4, each with one findings artifact):
<fill in> - Fan-in synthesis output:
<fill in — the one final report artifact> - Does it need a Stage 3 (MkDocs build)?:
<fill in — default: no> - Graph tools each role is expected to lean on (check
kg_tools/toolkit.pyfor the full list; don't invent tool names):<fill in>
Implement it by following every step above, in order, using the naming conventions table
exactly. Do not modify stages/agent.py, archetype_registry.py, artifact_planner.py,
preflight/*, stages/indexer.py, Dockerfile, or scripts/build-native.sh.