aetherscan 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. aetherscan-1.0.0/.claude/skills/aetherscan-repo-context/SKILL.md +283 -0
  2. aetherscan-1.0.0/.github/ISSUE_TEMPLATE/bug_report.md +33 -0
  3. aetherscan-1.0.0/.github/ISSUE_TEMPLATE/feature_request.md +22 -0
  4. aetherscan-1.0.0/.github/workflows/auto-assign-author.yml +51 -0
  5. aetherscan-1.0.0/.github/workflows/claude-code-review.yml +78 -0
  6. aetherscan-1.0.0/.github/workflows/claude-contribution-check.yml +136 -0
  7. aetherscan-1.0.0/.github/workflows/claude-dependency-check.yml +181 -0
  8. aetherscan-1.0.0/.github/workflows/claude-flaky-test-tracker.yml +151 -0
  9. aetherscan-1.0.0/.github/workflows/claude-issue-triage.yml +80 -0
  10. aetherscan-1.0.0/.github/workflows/claude-release-notes.yml +129 -0
  11. aetherscan-1.0.0/.github/workflows/claude-style-check.yml +217 -0
  12. aetherscan-1.0.0/.github/workflows/claude-update-docs.yml +379 -0
  13. aetherscan-1.0.0/.github/workflows/claude.yml +236 -0
  14. aetherscan-1.0.0/.github/workflows/pre-commit.yml +55 -0
  15. aetherscan-1.0.0/.github/workflows/release.yml +286 -0
  16. aetherscan-1.0.0/.github/workflows/sync-pr-labels.yml +170 -0
  17. aetherscan-1.0.0/.github/workflows/tests.yml +62 -0
  18. aetherscan-1.0.0/.gitignore +68 -0
  19. aetherscan-1.0.0/.pre-commit-config.yaml +46 -0
  20. aetherscan-1.0.0/AI_POLICY.md +31 -0
  21. aetherscan-1.0.0/CITATION.cff +31 -0
  22. aetherscan-1.0.0/CLAUDE.md +66 -0
  23. aetherscan-1.0.0/CODEOWNERS +7 -0
  24. aetherscan-1.0.0/CODE_OF_CONDUCT.md +83 -0
  25. aetherscan-1.0.0/CONTRIBUTING.md +622 -0
  26. aetherscan-1.0.0/KNOWN_ISSUES.md +645 -0
  27. aetherscan-1.0.0/LICENSE +13 -0
  28. aetherscan-1.0.0/PKG-INFO +1187 -0
  29. aetherscan-1.0.0/README.md +1134 -0
  30. aetherscan-1.0.0/SECURITY.md +204 -0
  31. aetherscan-1.0.0/aetherscan.def +64 -0
  32. aetherscan-1.0.0/benchmarks/README.md +549 -0
  33. aetherscan-1.0.0/benchmarks/_common.py +76 -0
  34. aetherscan-1.0.0/benchmarks/bench_datagen.py +189 -0
  35. aetherscan-1.0.0/benchmarks/bench_db_index_shapes.py +435 -0
  36. aetherscan-1.0.0/benchmarks/bench_gpu.py +389 -0
  37. aetherscan-1.0.0/benchmarks/bench_injection.py +71 -0
  38. aetherscan-1.0.0/benchmarks/bench_injection_index.py +314 -0
  39. aetherscan-1.0.0/benchmarks/bench_input_pipeline.py +564 -0
  40. aetherscan-1.0.0/benchmarks/bench_latent_gif.py +382 -0
  41. aetherscan-1.0.0/benchmarks/bench_lognorm_downsample.py +92 -0
  42. aetherscan-1.0.0/benchmarks/bench_normality.py +93 -0
  43. aetherscan-1.0.0/benchmarks/bench_pfb_vs_spline.py +92 -0
  44. aetherscan-1.0.0/benchmarks/bench_rf.py +170 -0
  45. aetherscan-1.0.0/benchmarks/parse_xplane_occupancy.py +82 -0
  46. aetherscan-1.0.0/docs/ARCHITECTURE.md +294 -0
  47. aetherscan-1.0.0/docs/BENCHMARKING.md +281 -0
  48. aetherscan-1.0.0/docs/CONFIG_AND_CLI.md +457 -0
  49. aetherscan-1.0.0/docs/DATABASE.md +465 -0
  50. aetherscan-1.0.0/docs/GITHUB_AUTOMATION.md +150 -0
  51. aetherscan-1.0.0/docs/GPU_RUNTIME_GUIDE.md +242 -0
  52. aetherscan-1.0.0/docs/INFERENCE_PIPELINE.md +508 -0
  53. aetherscan-1.0.0/docs/MODELS.md +266 -0
  54. aetherscan-1.0.0/docs/PREPROCESSING.md +357 -0
  55. aetherscan-1.0.0/docs/README.md +19 -0
  56. aetherscan-1.0.0/docs/RELEASE.md +274 -0
  57. aetherscan-1.0.0/docs/RUNTIME_SERVICES.md +273 -0
  58. aetherscan-1.0.0/docs/TESTING.md +286 -0
  59. aetherscan-1.0.0/docs/TRAINING_PIPELINE.md +888 -0
  60. aetherscan-1.0.0/docs/assets/aetherscan-banner.png +0 -0
  61. aetherscan-1.0.0/docs/assets/github-citation-button.png +0 -0
  62. aetherscan-1.0.0/environment.yml +74 -0
  63. aetherscan-1.0.0/pyproject.toml +191 -0
  64. aetherscan-1.0.0/requirements-container.txt +45 -0
  65. aetherscan-1.0.0/src/aetherscan/__init__.py +45 -0
  66. aetherscan-1.0.0/src/aetherscan/benchmark.py +179 -0
  67. aetherscan-1.0.0/src/aetherscan/candidate_figures.py +328 -0
  68. aetherscan-1.0.0/src/aetherscan/cli.py +3032 -0
  69. aetherscan-1.0.0/src/aetherscan/config.py +1002 -0
  70. aetherscan-1.0.0/src/aetherscan/dashboard.py +709 -0
  71. aetherscan-1.0.0/src/aetherscan/dashboard_cli.py +51 -0
  72. aetherscan-1.0.0/src/aetherscan/dashboard_launcher.py +194 -0
  73. aetherscan-1.0.0/src/aetherscan/data_generation.py +1448 -0
  74. aetherscan-1.0.0/src/aetherscan/db/__init__.py +21 -0
  75. aetherscan-1.0.0/src/aetherscan/db/db.py +2789 -0
  76. aetherscan-1.0.0/src/aetherscan/hf_hub.py +547 -0
  77. aetherscan-1.0.0/src/aetherscan/inference.py +912 -0
  78. aetherscan-1.0.0/src/aetherscan/inference_viz.py +1494 -0
  79. aetherscan-1.0.0/src/aetherscan/latent_gif.py +512 -0
  80. aetherscan-1.0.0/src/aetherscan/latent_variants.py +352 -0
  81. aetherscan-1.0.0/src/aetherscan/logger/__init__.py +21 -0
  82. aetherscan-1.0.0/src/aetherscan/logger/logger.py +467 -0
  83. aetherscan-1.0.0/src/aetherscan/logger/slack_handler.py +760 -0
  84. aetherscan-1.0.0/src/aetherscan/main.py +1269 -0
  85. aetherscan-1.0.0/src/aetherscan/manager/__init__.py +21 -0
  86. aetherscan-1.0.0/src/aetherscan/manager/manager.py +781 -0
  87. aetherscan-1.0.0/src/aetherscan/models/__init__.py +21 -0
  88. aetherscan-1.0.0/src/aetherscan/models/random_forest.py +171 -0
  89. aetherscan-1.0.0/src/aetherscan/models/vae.py +849 -0
  90. aetherscan-1.0.0/src/aetherscan/monitor/__init__.py +19 -0
  91. aetherscan-1.0.0/src/aetherscan/monitor/monitor.py +935 -0
  92. aetherscan-1.0.0/src/aetherscan/pfb.py +161 -0
  93. aetherscan-1.0.0/src/aetherscan/preprocessing.py +2718 -0
  94. aetherscan-1.0.0/src/aetherscan/rf_metrics.py +100 -0
  95. aetherscan-1.0.0/src/aetherscan/round_data.py +932 -0
  96. aetherscan-1.0.0/src/aetherscan/run_state.py +277 -0
  97. aetherscan-1.0.0/src/aetherscan/seeding.py +160 -0
  98. aetherscan-1.0.0/src/aetherscan/shap_parallel.py +238 -0
  99. aetherscan-1.0.0/src/aetherscan/tag_guards.py +206 -0
  100. aetherscan-1.0.0/src/aetherscan/train.py +7681 -0
  101. aetherscan-1.0.0/tests/conftest.py +231 -0
  102. aetherscan-1.0.0/tests/fixtures/ed_real_slice.npz +0 -0
  103. aetherscan-1.0.0/tests/integration/conftest.py +63 -0
  104. aetherscan-1.0.0/tests/integration/test_inference_smoke.py +108 -0
  105. aetherscan-1.0.0/tests/integration/test_model_behavior.py +143 -0
  106. aetherscan-1.0.0/tests/integration/test_seeding_determinism.py +145 -0
  107. aetherscan-1.0.0/tests/integration/test_train_smoke.py +98 -0
  108. aetherscan-1.0.0/tests/unit/test_benchmark.py +432 -0
  109. aetherscan-1.0.0/tests/unit/test_candidate_figures.py +158 -0
  110. aetherscan-1.0.0/tests/unit/test_cli_help_sync.py +64 -0
  111. aetherscan-1.0.0/tests/unit/test_cli_validation.py +1144 -0
  112. aetherscan-1.0.0/tests/unit/test_config.py +152 -0
  113. aetherscan-1.0.0/tests/unit/test_dashboard.py +367 -0
  114. aetherscan-1.0.0/tests/unit/test_dashboard_cli.py +76 -0
  115. aetherscan-1.0.0/tests/unit/test_dashboard_launcher.py +192 -0
  116. aetherscan-1.0.0/tests/unit/test_data_generation.py +688 -0
  117. aetherscan-1.0.0/tests/unit/test_db.py +1121 -0
  118. aetherscan-1.0.0/tests/unit/test_hf_hub.py +627 -0
  119. aetherscan-1.0.0/tests/unit/test_inference.py +401 -0
  120. aetherscan-1.0.0/tests/unit/test_inference_viz.py +744 -0
  121. aetherscan-1.0.0/tests/unit/test_latent_gif.py +254 -0
  122. aetherscan-1.0.0/tests/unit/test_latent_traversal.py +230 -0
  123. aetherscan-1.0.0/tests/unit/test_latent_variants.py +238 -0
  124. aetherscan-1.0.0/tests/unit/test_logger.py +248 -0
  125. aetherscan-1.0.0/tests/unit/test_main.py +636 -0
  126. aetherscan-1.0.0/tests/unit/test_manager.py +266 -0
  127. aetherscan-1.0.0/tests/unit/test_models.py +515 -0
  128. aetherscan-1.0.0/tests/unit/test_monitor.py +363 -0
  129. aetherscan-1.0.0/tests/unit/test_pfb.py +246 -0
  130. aetherscan-1.0.0/tests/unit/test_preprocessing.py +1870 -0
  131. aetherscan-1.0.0/tests/unit/test_rf_metrics.py +105 -0
  132. aetherscan-1.0.0/tests/unit/test_round_data.py +1179 -0
  133. aetherscan-1.0.0/tests/unit/test_run_state.py +426 -0
  134. aetherscan-1.0.0/tests/unit/test_seeding.py +120 -0
  135. aetherscan-1.0.0/tests/unit/test_shap_parallel.py +152 -0
  136. aetherscan-1.0.0/tests/unit/test_tag_guards.py +245 -0
  137. aetherscan-1.0.0/tests/unit/test_train_accumulation.py +191 -0
  138. aetherscan-1.0.0/tests/unit/test_train_datasets.py +316 -0
  139. aetherscan-1.0.0/tests/unit/test_train_distribution.py +217 -0
  140. aetherscan-1.0.0/tests/unit/test_train_utils.py +1097 -0
  141. aetherscan-1.0.0/utils/benchmark_report.py +545 -0
  142. aetherscan-1.0.0/utils/fetch_run_outputs.sh +233 -0
  143. aetherscan-1.0.0/utils/find_optimal_configs.py +242 -0
  144. aetherscan-1.0.0/utils/get_system_info.sh +105 -0
  145. aetherscan-1.0.0/utils/hf_tag_release.py +107 -0
  146. aetherscan-1.0.0/utils/kill_pipeline.sh +282 -0
  147. aetherscan-1.0.0/utils/print_cli_help.py +76 -0
  148. aetherscan-1.0.0/utils/run_container.sh +129 -0
  149. aetherscan-1.0.0/utils/start_tmux_session.sh +76 -0
  150. aetherscan-1.0.0/utils/verify_train_test_files.py +195 -0
@@ -0,0 +1,283 @@
1
+ ---
2
+ name: aetherscan-repo-context
3
+ description: Deep-dive context for working inside the Aetherscan repo — Breakthrough Listen's deep-learning SETI pipeline (Beta-VAE → Random Forest, multi-GPU). Use when developing, debugging, configuring, running, or reviewing changes to this codebase, or when answering questions about its install paths, CLI, config system, conventions, contribution workflow, or security practices.
4
+ ---
5
+
6
+ # Working in the Aetherscan Repo
7
+
8
+ Aetherscan is Breakthrough Listen's first end-to-end production-grade deep-learning pipeline for SETI at scale. It detects anomalies in radio spectrograms with technosignature-like characteristics by pairing a **beta-VAE** (dimensionality reduction / feature extraction) with a **Random Forest** ensemble (candidate detection). It is based on [Ma et al. 2023](https://arxiv.org/abs/2301.12670) and runs single-node data-parallel distributed training/inference.
9
+
10
+ > **Scope.** `CLAUDE.md` (repo root) holds the lean, always-on rules; this skill is the on-demand deep-dive — read it when a task needs more than the essentials. The authoritative sources are `README.md`, `CONTRIBUTING.md`, `SECURITY.md`, `KNOWN_ISSUES.md`, and `docs/`; when this file disagrees with them, they win and this file should be updated. **All paths below are relative to the repository root** (an agent's working directory).
11
+
12
+ ---
13
+
14
+ ## Entry Point & How to Run
15
+
16
+ `src/aetherscan/main.py` is the **primary** designated entry point for the pipeline. Non-development workflows should never call other scripts/modules directly — the one exception is `aetherscan-dashboard`, the console script for manual dashboard runs against a saved DB (see `dashboard_cli.py`). `main.py` dispatches to one of two subcommands via the first positional argument: `train` or `inference`.
17
+
18
+ There are two install paths off the same source tree:
19
+
20
+ | Path | Status | When | Launcher |
21
+ | ------------------------------------------- | ---------------------------------------------------------- | ----------------------------- | --------------------------------------------------------------------------- |
22
+ | **NGC container** (Apptainer/SingularityCE) | Canonical; runs on both clusters; only option on Blackwell | Default | `./utils/run_container.sh python -m aetherscan.main {train\|inference} ...` |
23
+ | **Conda env** | Alternative; **Ampere only** | When containers aren't usable | `PYTHONPATH=src python -m aetherscan.main {train\|inference} ...` |
24
+
25
+ CLI flags are identical between the two paths; only the launcher differs. `PYTHONPATH=src` makes the `aetherscan` package importable from `src/` without a `pip install -e .`; the container sets `PYTHONPATH` automatically.
26
+
27
+ - **Container build:** `singularity build aetherscan-ngc25.02.sif aetherscan.def` (or `apptainer build ...`) — same `aetherscan.def` recipe builds with either runtime. Build on the cluster you intend to run on.
28
+ - **Conda env:** `conda env create -f environment.yml && conda activate aetherscan`
29
+ - **`utils/fetch_run_outputs.sh`** rsyncs one run's outputs from remote cluster node(s) to the local `outputs/` tree, selecting files by the universal `*_<save_tag>.*` suffix and renaming each to `<machine>_<basename>` (collision-free across nodes). `<train|inference> <save_tag> <machine>...`; `--all` adds train checkpoints/archive, `--db` pulls the SQLite DB into `outputs/data/db/`, `--dry-run`. Per-run logs are tag-scoped (`logs/aetherscan_<save_tag>.log`, since PR #221), so the script picks each run's log up by its tag like every other output; the inference branch is provisional pending the inference pipeline.
30
+ - **`utils/kill_pipeline.sh`** stops a running pipeline (main process + all worker children) from a separate shell on the same machine — works for both run modes, finds the process tree itself, and tries a graceful SIGTERM (lets `ResourceManager` close pools/SHM) before escalating to SIGKILL. When no main process is found, sweeps `{round_data_root}/*/producer.pid` for orphaned `RoundDataProducer` trees left by an ungraceful main-process death and reaps them. Assumes a single running instance. `--force` / `--dry-run` / `--timeout N` / `--round-data-root DIR`.
31
+ - **`utils/run_container.sh`** auto-detects apptainer vs singularity (Apptainer wins when both present), sets `--nv` for GPU passthrough, auto-loads `<repo>/.env`, and bind-mounts the repo + `AETHERSCAN_{DATA,MODEL,OUTPUT}_PATH` 1:1 so absolute paths persisted in the DB stay valid across host and container. `AETHERSCAN_EXTRA_BINDS` (comma-separated host paths) appends additional 1:1 binds for data outside the standard dirs (e.g. raw `.h5` files under `/datag` for inference); the runtime's native `SINGULARITY_BIND` / `APPTAINER_BIND` still pass through and are additive.
32
+ - **`utils/start_tmux_session.sh`** (optional) spins up a four-window tmux session — a single-pane `pipeline` working window plus three monitoring windows: `htop` (htop 75% / CPU-MEM ticker 25%), `nvidia-smi`, and `data` (four vertical panes: `/dev/shm`, then `tree` of data / models / outputs). Idempotent.
33
+
34
+ **Common invocations:**
35
+
36
+ ```bash
37
+ # Train (container, canonical)
38
+ ./utils/run_container.sh python -m aetherscan.main train --save-tag train
39
+
40
+ # Train (conda source, Ampere)
41
+ PYTHONPATH=src python -m aetherscan.main train --save-tag train
42
+
43
+ # Inference from a pre-processed .npy
44
+ ./utils/run_container.sh python -m aetherscan.main inference \
45
+ --test-files real_filtered_LARGE_test_HIP15638.npy \
46
+ --encoder-path /path/to/vae_encoder.keras \
47
+ --rf-path /path/to/random_forest.joblib \
48
+ --config-path /path/to/config.json
49
+
50
+ # Inference from raw .h5 (triggers energy-detection preprocessing) — bind
51
+ # extra host paths if the .h5 files live outside the standard bind mounts
52
+ AETHERSCAN_EXTRA_BINDS=/datag ./utils/run_container.sh python -m aetherscan.main inference \
53
+ --inference-files complete_cadences_catalog.csv \
54
+ --encoder-path /path/to/vae_encoder.keras \
55
+ --rf-path /path/to/random_forest.joblib \
56
+ --config-path /path/to/config.json
57
+ ```
58
+
59
+ `--inference-files` (raw `.h5` catalog) triggers the energy-detection preprocessing pipeline and takes precedence over `--test-files` (pre-processed `.npy`).
60
+
61
+ ---
62
+
63
+ ## Configuration & CLI
64
+
65
+ Hierarchical, dataclass-based config with a thread-safe singleton. Resolution order at command time:
66
+
67
+ 1. **Defaults** — in `src/aetherscan/config.py`
68
+ 2. **Environment variables** — for paths and secrets
69
+ 3. **CLI flags** — override both on startup
70
+
71
+ At runtime, the singleton `Config` is read via `get_config()` and may be modified programmatically. See `docs/CONFIG_AND_CLI.md`.
72
+
73
+ **Secrets & paths** come from a `.env` file at the repo root (gitignored). Shell `export` takes precedence over `.env`. The container wrapper forwards `SLACK_*`, `AETHERSCAN_*`, and `HF_TOKEN` via `--env`; the source path loads the **whole** `.env` into `os.environ` at the top of `main.py` via python-dotenv.
74
+
75
+ ```ini
76
+ # .env example
77
+ # Defaults to /datax/scratch/zachy/{data|models|outputs}/aetherscan; CLI flags override
78
+ AETHERSCAN_DATA_PATH=/path/to/data
79
+ AETHERSCAN_MODEL_PATH=/path/to/models
80
+ AETHERSCAN_OUTPUT_PATH=/path/to/outputs
81
+ # Optional: comma-separated extra host paths for run_container.sh to bind 1:1
82
+ AETHERSCAN_EXTRA_BINDS=/extra/host/paths
83
+ # Only needed for uploading model weights to the HuggingFace Hub (train --hf-upload);
84
+ # downloads (the inference default) hit a public repo and need no token
85
+ HF_TOKEN=your-huggingface-write-token
86
+ # Slack integration auto-disables if unset
87
+ SLACK_BOT_TOKEN=your-slack-bot-token
88
+ SLACK_CHANNEL=your-slack-channel
89
+ ```
90
+
91
+ **The CLI Reference in `README.md` is a tight source↔doc contract.** The three code blocks under `## CLI Reference` (Top-Level / Train / Inference Help) are pasted-verbatim argparse output. If `src/aetherscan/cli.py` changes (flags, help strings, subparsers), regenerate them from the repo root with:
92
+
93
+ ```bash
94
+ PYTHONPATH=src python utils/print_cli_help.py all
95
+ ```
96
+
97
+ `print_cli_help.py` imports only `aetherscan.config` and `aetherscan.cli` (pure stdlib, no TensorFlow/conda needed) and pins `COLUMNS=80` for deterministic wrapping. Replace each block verbatim, preserving each subsection's "Regenerate this output with ..." intro paragraph.
98
+
99
+ ---
100
+
101
+ ## Project Structure
102
+
103
+ The tree below annotates the **source** layout. For the complete repository structure — root-level build/config files (`pyproject.toml`, `environment.yml`, `aetherscan.def`, `requirements-container.txt`, `.pre-commit-config.yaml`), governance docs (`CLAUDE.md`, `CONTRIBUTING.md`, `SECURITY.md`, `KNOWN_ISSUES.md`, `AI_POLICY.md`), and the `.claude/` and `.github/` directories — see the Project Structure tree in `CONTRIBUTING.md` (the canonical source).
104
+
105
+ ```
106
+ src/aetherscan/
107
+ ├── main.py # Entry point, command dispatch, GPU strategy setup (NCCL + fallback)
108
+ ├── cli.py # Argument parsing, validation, config override
109
+ ├── config.py # Configuration dataclasses & defaults (singleton)
110
+ ├── train.py # Training orchestration, curriculum learning, checkpointing
111
+ ├── round_data.py # Disk-backed (memmap) round datasets + background producer process
112
+ ├── run_state.py # Persisted training-run manifest (stage-aware resume)
113
+ ├── inference.py # Inference orchestration, candidate detection
114
+ ├── inference_viz.py # End-of-run inference visualization suite
115
+ ├── candidate_figures.py # Per-candidate figure renderer (TF-free; forkserver pool; called by inference_viz.py)
116
+ ├── preprocessing.py # Loading / downsampling / log-normalization + energy detection
117
+ ├── pfb.py # PFB static passband equalization (bandpass flattening)
118
+ ├── data_generation.py # Synthetic signal injection — batched memmap workers + background producer
119
+ ├── seeding.py # Root-seed stream derivation (reproducible train + inference runs)
120
+ ├── benchmark.py # Always-on stage timing to the pipeline_stages table
121
+ ├── dashboard.py # Streamlit live-monitoring dashboard (DB-driven)
122
+ ├── dashboard_launcher.py # Spawns the headless dashboard subprocess (guarded)
123
+ ├── dashboard_cli.py # Console entry point for manual dashboard runs (aetherscan-dashboard)
124
+ ├── hf_hub.py # HuggingFace Hub artifact upload/download
125
+ ├── tag_guards.py # Fail-early --save-tag dedup guards
126
+ ├── rf_metrics.py # Pure RF eval-metric helper (persisted to training_stats by train.py)
127
+ ├── shap_parallel.py # RF SHAP process-pool wrapper (TF-free; called by train.py)
128
+ ├── latent_variants.py # Latent-representation variant catalogue + selection/calibration (TF-free; shared by train.py + inference.py)
129
+ ├── latent_gif.py # Process-parallel latent-GIF frame renderer (TF-free; called by train.py)
130
+ ├── models/{vae,random_forest}.py
131
+ ├── db/db.py # Thread-safe SQLite, async queue-based writes, schema migration, supersede semantics
132
+ ├── logger/ # Multi-handler logging + Slack integration
133
+ ├── monitor/monitor.py # Background resource monitoring (CPU, RAM, GPU)
134
+ └── manager/manager.py # Resource lifecycle management (pools, shared memory)
135
+ utils/ # benchmark_report.py, fetch_run_outputs.sh,
136
+ # find_optimal_configs.py, get_system_info.sh,
137
+ # hf_tag_release.py, kill_pipeline.sh, print_cli_help.py,
138
+ # run_container.sh, start_tmux_session.sh,
139
+ # verify_train_test_files.py
140
+ docs/ # Full technical doc suite, one doc per pipeline surface —
141
+ # indexed in docs/README.md; start at docs/ARCHITECTURE.md
142
+ tests/ # Pytest suite: unit/ (CI surface) + gpu/cluster-marked
143
+ # integration/ smokes — see the "Testing" section below
144
+ benchmarks/ # Standalone benchmarks — CPU micro-benchmarks + a GPU
145
+ # benchmark (bench_gpu.py, container-only); not collected
146
+ # by pytest. See benchmarks/README.md + docs/BENCHMARKING.md
147
+ ```
148
+
149
+ ---
150
+
151
+ ## Architecture Patterns (load-bearing)
152
+
153
+ - **Distributed training/inference** — Gradients sync via TF `MirroredStrategy` + NCCL AllReduce, with gradient accumulation for larger effective batches under low VRAM. All TensorFlow model ops **must** occur within `strategy.scope()`.
154
+ - **Cadence-aware composite loss** — beta-VAE reconstruction + β-weighted KL divergence + α-weighted true/false clustering (ON-ON / OFF-OFF proximity, ON-OFF separation for true signals; uniform for false).
155
+ - **Curriculum training** — progressive SNR difficulty with adaptive LR that decays on validation plateaus and resets each round; per-round checkpointing. A persisted run manifest (`run_state_{save_tag}.json`) drives fault-tolerant resume: an explicit stage machine (vae_rounds → vae_plots → rf_train → rf_plots → final_save) skips completed stages, and stale DB rows from failed attempts are marked superseded (never deleted).
156
+ - **Thread-safe singletons** — `Config`, `Database`, `ResourceManager`. Always use the accessors `get_config()`, `get_db()`, `get_manager()`; never instantiate directly.
157
+ - **Shared-memory zero-copy parallelism** — worker pools communicate via shared memory (no serialization). Allocate via `manager.create_shared_memory()`; ResourceManager owns cleanup. Only the **creator** may call `shm.unlink()`, never workers. Training-round datasets are disk-backed memmaps (`round_data.py`): workers write disjoint row ranges in-place, eliminating per-sample IPC; steady-state reads come from page cache.
158
+ - **Data holders** — `TrainDataHolder` / `VizDataHolder` (`train.py`) wrap memmap references (or arrays) with a lock. RF training reuses `TrainDataHolder` via `prepare_distributed_train_dataset`. Call `holder.clear()` after processing completes. (Inference encodes directly from numpy slices since #298 — no holder on that path.)
159
+ - **Background data producer** — `RoundDataProducer` (spawn-started process with its own worker pool) generates round k+1 while round k trains; registered with ResourceManager as a `ManagedProcess`. `CUDA_VISIBLE_DEVICES` is blanked so the producer tree never initializes CUDA; logging crosses the spawn boundary via a `QueueListener` relay.
160
+ - **Worker cleanup** — custom SIGTERM handlers free resources on interruption. **Never log inside SIGTERM handlers** (deadlock risk).
161
+
162
+ ---
163
+
164
+ ## Code Style & Conventions
165
+
166
+ Enforced by **ruff** (lint + format) via pre-commit; full config in `pyproject.toml` under `[tool.ruff]`.
167
+
168
+ - **Target**: Python 3.10 (lowest common denominator across the conda 3.10 and container 3.12 paths). **Line length 100** (formatter wraps; `E501` is intentionally ignored).
169
+ - **Modern typing** — every module starts with `from __future__ import annotations` (isort's `required-imports`/`I002` auto-inserts it). Use PEP 604 unions (`X | None`, not `Optional[X]`) and PEP 585 generics (`list[int]`, `dict[str, float]`, not `typing.List`/`Dict`). Annotate args **and** return types. The `UP` family auto-fixes legacy idioms.
170
+ - **Docstrings** — short, plain prose. No Sphinx/Google/Numpy section markers. One-liners are fine for self-evident helpers.
171
+ - **Logging** — `logger = logging.getLogger(__name__)`, f-strings for messages. `T20` rejects bare `print()` outside one-off `utils/` scripts (and the self-logging `slack_handler.py`); `G001`–`G003` reject `%`/`str.format()`/`+` pre-formatted log messages. The Slack handler attaches automatically when `SLACK_BOT_TOKEN` is set, so anything at `INFO+` may surface in Slack — keep messages information-dense and **free of secrets**.
172
+ - **Config access** — `get_config()` returns `Config | None`; the canonical idiom guards `if config is None: raise ValueError(...)` (None only happens if `init_config()` hasn't run — a programming error).
173
+ - **Dataclass mutable defaults** — always `field(default_factory=...)`, never a bare `[...]` (shared mutable state; `B`/bugbear flags it).
174
+ - **Retry/error-handling** — pipeline retry loops catch `KeyboardInterrupt` separately and re-raise, log with `logger.error`, then either retry after `time.sleep(retry_delay)` or `sys.exit(1)`. Resume is manifest-driven (no checkpoint hunting): the `TrainingRunState` manifest tells the new pipeline which rounds/stages already completed. Non-critical stages (plots) record failures without forcing a retry; `main.py` exits nonzero if they never recover. Reference: `train_command` / `inference_command`, `run_state.py`.
175
+ - **Naming** — descriptive full words (`num_training_rounds`, not `n`/`bs`). Single letters only in tight loops / math / indexing.
176
+
177
+ | Element | Convention | Example |
178
+ | ------------- | ----------- | ------------------------ |
179
+ | Classes | PascalCase | `DataGenerator` |
180
+ | Functions | snake_case | `run_training_pipeline` |
181
+ | Constants | UPPER_SNAKE | `MAX_RETRIES` |
182
+ | Private | \_prefix | `_init_worker` |
183
+ | Config fields | snake_case | `per_replica_batch_size` |
184
+
185
+ **Grep-friendly inline comment markers** (used consistently): `# TODO:` (actionable work), `# NOTE:` (rationale/question), `# FIX:` (known issue, no time now), `# BUG:` (known bug, often with workaround), `# TEST:` (behavior to verify — now backed by the `tests/` suite). Prefer `# NOTE:` over `# TODO:` when there's no obvious action.
186
+
187
+ ---
188
+
189
+ ## Testing
190
+
191
+ The `tests/` suite splits along a hardware axis:
192
+
193
+ - **`tests/unit/`** — fast, hardware-independent, one `test_<module>.py` per source module. This is the CI surface; everything here must pass on a CPU-only runner.
194
+ - **`tests/integration/`** — `gpu`/`cluster`-marked tests that need real GPUs and cluster-resident data/models; not run in CI. Two end-to-end smokes (`test_train_smoke.py`, `test_inference_smoke.py`) launch `python -m aetherscan.main ...` as a real subprocess (hours of wall time each); the model-behavior gate (`test_model_behavior.py`, issue #139 Gate 2) instead drives generation and scoring in-process against the persisted VAE+RF (minutes, not hours).
195
+
196
+ **Default selection — matches what CI runs** (`.github/workflows/tests.yml`, on Python 3.10, 3.11, and 3.12), no GPUs or cluster data needed. CI adds an explicit `and not integration` as a defense-in-depth leak-guard (see the markers table below), so the exact CI expression is `pytest -m "not gpu and not cluster and not integration" -q`; today the two expressions select the same set because every `integration` test is also `gpu`+`cluster`.
197
+
198
+ ```bash
199
+ pytest -m "not gpu and not cluster" -q
200
+ ```
201
+
202
+ `pyproject.toml`'s `[tool.pytest.ini_options]` sets `pythonpath = ["src"]`, so **pytest needs no `PYTHONPATH=src` prefix** (unlike running `main.py` from source); it also sets `testpaths = ["tests"]` and `--strict-markers` (a typo'd marker is a collection error, not a silently-ignored one).
203
+
204
+ **Markers** (declared in `pyproject.toml`; `--strict-markers` rejects undeclared ones):
205
+
206
+ | Marker | Meaning | In default selection? |
207
+ | ------------- | ------------------------------------------------ | ----------------------- |
208
+ | `slow` | Slower CPU tests (e.g. builds real TF graphs) | **Yes** — CI runs them |
209
+ | `gpu` | Needs one or more physical GPUs | No |
210
+ | `cluster` | Needs cluster-resident data/models (blpc3/bla0) | No |
211
+ | `integration` | End-to-end subprocess runs; **skips isolation** | No — also `gpu`+`cluster`; CI excludes by marker too as a leak-guard |
212
+
213
+ **Isolation.** The autouse `aetherscan_isolated_env` fixture in `tests/conftest.py` wraps every non-integration test: it points `AETHERSCAN_{DATA,MODEL,OUTPUT}_PATH` at a fresh `tmp_path` tree, deletes `SLACK_BOT_TOKEN`/`SLACK_CHANNEL` (tests must never talk to Slack), resets all five singletons (`Config`, `Database`, `Logger`, `ResourceManager`, `ResourceMonitor`) via their `_reset()` hooks, then on teardown stops any leaked background threads/pools and restores the snapshotted SIGINT/SIGTERM handlers and stdout/stderr. Net effect: tests never touch real data and can't leak state into one another. Integration tests are exempt — they inherit the real environment and run the pipeline as a subprocess.
214
+
215
+ **Discipline.** Run the suite (or the subset you can) before claiming a change works, and **ship unit tests with new logic** — every behavior change should land tests under `tests/unit/` in the matching `test_<module>.py` (create it if the module is new).
216
+
217
+ **Gotcha.** Most unit modules import TensorFlow at collection time, so a bare `pytest` needs the full dependency stack (CI installs `tensorflow-cpu==2.17.*` plus the container requirements). If that stack isn't available locally, run the TF-free subset you can — e.g. `pytest tests/unit/test_config.py -q` — and **say exactly what you ran** rather than claiming the whole suite passed.
218
+
219
+ Deep dive: `docs/TESTING.md` covers the full layout, the synthetic data factories, the coverage-and-deliberate-gaps notes (`logger` / `slack_handler` / `benchmark` stage-timing wiring), CI specifics, how to run the cluster smokes, and the adding-tests checklist.
220
+
221
+ ---
222
+
223
+ ## Contribution Workflow
224
+
225
+ > **All issues are actionable, and all PRs must be tied to an existing issue.** Read `AI_POLICY.md` before doing AI-assisted work — the project has strict AI-usage rules.
226
+
227
+ 1. **Discussion first** — check for existing PRs/issues/discussions; otherwise open a [GitHub Discussion](https://github.com/zachtheyek/Aetherscan/discussions) or Slack thread. "Drive-by" issues with no prior discussion may be closed.
228
+ 2. **Open an issue** via the template; Claude auto-triages and labels it.
229
+ 3. **Feature branch** — `category/description` with prefix: `feature/` (new functionality), `hotfix/` (bug fixes), `misc/` (housekeeping), `claude/` (reserved for the Claude assistant).
230
+ 4. **Implement** — focused commits, pass all pre-commit hooks, follow `pyproject.toml` style.
231
+ 5. **PR** — rebase (not merge) onto `master`; **all commits need verified GPG signatures**; fill the PR template; link the issue via the Development sidebar or `Closes #N` / `Fixes #N` (enables label sync). PRs not tied to an issue may be closed.
232
+ 6. **Review** — needs passing checks, ≥1 maintainer approval, all conversations resolved, branch up to date. Approvals are voided when new commits are pushed. Claude provides an initial review automatically.
233
+
234
+ **Invoking vs. mentioning the assistant.** The assistant workflow (`claude.yml`) triggers whenever the assistant handle — an `@` immediately followed by `claude` — appears in the title/body of a Discussion, issue, or PR (or a comment on one). Write it only when you actually want to summon the assistant (e.g. an auto-filed docs issue asking it to open a PR). To refer to the handle as plain text anywhere else — a PR description, issue body, commit message, review comment — write it as `"@ claude"` (a space after the `@`, double quotes on both sides) so the `contains(…, '@claude')` trigger can't match. Tagging it unintentionally spawns a spurious assistant run and follow-up PR (this is what happened around issue #83).
235
+
236
+ **Responding to the automated review.** Opening (or marking ready) a PR triggers `claude-code-review.yml`, which posts a first-pass review with inline comments (catalogued in `docs/GITHUB_AUTOMATION.md`). Treat it as input, not verdict: wait for the review to land, then work through each comment individually, weighing it against your own understanding of the codebase and the change you actually made — don't assume the reviewer is right. Where a comment exposes a genuine blind spot, fix it in a focused, self-contained commit pushed to the *same* PR; where you're convinced it's wrong, leave the code untouched and be ready to explain concretely why. Then post a single PR comment covering both halves — first the points you addressed (what you changed and the rationale), then the points you think the reviewer got wrong (with your reasoning) — and close that comment by deliberately tagging the assistant handle to kick off a second-pass review. This is precisely the "you actually want to summon it" case from the paragraph above, not a violation of the don't-tag-unintentionally rule. Then repeat the loop — wait, read, validate, address, rebut, comment, re-invoke — until the reviews either come back clean (no further notes / LGTM) or they start drifting out of scope (raising points unrelated to the PR's theme) or turn nonsensical. At that stopping point, post a comment explaining why you're stopping, and do **not** tag the assistant handle again.
237
+
238
+ **Pre-commit hooks** (`pre-commit install` to activate): `ruff` (lint, `--fix`), `ruff-format`, general `pre-commit-hooks` (large files >1 MB, case conflict, merge conflict, YAML/TOML syntax, EOF/trailing-whitespace, private-key detection, `no-commit-to-branch` on master), and `gitleaks` (secret detection). Ruff-format auto-reformats on commit — **re-run `git add` after** it modifies files, then commit again. Bypass only sparingly with `git commit --no-verify`.
239
+
240
+ ```bash
241
+ pre-commit run # staged files
242
+ pre-commit run --all-files # everything
243
+ pre-commit run ruff --all-files
244
+ ```
245
+
246
+ ---
247
+
248
+ ## Security
249
+
250
+ - **Never commit secrets** (tokens, credentials, private data, internal URLs/IPs). Use `.env` (gitignored). `gitleaks` pre-commit hook + GitHub Dependabot back this up but aren't foolproof.
251
+ - **Secrets in use**: `SLACK_BOT_TOKEN` (Slack alerts/notifications); `HF_TOKEN` (HuggingFace Hub upload via `train --hf-upload` — inference downloads hit a public repo and need no token). Use separate dev/prod tokens; store via a secrets manager or restricted-permission encrypted env files.
252
+ - **If a token leaks** — rotate immediately. Slack: revoke in [Slack API](https://api.slack.com/apps) → OAuth & Permissions, reinstall with scopes `channels:read, chat:write, files:write, groups:read, incoming-webhook`, update `SLACK_BOT_TOKEN` everywhere, verify with `PYTHONPATH=src python utils/print_cli_help.py train` (no Slack errors). HuggingFace: invalidate/delete the token at [huggingface.co/settings/tokens](https://huggingface.co/settings/tokens), create a replacement (**write** scope only if you upload), update `HF_TOKEN` everywhere — full steps in `SECURITY.md`.
253
+ - **Incident response**: Contain (revoke creds) → Assess → Notify → Remediate (rotate secrets) → Document → Improve.
254
+ - **Reporting**: non-critical → [GitHub Discussion](https://github.com/zachtheyek/Aetherscan/discussions) with the "security" label; critical → contact [@zachtheyek](https://breakthroughlisten.slack.com/archives/D01SJG0L0TE) on Slack directly (do **not** open a public issue), expect a response in 48–72h.
255
+ - **Data security**: major outputs (weights, code, search results, training/inference data) are publicly disclosed via HuggingFace / GitHub / publications / [BL Open Data Archive](https://breakthroughinitiatives.org/opendatasearch); intermediate products (DB records, plots) stay on access-controlled HPC servers.
256
+ - **Dependency versions**: when bumping a dep, don't chase the latest — target the **newer** of {two minors below the latest stable, the latest stable ≥6 months old}, stable releases only (no alpha/beta/rc/nightly). A known advisory on that target overrides the lag → jump to the minimum patched version. Never cross a documented ceiling (`numpy<2.0`, `setuptools<81`) or the NGC TF 2.17 ABI, and keep `environment.yml` / `requirements-container.txt` / `aetherscan.def` / `pyproject.toml` in lockstep for shared deps. Full policy in `SECURITY.md` → Security Scanning → Version Selection Policy.
257
+ - False positives: add `file:line` to `.gitleaksignore` or inline `# gitleaks:allow` (less preferred).
258
+
259
+ ---
260
+
261
+ ## Reference Files
262
+
263
+ Paths relative to the repo root:
264
+
265
+ - `CLAUDE.md` — condensed always-on agent rules (this skill is its deep-dive companion)
266
+ - `README.md` — overview, install matrix, usage examples, full CLI reference
267
+ - `CONTRIBUTING.md` — workflow, project structure, code style, pre-commit
268
+ - `SECURITY.md` — security policy, secrets management, token rotation
269
+ - `KNOWN_ISSUES.md` — known bugs and workarounds
270
+ - `AI_POLICY.md` — AI usage policy (read before AI-assisted contributions)
271
+ - `docs/README.md` — index of the technical documentation suite (one doc per surface)
272
+ - `docs/ARCHITECTURE.md` — system map: data model, module map, init order, artifact layout
273
+ - `docs/TRAINING_PIPELINE.md` — rounds, round data + producer, retries, every training plot
274
+ - `docs/INFERENCE_PIPELINE.md` — catalogs, streaming flow, manifest retries, inference figures
275
+ - `docs/PREPROCESSING.md` — energy detection math (PFB/spline, k² derivation), signal injection
276
+ - `docs/MODELS.md` — Beta-VAE architecture/loss math, RF features + threshold semantics
277
+ - `docs/DATABASE.md` — schema, writer thread, flush/supersede protocols, migrations
278
+ - `docs/RUNTIME_SERVICES.md` — logger/Slack, ResourceManager lifecycle, resource monitor
279
+ - `docs/TESTING.md` — suite layout, markers, isolation fixtures, CI, cluster smokes
280
+ - `docs/GITHUB_AUTOMATION.md` — every workflow, dedup guards, assistant-handle rules
281
+ - `docs/RELEASE.md` — version-coupling contract, CD gates, release runbook
282
+ - `docs/GPU_RUNTIME_GUIDE.md` — container build/runtime runbook
283
+ - `docs/CONFIG_AND_CLI.md` — config system deep dive
@@ -0,0 +1,33 @@
1
+ ---
2
+ name: Bug report
3
+ about: Create a report to help us improve
4
+ title: "[BUG]"
5
+ labels: bug
6
+ assignees: ""
7
+ ---
8
+
9
+ **Describe the bug**
10
+ A clear and concise description of what the bug is.
11
+
12
+ **To reproduce**
13
+ Steps to reproduce the behavior:
14
+
15
+ 1. Go to '...'
16
+ 2. Click on '....'
17
+ 3. Scroll down to '....'
18
+ 4. See error
19
+
20
+ **Expected behavior**
21
+ A clear and concise description of what you expected to happen.
22
+
23
+ **Screenshots**
24
+ If applicable, add screenshots to help explain your problem.
25
+
26
+ **System information**
27
+ Run `utils/get_system_info.sh` and append the outputs as attachments
28
+
29
+ **Additional context**
30
+ Add any other context about the problem here.
31
+
32
+ **Discussion links**
33
+ All issues are required to have at least one prior discussion with a maintainer, either via GitHub discussions or Slack (as per CONTRIBUTING.md). Please link to the associated discussion(s) here for future reference
@@ -0,0 +1,22 @@
1
+ ---
2
+ name: Feature request
3
+ about: Suggest an idea for this project
4
+ title: "[FEATURE]"
5
+ labels: enhancement
6
+ assignees: ""
7
+ ---
8
+
9
+ **Is your feature request related to a problem? Please describe.**
10
+ A clear and concise description of what the problem is. E.g. I'm always frustrated when [...]
11
+
12
+ **Describe the solution you'd like**
13
+ A clear and concise description of what you want to happen.
14
+
15
+ **Describe alternatives you've considered**
16
+ A clear and concise description of any alternative solutions or features you've considered.
17
+
18
+ **Additional context**
19
+ Add any other context or screenshots about the feature request here.
20
+
21
+ **Discussion links**
22
+ All issues are required to have at least one prior discussion with a maintainer, either via GitHub discussions or Slack (as per CONTRIBUTING.md). Please link to the associated discussion(s) here for future reference
@@ -0,0 +1,51 @@
1
+ # Automatically designate the author of an issue/PR as the assignee
2
+ # Note, this does not prevent more assignees from being added in the future
3
+ name: Auto-assign author
4
+
5
+ on:
6
+ issues:
7
+ types: [opened]
8
+ pull_request:
9
+ types: [opened]
10
+
11
+ permissions:
12
+ issues: write
13
+ pull-requests: write
14
+
15
+ jobs:
16
+ assign-author:
17
+ runs-on: ubuntu-latest
18
+
19
+ steps:
20
+ - name: Assign author to issue/PR
21
+ uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
22
+ with:
23
+ script: |
24
+ const sender = context.payload.sender;
25
+
26
+ // GitHub's REST API does not support assigning bot/agent accounts
27
+ // (e.g. GitHub Apps, Copilot/Claude agents) via App installation
28
+ // tokens, so skip the assignment when the sender is not a human user.
29
+ if (sender.type !== "User") {
30
+ core.info(`Skipping assignment: sender ${sender.login} has type "${sender.type}"`);
31
+ return;
32
+ }
33
+
34
+ const issueNumber = context.eventName === "issues"
35
+ ? context.payload.issue.number
36
+ : context.payload.pull_request.number;
37
+
38
+ try {
39
+ await github.rest.issues.addAssignees({
40
+ owner: context.repo.owner,
41
+ repo: context.repo.repo,
42
+ issue_number: issueNumber,
43
+ assignees: [sender.login],
44
+ });
45
+ } catch (error) {
46
+ // Defense-in-depth: if GitHub still rejects the assignment (e.g.
47
+ // an agent identity slips past the type check above), log it
48
+ // rather than failing the workflow — the issue/PR was created
49
+ // successfully and assignment is a non-critical convenience.
50
+ core.warning(`Could not assign ${sender.login}: ${error.message}`);
51
+ }
@@ -0,0 +1,78 @@
1
+ # Auto review every PR (once on open, once when marked as "ready for review")
2
+ name: Claude Code Review
3
+
4
+ on:
5
+ pull_request:
6
+ types: [opened, ready_for_review]
7
+
8
+ jobs:
9
+ claude-code-review:
10
+ runs-on: ubuntu-latest
11
+ # Reviewing a PR diff with inline comments scales with PR size but shouldn't exceed 20 mins
12
+ # (refine if data shows otherwise)
13
+ timeout-minutes: 20
14
+ permissions:
15
+ contents: read
16
+ pull-requests: write
17
+ issues: write
18
+ id-token: write
19
+
20
+ steps:
21
+ - name: Checkout repository
22
+ uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
23
+ with:
24
+ fetch-depth: 1
25
+
26
+ - name: Run Claude Code Review
27
+ id: claude-review
28
+ uses: anthropics/claude-code-action@5d5c10a4f389689f992ea10bb14dcb6fcc83146d # v1.0.102
29
+ with:
30
+ claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
31
+ allowed_bots: "claude[bot]"
32
+ track_progress: true
33
+ prompt: |
34
+ ## Role
35
+ You are an automated code reviewer for the pull request below. Provide feedback on the changes — do not modify them.
36
+
37
+ ## Context
38
+ REPO: ${{ github.repository }}
39
+ PR NUMBER: ${{ github.event.pull_request.number }}
40
+ PR TITLE: ${{ github.event.pull_request.title }}
41
+ PR BODY: ${{ github.event.pull_request.body }}
42
+ PR AUTHOR: ${{ github.event.pull_request.user.login }}
43
+
44
+ ## Task
45
+ Please review this pull request and provide feedback on:
46
+ - Code quality and best practices
47
+ - Potential bugs or issues
48
+ - Performance considerations
49
+ - Security implications
50
+ - Test coverage
51
+
52
+ ## Available tools
53
+ PR interaction:
54
+ - `gh pr diff` to get the PR diff
55
+ - `gh pr view` to get PR metadata
56
+ - `gh pr comment` to post top-level feedback
57
+ - `mcp__github_inline_comment__create_inline_comment` to post inline code comments
58
+
59
+ Issue lookup:
60
+ - `gh issue view` to look up linked issues (primary use) or related issues if necessary
61
+
62
+ Git inspection:
63
+ - `git fetch origin <base-branch>` to fetch the base branch before diffing against it
64
+ - `git diff` to compare code changes between commits (e.g. `git diff origin/<base-branch>...HEAD`)
65
+ - `git log` to inspect commit history
66
+
67
+ Code exploration:
68
+ - `Read`, `Glob`, `Grep` to explore the codebase
69
+
70
+ ## Guidelines
71
+ - See CLAUDE.md for context about the repository
72
+ - Only post GitHub comments — don't submit review text as messages
73
+ - Be constructive and helpful in your feedback
74
+ - Your only responsibilities are to review the current PR. Do not do anything that is not within this job scope.
75
+
76
+ claude_args: |
77
+ --allowedTools "Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr comment:*),mcp__github_inline_comment__create_inline_comment,Bash(gh issue view:*),Bash(git fetch:*),Bash(git diff:*),Bash(git log:*),Read,Glob,Grep"
78
+ --model opus
@@ -0,0 +1,136 @@
1
+ # Enforce contribution rules: PRs need linked issues, issues need prior discussions
2
+ # Handles both existence & correctness
3
+ name: Claude Contribution Check
4
+
5
+ on:
6
+ issues:
7
+ types: [opened]
8
+ pull_request:
9
+ types: [opened]
10
+
11
+ permissions:
12
+ contents: read
13
+ pull-requests: write
14
+ issues: write
15
+ id-token: write
16
+
17
+ jobs:
18
+ contribution-check:
19
+ # Never run on bot- or automation-authored issues/PRs. Our own workflows (e.g.
20
+ # claude-update-docs) file issues as the `claude` bot; without this guard the job would spin
21
+ # up a runner and claude-code-action would abort with a "non-human actor" error — a red
22
+ # failure on every bot issue. Skip the whole job instead. (allowed_bots is already unset, so
23
+ # no comment is ever posted; this additionally avoids the noisy failed run.) See issue #83.
24
+ if: >-
25
+ github.event.issue.user.type != 'Bot' &&
26
+ github.event.pull_request.user.type != 'Bot' &&
27
+ !endsWith(github.actor, '[bot]') &&
28
+ github.actor != 'claude'
29
+ runs-on: ubuntu-latest
30
+ # Contribution check shouldn't take more than 5 mins (refine if data shows otherwise)
31
+ timeout-minutes: 5
32
+
33
+ steps:
34
+ - name: Checkout repository
35
+ uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
36
+ with:
37
+ fetch-depth: 1
38
+
39
+ - name: Run Claude Contribution Check
40
+ uses: anthropics/claude-code-action@5d5c10a4f389689f992ea10bb14dcb6fcc83146d # v1.0.102
41
+ with:
42
+ claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
43
+ prompt: |
44
+ ## Role
45
+ You are a contribution compliance checker. Your job is to verify that contributions follow the project's workflow rules.
46
+
47
+ ## Context
48
+ REPO: ${{ github.repository }}
49
+ EVENT: ${{ github.event_name }}
50
+ ACTION: ${{ github.event.action }}
51
+
52
+ ## Task
53
+ ### If this is a pull_request event:
54
+
55
+ PR NUMBER: ${{ github.event.pull_request.number }}
56
+ PR TITLE: ${{ github.event.pull_request.title }}
57
+ PR BODY: ${{ github.event.pull_request.body }}
58
+ PR AUTHOR: ${{ github.event.pull_request.user.login }}
59
+
60
+ Check if this PR has a formally linked issue using the GitHub GraphQL API:
61
+ ```bash
62
+ gh api graphql -f query='
63
+ query {
64
+ repository(owner: "${{ github.repository_owner }}", name: "${{ github.event.repository.name }}") {
65
+ pullRequest(number: ${{ github.event.pull_request.number }}) {
66
+ closingIssuesReferences(first: 5) {
67
+ nodes { number title }
68
+ }
69
+ }
70
+ }
71
+ }
72
+ '
73
+ ```
74
+
75
+ If NO linked issues are found:
76
+ 1. Add the label `needs-issue` to the PR: `gh pr edit ${{ github.event.pull_request.number }} --add-label "needs-issue"`
77
+ 2. Post a polite warning comment on the PR explaining that all PRs must be linked to an existing issue (via `Closes #N` or `Fixes #N` in the PR body, or via the "Development" sidebar). Reference the CONTRIBUTING.md guidelines.
78
+
79
+ If linked issues ARE found, verify correctness:
80
+ 1. View each linked issue (`gh issue view <number>`) and check that its title/description plausibly relates to the PR's title, description, and stated changes.
81
+ 2. If a linked issue appears unrelated to the PR (e.g. the PR is about fixing a typo but the linked issue is about a performance optimization), post a friendly comment noting the potential mismatch and asking the author to verify the correct issue is linked.
82
+ 3. If the PR body mentions issue numbers (e.g. "Related to #10") that are NOT formally linked via closing keywords, post a friendly reminder suggesting the author use `Closes #N` or `Fixes #N` to create a formal link.
83
+ 4. If all linked issues are relevant and correctly linked — do nothing, the PR is compliant.
84
+
85
+ ### If this is an issues event:
86
+
87
+ ISSUE NUMBER: ${{ github.event.issue.number }}
88
+ ISSUE TITLE: ${{ github.event.issue.title }}
89
+ ISSUE BODY: ${{ github.event.issue.body }}
90
+ ISSUE AUTHOR: ${{ github.event.issue.user.login }}
91
+
92
+ Search for related GitHub Discussions that may have preceded this issue:
93
+ ```bash
94
+ gh search issues --type discussion --repo ${{ github.repository }} "<relevant keywords from issue title>"
95
+ ```
96
+
97
+ Note that the repository's issue templates provide a designated "Discussion links" section for authors to link to relevant discussions in the issue body. These may be a good starting point, though you shouldn't assume they're always accurate due to the potential for human error.
98
+
99
+ If NO related discussions are found:
100
+ 1. Add the label `needs-discussion` to the issue: `gh issue edit ${{ github.event.issue.number }} --add-label "needs-discussion"`
101
+ 2. Post a friendly reminder comment explaining that issues should be preceded by a GitHub Discussion or Slack thread (per CONTRIBUTING.md). This is a gentle nudge, not a hard block.
102
+ 3. If the original issue body didn't include a Slack thread link under "Discussion links", add a note towards the end of your reminder stating that if the discussion happened on Slack rather than GitHub Discussions, this reminder may be safely ignored, but the issue author should link to the relevant Slack threads in the original issue body.
103
+ 4. If the original issue body DID include a Slack thread link under "Discussion links", do not assign the `needs-discussion` label, or post the reminder comment. Assume the issue is compliant.
104
+
105
+ If related discussions ARE found, but the original issue body has incorrect/incomplete links:
106
+ 1. Post a friendly reminder comment suggesting the issue author modify the original issue body with the correct links, then link to the correct links that you found.
107
+
108
+ If all related discussions found match the issues linked in the original issue body — do nothing, the issue is compliant.
109
+
110
+ ## Available tools
111
+ GitHub API:
112
+ - `gh api graphql` to query a PR's formally linked issues via `closingIssuesReferences` (the only reliable way to detect `Closes #N` / `Fixes #N` style links)
113
+
114
+ PR interaction (pull_request events):
115
+ - `gh pr edit --add-label` to apply the `needs-issue` label when a PR has no linked issue
116
+ - `gh pr comment` to post the missing-issue warning or a relevance-mismatch reminder
117
+
118
+ Issue interaction (issues events, plus `gh issue view` on PR events):
119
+ - `gh issue view` to inspect each linked issue referenced by a PR and check its relevance to the PR's stated changes
120
+ - `gh issue edit --add-label` to apply the `needs-discussion` label when a new issue lacks a prior discussion
121
+ - `gh issue comment` to post the missing-discussion reminder or a links-correction nudge
122
+
123
+ Search:
124
+ - `gh search issues --type discussion` to find prior GitHub Discussions related to a newly opened issue
125
+
126
+ Code exploration:
127
+ - `Read`, `Glob`, `Grep` to consult CONTRIBUTING.md, issue templates, or other repo context when assessing compliance
128
+
129
+ ## Guidelines
130
+ - Be concise in your comments (2-3 sentences max)
131
+ - Do NOT close or reject any PRs or issues — only warn and label
132
+ - Your only responsibilities are to enforce contribution compliance. Do not do anything that is not within this job scope.
133
+
134
+ claude_args: |
135
+ --allowedTools "Bash(gh api:*),Bash(gh pr edit:*),Bash(gh pr comment:*),Bash(gh issue view:*),Bash(gh issue edit:*),Bash(gh issue comment:*),Bash(gh search:*),Read,Glob,Grep"
136
+ --model opus