diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 5867e1cd8a..9c2e0cae2f 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -28,7 +28,6 @@
# Documentation and notebooks
/docs/** @stranske
-/Old/** @stranske
/notebooks/** @stranske
/*.ipynb @stranske
@@ -36,8 +35,7 @@
/.github/workflows/** @stranske
# Critical agent workflows require owner approval
-/.github/workflows/agents-63-chatgpt-issue-sync.yml @stranske
-/.github/workflows/agents-63-codex-issue-bridge.yml @stranske
+/.github/workflows/agents-63-issue-intake.yml @stranske
/.github/workflows/agents-70-orchestrator.yml @stranske
/Dockerfile @stranske
/docker-compose.yml @stranske
diff --git a/.github/workflows/README.md b/.github/workflows/README.md
index aef1f8856c..3f538086fa 100644
--- a/.github/workflows/README.md
+++ b/.github/workflows/README.md
@@ -19,7 +19,6 @@ Core layers:
- Governance & Health: `health-40-repo-selfcheck.yml`, `health-41-repo-health.yml`, `health-42-actionlint.yml`, `health-43-ci-signature-guard.yml`, `health-44-gate-branch-protection.yml`, labelers, dependency review, CodeQL.
- Keepalive heartbeat (`maint-keepalive.yml`): twice-daily cron + dispatch workflow that posts a timestamped comment (with run link) to the Ops heartbeat issue using `ACTIONS_BOT_PAT` and fails fast if either the issue variable or PAT is missing.
- Coverage guard (`maint-coverage-guard.yml`): daily cron + dispatch workflow that fetches the latest Gate coverage artifacts, compares them to the configured baseline, and maintains the rolling `[coverage] baseline breach` issue.
-- Path Labeling: `pr-path-labeler.yml` auto-categorizes PRs.
### 1.1 Current CI Topology (Issue #2439)
The CI stack now routes every pull request through a single Gate workflow that orchestrates the reusable CI and Docker checks:
diff --git a/.github/workflows/pr-00-gate.yml b/.github/workflows/pr-00-gate.yml
index 08bcdfe5d4..ed806aed7c 100644
--- a/.github/workflows/pr-00-gate.yml
+++ b/.github/workflows/pr-00-gate.yml
@@ -119,7 +119,7 @@ jobs:
with:
python-version: '3.11'
- run: python -m pip install --upgrade pip pytest requests
- - run: pytest tests/github_scripts
+ - run: pytest tests/workflows/github_scripts
docker-smoke:
name: docker smoke
diff --git a/.gitignore b/.gitignore
index e9b3593845..2e0fef1e7f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -62,6 +62,10 @@ results/
!tests/fixtures/keepalive_post_work/*.json
!tests/fixtures/agents_pr_meta/*.json
!tests/fixtures/orchestrator/*.json
+!tests/workflows/fixtures/keepalive/*.json
+!tests/workflows/fixtures/keepalive_post_work/*.json
+!tests/workflows/fixtures/agents_pr_meta/*.json
+!tests/workflows/fixtures/orchestrator/*.json
!tests/data/*.csv
# generated tear sheets
reports/*.png
diff --git a/AGENTS_APP.md b/AGENTS_APP.md
deleted file mode 100644
index 5fd7f37569..0000000000
--- a/AGENTS_APP.md
+++ /dev/null
@@ -1,6 +0,0 @@
-# Codex Work Instructions (App + Sim Layer)
-- Prefer calling `trend_analysis.pipeline.single_period_run` when available.
-- Expand tests first; each PR solves one issue.
-- How to run: `./scripts/run_streamlit.sh`, `pytest -q`.
-- Acceptance criteria: schema validator, policy engine behavior, simulator smoke test, pipeline integration parity within tolerance.
-- Backlog: preview score frame, weight heatmap, integrate native rank_selection after upstream merge, add expected shortfall & diversification value, export commit hash.
diff --git a/Agents.md b/Agents.md
index da5123db1b..61bada472e 100644
--- a/Agents.md
+++ b/Agents.md
@@ -1,763 +1,180 @@
# Agents.md
-"""
-YOU ARE CODEX. EXTEND THE VOL_ADJ_TREND_ANALYSIS PROJECT AS FOLLOWS
---------------------------------------------------------------------
-## Agents consumer workflows (historical)
+> **Canonical implementation roadmap**: See [docs/phase-2/Agents.md](docs/phase-2/Agents.md) for the complete Phase-2 implementation spec and step-by-step guidance.
-Manual consumer wrappers were fully retired once the orchestrator became the
-sole automation entry point. They linger only in git history alongside the
-ledger notes in [ARCHIVE_WORKFLOWS.md](docs/archive/ARCHIVE_WORKFLOWS.md), which documents
-their removal and the consolidated keep vs retire roster.
-
-## Communication guard-rails
-- When a request contains multiple tasks, explicitly recap which items are complete and which remain before handing control back.
-- Call out any suggestion that is only a partial fix, and list the follow-up steps required for it to succeed.
-- Highlight assumptions about workflow or automation status (e.g., scheduled runs, failing jobs) and offer to pull the live data when needed.
-
-## Demo pipeline (maintenance / CI)
-
-1. **Bootstrap environment**
-
- ```bash
- ./scripts/setup_env.sh
- ```
-
-2. **Generate demo dataset**
-
- ```bash
- python scripts/generate_demo.py
- ```
-
-3. **Run full demo pipeline and export checks**
-
- ```bash
- python scripts/run_multi_demo.py
- ```
-
- The script must invoke `export.export_data()` with the demo results so CSV,
- Excel, JSON **and TXT** outputs are generated in one call. Update it
- whenever new exporter functionality is added.
-
- When exporter features evolve (e.g. additional formats or option flags),
- extend both `run_multi_demo.py` and `config/demo.yml` so the demo pipeline
- exercises every new code path. This keeps CI in lock‑step with the live
- exporter behaviour.
-
-4. **Run the test suite**
- ```bash
- ./scripts/run_tests.sh
- ```
-
-5. **Keep demo config current**
- - Update `config/demo.yml` and demo scripts whenever export or pipeline
- behaviour changes so that the demo exercises all features.
-
-See **[docs/DemoMaintenance.md](docs/DemoMaintenance.md)** for a concise
-checklist of these steps.
-
-High‑level goal
-~~~~~~~~~~~~~~~
-Add a **performance‑based manager‑selection mode** that works alongside the
-existing 'all', 'random', and 'manual' modes. Make the pipeline fully
-config‑driven and keep everything vectorised.
-
-Functional spec
-~~~~~~~~~~~~~~~
-1. New selection mode keyword: `rank`.
- • Works on the *in‑sample* window **after** the usual data‑quality filters.
- • Supported inclusion approaches:
- - `'top_n'` – keep the N best funds.
- - `'top_pct'` – keep the top P percent.
- - `'threshold'` – keep funds whose score ≥ user threshold
- (this is the “useful extra” beyond N and percentile).
-
-2. Rank criteria (`score_by`):
- • Any single metric registered in `METRIC_REGISTRY`
- (e.g. 'Sharpe', 'AnnualReturn', 'MaxDrawdown', …).
- • Special value `'blended'` that combines up to three metrics with
- user‑supplied *positive* weights (weights will be normalised to 1.0).
-
-3. Direction‑of‑merit:
- • Metrics where “larger is better” → rank descending.
- • Metrics where “smaller is better” (currently **only** MaxDrawdown)
- → rank ascending. Future metrics can extend `ASCENDING_METRICS`.
-
-4. Config file (YAML) drives everything – sample below.
-
-5. UI flow (ipywidgets, no external deps):
- > Notebook widgets live in the optional module
- > `trend_analysis.ui.rank_widgets`. Import `build_ui()` from there (or use
- > the compatibility wrapper `trend_analysis.core.rank_selection.build_ui()`)
- > only when `ipywidgets` is available so that pipeline consumers stay
- > headless-friendly by default.
- Step 1 – Mode (‘all’, ‘random’, ‘manual’, **‘rank’**),
- checkboxes for “vol‑adj” and “use ranking”.
- Step 2 – If mode == 'rank' **or** user ticked “use ranking”
- → reveal controls for `inclusion_approach`,
- `score_by`, `N / Pct / Threshold`, and (if blended)
- three sliders for weights + metric pickers.
- Step 3 – If mode == 'manual'
- → display an interactive DataFrame of the IS scores so the
- user can override selection and set weights.
- Step 4 – Output format picker (csv / xlsx / json) then fire
- `run_analysis()` and `export_to_*`.
-
-6. No broken changes:
- • Default behaviour (config absent) must be identical to current build.
- • All heavy computation stays in NumPy / pandas vector land.
-
-7. Unit‑test hooks:
- • New pure functions must be import‑safe and testable without widgets.
- (e.g. `rank_select_funds()`).
-
-Sample YAML
-~~~~~~~~~~~
-selection:
- mode: rank # all | random | manual | rank
- random_n: 12 # only if mode == random
- use_vol_adjust: true
-rank:
- inclusion_approach: top_n # top_n | top_pct | threshold
- n: 8 # for top_n
- pct: 0.10 # for top_pct (decimal, not %)
- threshold: 1.5 # ignored unless approach == threshold
- score_by: blended # Sharpe | AnnualReturn | … | blended
- blended_weights:
- Sharpe: 0.5
- AnnualReturn: 0.3
- MaxDrawdown: 0.2
-output:
- format: excel # csv | excel | json
-"""
-
-"""
-🔄 2025‑06‑15 UPDATE — PHASE‑1 ENHANCEMENTS
-------------------------------------------
-• Blended ranking **must** use *z‑scores* (mean‑0, stdev‑1) before the
- weighted sum so metrics on different scales are commensurable.
-• MaxDrawdown is currently the only “smaller‑is‑better” metric; the
- ASCENDING_METRICS set remains {"MaxDrawdown"} until further notice.
-• Config format stays YAML.
-"""# agents.md
## Mission
-Converge the scattered modules into one fully‑test‑covered, vectorised pipeline that can be invoked from a single CLI entry‑point.
-Never touch notebooks living under any directory whose name ends in old/.
-
----
-
-## Automation entry points (Orchestrator → bridge → verification)
-
-### Agents 70 Orchestrator
-- **File**: `.github/workflows/agents-70-orchestrator.yml`.
-- **Triggers**: 20-minute `schedule` sweep plus manual `workflow_dispatch` with curated inputs for readiness, preflight, verification, watchdog, and keepalive paths.
-- **Role**: single automation front door. Each invocation delegates to `reusable-16-agents.yml`, merging booleans and JSON overrides into one payload so the reusable flow can run bootstrap, readiness, watchdog, verification, and keepalive passes in the same job graph.
-- **Manual run**: Actions → **Agents 70 Orchestrator** → **Run workflow**. Pass booleans as strings (`true`/`false`) for the built-in toggles and use the `options_json` input for advanced settings such as `enable_bootstrap`, additional keepalive tuning, or overriding valid assignees.
-
-### Agents 63 Issue Intake
-- **File**: `.github/workflows/agents-63-issue-intake.yml`.
-- **Triggers**: Issue events (`opened`, `labeled`, `reopened`) when the target carries either the `agent:codex` or `agents:codex` label, plus `workflow_dispatch` for manual Codex rehearsals and `workflow_call` for ChatGPT sync delegation.
-- **Role**: canonical automation front door. Label-triggered runs bootstrap Codex PRs (or invite flows) while manual/ChatGPT dispatches reuse the same normalization pipeline so everything flows through one job graph. Manual Codex runs accept the issue number and optional comment toggles; ChatGPT sync continues to provide topic inputs via the wrapper.
-
-### Agents 64 Verify Agent Assignment
-- **File**: `.github/workflows/agents-64-verify-agent-assignment.yml`.
-- **Triggers**: Exposes a reusable `workflow_call` contract (required `issue_number`, optional `valid_assignees`) alongside a manual `workflow_dispatch` for ad-hoc audits.
-- **Role**: Validates that an issue retains the `agent:codex` label and one of the accepted agent assignees, then publishes Markdown + JSON outputs consumed by the orchestrator. Manual runs mirror the same inputs—supply the issue number (and custom assignee list if needed) and review the generated run summary for status.
-- **Pass condition**: Returns `pass` when the target keeps `agent:codex` *and* at least one approved login (`copilot`, `chatgpt-codex-connector`, or `stranske-automation-bot`) is assigned. `valid_assignees` lets you override that list for edge cases.
-- **Pass signal**: The verify job writes `status: pass`, surfaces the matched login in both the step summary and the `matched_assignee` output, and the orchestrator emits a notice naming who satisfied the check. Scheduled runs show the same pass table; skipped runs only appear when the label is missing.
-- **Outputs**: The step summary and `matched_assignee` output both name the login that satisfied verification so orchestration logs highlight the successful actor during green runs.
-
-### 2025-06-27 UPDATE — RISK-METRICS EXPORT (SERIOUSLY, LEAVE THIS IN)
-
-Codex removed the pretty reporting layer once; it shall not happen again.
-Follow these guard-rails whenever you touch export logic.
-
-1. **Call the canonical exporters**
- After `pipeline.run()` completes, pipe the returned `Mapping[str, pd.DataFrame]`
- into **exactly one** of
- `trend_analysis.export.export_to_excel | export_to_csv | export_to_json`.
-
-2. **Sheet / file keys**
-
-3. **Excel format contract**
-* Generate the summary sheet formatter via
- `trend_analysis.export.make_summary_formatter(...)`.
-* Register any other sheet formatter with `@register_formatter_excel`
- so `export_to_excel` auto-hooks it.
-* Required cosmetics:
- - bold title row,
- - `0.00%` for CAGR & Vol,
- - `0.00` for Sharpe & Sortino,
- - red numerals for MaxDD,
- - freeze panes on header,
- - auto-filter,
- - column width = `max(len(header)) + 2`.
-
-4. **Column order = law**
-Tests must fail if this order mutates.
-
-5. **Config switches**
-`output.format` = `excel | csv | json`
-`output.path` = prefix used by exporter (Excel auto-appends `.xlsx`).
-
-6. **Tests**
-* In-memory smoke test: write to `BytesIO`, assert two sheets and cell
- `A1 == "Vol-Adj Trend Analysis"`.
-* Regression test: `assert list(df.columns) == EXPECTED_COLUMNS`.
-
-7. **Back-compat**
-Silent config = drop the fully formatted Excel workbook into `outputs/`
-exactly as v1.0 did. Breaking that throws `ExportError`.
-
-> 🛡️ If you rip out these formatters again, CI will chaperone you with a failing gate and a stern commit message.
-
-
-## | Layer / concern | **Canonical location** | Everything else is **deprecated** |
-| ------------------------------------ | -------------------------------------------------------------------------- | --------------------------------------------------------- |
-| **Data ingest & cleaning** | `trend_analysis/data.py` (alias exported as `trend_analysis.data`) | `data_utils.py`, helper code in notebooks or `scripts/` |
-| **Portfolio logic & metrics** | `trend_analysis/metrics.py` (vectorised) | loops inside `run_analysis.py`, ad‑hoc calcs in notebooks |
-| **Export / I/O** | `trend_analysis/export.py` | the root‑level `exports.py`, snippets inside notebooks |
-| **Domain kernels (fast primitives)** | `trend_analysis/core/` package | stand‑alone modules under the top‑level `core/` directory |
-| **Pipeline orchestration** | `trend_analysis/pipeline.py` (pure) | any duplicated control flow elsewhere |
-| **CLI entry‑point** | `run_analysis.py` **only** (thin wrapper around `trend_analysis.cli:main`) | bespoke `scripts/*.py` entry points |
-| **Config** | `config/defaults.yml` loaded through `trend_analysis.config.load()` | hard‑coded constants, magic numbers in notebooks |
-| **Tests** | `tests/` (pytest; 100 % branch‑aware coverage gate) | — |
-One concern → one module.
-Replacements must delete or comment‑out whatever they obsolete in the same PR.
-
-### 2025-09-18 UPDATE — SCALAR METRIC MEMOIZATION (ISSUE #1156)
-
-An opt-in memoization layer now caches scalar per‑fund metric series accessed via `WindowMetricBundle.ensure_metric`. Enable with:
-
-```yaml
-performance:
- cache:
- metrics: true
-```
-
-Default remains off (non‑breaking). Covariance payload caching is unaffected. Refer to `docs/metric_cache.md` for details.
-
-### 2025-09-18 UPDATE — PR DRAFT TOGGLE (ISSUE #1137)
-Codex bootstrap workflow now supports a `codex_pr_draft` input (workflow_dispatch) controlling whether the automatically created PR is a draft. Default remains non‑draft to ensure agent engagement (addressing Issue #1137). Fallback and composite bootstrap paths both consume the resolved draft flag so behavior is consistent. Invite mode (issue-triggered path) still requires a human-authored PR and is unaffected.
+Converge the scattered modules into one fully test-covered, vectorised pipeline that can be invoked from a single CLI entry-point. Never touch notebooks living under any directory whose name ends in `old/`.
-Immediate Refactor Tasks
-Flatten duplications
-
-Rename data_utils.py → trend_analysis/data.py, adjust imports, delete the original.
-
-Migrate the contents of the top‑level exports.py into trend_analysis/export.py; keep only a re‑export stub for one minor release.
-
-Turn the stray core/ directory into an importable sub‑package:
-core/indicator.py → trend_analysis/core/indicator.py, etc.
-
-Single pipeline
-
-Implement trend_analysis/pipeline.py exposing a pure function
-run(config: Config) -> pd.DataFrame.
-
-run_analysis.py should parse CLI args, build a Config, pass it to pipeline.run, then handle pretty printing / file output only.
-
-Config resolution
-
-# trend_analysis/config.py
-from pydantic import BaseModel
-class Config(BaseModel):
- defaults: str = Path(__file__).with_name("..").joinpath("config/defaults.yml")
- # ...other validated fields...
-def load(path: str | None = None) -> Config: ...
-
-Env‑var override: TREND_CFG=/path/to/override.yml run_analysis ...
-
-Dependency hygiene
-
-Heavy imports (numpy, pandas, scipy) at top of each module are fine.
+---
-No circular imports. pipeline.py orchestrates; nothing imports it.
+## Quick Reference
-Tests
+### Demo Pipeline
-NOTE: Test fixtures must be text-serialised (CSV/JSON); no binary formats in PRs.
+```bash
+# 1. Bootstrap environment
+./scripts/setup_env.sh
-Require 100 % branch coverage on trend_analysis/* via pytest‑cov in CI.
+# 2. Generate demo dataset
+python scripts/generate_demo.py
-Conventions & Guard‑rails
-Vectorise first.
-Falling back to for‑loops requires a comment justifying why vectorisation is impossible or harmful.
+# 3. Run full demo pipeline
+python scripts/run_multi_demo.py
-Public API (exported in __all__) uses US‑English snake‑case; private helpers are prefixed with _.
+# 4. Run test suite
+./scripts/run_tests.sh
+```
-Notebook hygiene: any new exploratory notebook must start with the header
-# 🔬 scratchpad – may be deleted at any time.
+See [docs/DemoMaintenance.md](docs/DemoMaintenance.md) for the full checklist.
-CI (GitHub Actions) stages to add:
+### Key Entry Points
-lint (ruff + black –‑check)
-- Always run `black` on changed files before committing so lint passes.
+| Purpose | Command |
+|---------|---------|
+| CLI analysis | `PYTHONPATH="./src" python -m trend_analysis.run_analysis -c config/demo.yml` |
+| Streamlit app | `./scripts/run_streamlit.sh` |
+| Tests | `./scripts/run_tests.sh` |
+| Fast validation | `./scripts/dev_check.sh --fix` |
-type‑check (mypy, strict)
+---
-test (pytest ‑‑cov trend_analysis ‑‑cov‑branch)
+## Automation Entry Points
-build‑wheel (tags only)
+### Agents 70 Orchestrator
+- **File**: `.github/workflows/agents-70-orchestrator.yml`
+- **Role**: Single automation front door for all agent operations
+- **Triggers**: 20-minute schedule sweep plus manual `workflow_dispatch`
-##NEW
+### Agents 63 Issue Intake
+- **File**: `.github/workflows/agents-63-issue-intake.yml`
+- **Role**: Bootstrap Codex PRs on `agent:codex` labeled issues
+- **Triggers**: Issue events (`opened`, `labeled`, `reopened`) plus manual dispatch
-### ✨ Task: Integrate `information_ratio` end‑to‑end (#metrics‑IR)
+### Agents 64 Verify Agent Assignment
+- **File**: `.github/workflows/agents-64-verify-agent-assignment.yml`
+- **Role**: Validates agent assignment before orchestrator proceeds
-**Motivation**
-Phase‑1 now includes a vectorised `information_ratio` metric.
-It is fully unit‑tested but not yet surfaced in the CLI / Excel export or
-multi‑benchmark workflows.
+Legacy consumer wrappers were retired. See [docs/archive/ARCHIVE_WORKFLOWS.md](docs/archive/ARCHIVE_WORKFLOWS.md).
---
-#### 1. Pipeline / Statistics
-
-* [x] Extend `_Stats` dataclass with `information_ratio: float`.
-* [x] In `_compute_stats()` compute `information_ratio(df[col], rf_series)`.
-* [x] Ensure `out_stats_df` includes the new field.
-
-#### 2. Multi‑benchmark support
-
-* [x] Accept `benchmarks:` mapping in YAML cfg, e.g.
-
-```yaml
-benchmarks:
- spx: SPX
- tsx: TSX
-
-### 2025‑07‑03 UPDATE — STEP 4: surface a real `score_frame`
-
-* **Add** `single_period_run()` to **`trend_analysis/pipeline.py`**
- * **Signature**
- ```python
- def single_period_run(
- df: pd.DataFrame,
- start: str,
- end: str,
- *,
- stats_cfg: "RiskStatsConfig" | None = None
- ) -> pd.DataFrame:
- ...
- ```
- * **Behaviour**
- 1. Slice *df* to `[start, end]` (inclusive) and drop the Date column into the index.
- 2. For every metric listed in `stats_cfg.metrics_to_run` **call the public registry** (`core.rank_selection._compute_metric_series`) to obtain a vectorised series.
- 3. **Concatenate** those series column‑wise into **`score_frame`** (`index = fund code`, `columns = metric names`, dtype `float64`).
- 4. Attach metadata
- ```python
- score_frame.attrs["insample_len"] = len(window) # number of bars
- score_frame.attrs["period"] = (start, end) # optional helper
- ```
- 5. Return `score_frame` – *no side effects, no I/O*.
-
-* **Update callers**
- * `pipeline._run_analysis()` should call `single_period_run()` once, stash the resulting frame in the returned dict under key `"score_frame"`, but **must not** change existing outputs or CLI flags.
- * Existing metrics‑export logic stays exactly as is.
-
-* **Tests**
- 1. **Golden‑master**: compare the new `score_frame` against a pre‑generated CSV fixture for a small sample set.
- 2. **Metadata**: `assert sf.attrs["insample_len"] == expected_len`.
- 3. **Column order** equals the order of `stats_cfg.metrics_to_run`; failing this should raise.
-
-* **Performance / style guard‑rails**
- * Remain fully vectorised—no per‑fund Python loops.
- * Keep `single_period_run()` *pure* (no global writes, no prints).
- * Do **not** introduce extra dependencies; stick to `numpy` + `pandas`.
-
-> Once the test suite passes with the new `score_frame`, proceed to steps 5‑7 (Selector & Weighting classes).
-
-
-### Step 5 – Selector classes
-
-| Class | Purpose | Key Inputs |
-| ----- | ------- | ---------- |
-| `RankSelector` | Pure rank‑based inclusion identical to Phase 1 behaviour, but exposed as a plug‑in. | `score_frame`, `top_n`, `rank_column` |
-| `ZScoreSelector` | Filters candidates whose z‑score > `threshold`; supports negative screening by passing `direction=-1`. | `score_frame`, `threshold`, `direction` |
-
-Both selectors must return **two** DataFrames:
-1. `selected` – rows kept for the rebalancing date
-2. `log` – diagnostic table (candidate, metric, reason) used by UI & tests.
-
----
+## Canonical Module Locations
-### Step 6 – Weighting classes
+| Layer / Concern | Canonical Location | Deprecated |
+|-----------------|-------------------|------------|
+| Data ingest & cleaning | `trend_analysis/data.py` | `data_utils.py`, notebook helpers |
+| Portfolio logic & metrics | `trend_analysis/metrics.py` | loops in `run_analysis.py` |
+| Export / I/O | `trend_analysis/export/` | root-level `exports.py` |
+| Domain kernels | `trend_analysis/core/` | standalone `core/` directory |
+| Pipeline orchestration | `trend_analysis/pipeline.py` | duplicated control flow |
+| CLI entry-point | `trend_analysis/cli.py` | bespoke `scripts/*.py` |
+| Config | `config/defaults.yml` via `trend_analysis.config.load()` | hard-coded constants |
+| Tests | `tests/` (pytest with coverage gate) | — |
-| Class (inherits `BaseWeighting`) | Logic | YAML Config Stub |
-| --- | --- | --- |
-| `EqualWeight` | 1/N allocation across `selected`; rounds to nearest bps to avoid float dust. | `portfolio.weighting: {method: equal}` |
-| `ScorePropSimple` | Weight ∝ positive score; rescales to 100 %. | `portfolio.weighting: {method: score_prop}` |
-| `ScorePropBayesian` | Same, but shrinks extreme scores toward the cross‑sectional mean using a conjugate‑normal update. | `portfolio.weighting: {method: score_prop_bayes, shrink_tau: 0.25}` |
+**Rule**: One concern → one module. Replacements must delete or comment-out whatever they obsolete in the same PR.
---
-### Step 7 – Engine wiring
-
-Minimal loop in `multi_period/engine.py`:
-
-```python
-for date in schedule:
- candidates = selector.select(score_frames[date])
- weights = weighting.weight(candidates)
- portfolio.rebalance(date, weights)
-```
+## Guard-Rails
-Step 8 – Config schema delta
-
-```yaml
-metrics:
- registry: [annual_return, downside_deviation, sharpe_ratio]
-
-portfolio:
- selector:
- name: zscore # or 'rank'
- params:
- threshold: 1.0 # σ
- weighting:
- name: score_prop_bayes
- params:
- shrink_tau: 0.25
-```
+### Communication
+- When a request contains multiple tasks, explicitly recap which items are complete and which remain before handing control back.
+- Call out any suggestion that is only a partial fix, and list the follow-up steps required for it to succeed.
+- Highlight assumptions about workflow or automation status and offer to pull the live data when needed.
-Step 9 – Unit‑test skeletons
+### Code Quality
+- **Vectorise first**: Falling back to for-loops requires a comment justifying why vectorisation is impossible or harmful.
+- **Public API**: Uses US-English snake_case; private helpers prefixed with `_`.
+- **No circular imports**: `pipeline.py` orchestrates; nothing imports it.
+- **Test fixtures**: Must be text-serialised (CSV/JSON); no binary formats in PRs.
+### Git Workflow
+Before pushing, verify the target PR is still open:
+```bash
+gh pr view --json state,mergedAt,closed
```
-tests/
-└─ test_selector_weighting.py
- ├─ fixtures/score_frame_2025‑06‑30.csv
- ├─ test_rank_selector()
- ├─ test_zscore_selector_edge()
- ├─ test_equal_weighting_sum_to_one()
- └─ test_bayesian_shrinkage_monotonic()
-```
-
-
-and compare selector/weighting outputs bit‑for‑bit (tolerances < 1e‑9).
-
-Step 10 – Docs housekeeping
-
-Phase‑1 docs stay at docs/phase-1/Agents.md.
-Phase‑2 docs live in docs/phase-2/Agents.md (this file).
-Cross‑link at the top.
-
-### 2025‑07‑04 UPDATE — MULTI-PERIOD METRICS EXPORT
-
-Phase‑2 introduces a rolling back‑tester. The intent is that metrics from each
-period mirror the Phase‑1 Excel output. The export helpers must therefore
-collect the per‑period metric tables and emit one worksheet per period in the
-workbook. CSV and JSON exports should likewise produce one file per period
-using the existing :func:`export.export_data` helpers.
-
-### 2025‑07‑10 UPDATE — PER-PERIOD WORKBOOK DETAIL
-
-The original design goal is that each back‑test period should produce an Excel
-worksheet indistinguishable from the Phase‑1 summary. CSV and JSON exports must
-likewise emit one file per period. This behaviour has yet to be fully realised
-in code and tests.
-
-### 2025‑07‑11 UPDATE — PER‑PERIOD SUMMARY TABLES
-
-Implement helpers so every multi‑period run yields one workbook tab (or file)
-per period containing the full Phase‑1 style summary table. Excel sheets are
-formatted via ``make_period_formatter`` while CSV/JSON outputs receive the same
-rows using ``summary_frame_from_result``.
-
-### 2025‑07‑12 UPDATE — COMBINED SUMMARY SHEET
-
-Multi‑period exports must also include a ``summary`` sheet aggregating portfolio
-performance across all periods. The sheet uses the **same** format as the
-per‑period tabs and is generated via ``make_period_formatter`` on a result
-dictionary produced by ``combined_summary_result(results)``. CSV/JSON formats
-write a ``_summary`` file derived from ``summary_frame_from_result``.
-
-### 2025‑07‑13 UPDATE — MULTI‑PERIOD EXPORT ROADMAP
-
-The backlog still includes emitting a Phase‑1 style workbook with **one tab per
-period** for multi‑period runs. CSV and JSON outputs must create one file per
-period using the same summary table produced by
-``summary_frame_from_result``. A ``summary`` sheet/file aggregates portfolio
-returns across all periods, formatted identically to each individual period
-sheet. Work is in progress to expose this via a new ``run_multi_analysis`` CLI
-that calls ``export.export_multi_period_metrics``.
-
-### 2025-07-20 UPDATE — MULTI-PERIOD OUTPUT SPEC
-
-The pending export work shall produce an Excel workbook with one worksheet per period and a final `summary` sheet. Each tab uses the exact Phase‑ style summary table via `make_period_formatter`. CSV and JSON exports mirror this by writing one file per period plus a `_summary` file. A new helper `period_frames_from_results()` converts a sequence of result dictionaries into the mapping consumed by `export_multi_period_metrics`.
-
-### 2025-08-01 UPDATE — PER-PERIOD METRICS WORKBOOK
+- ❌ Never push to merged/closed PRs
+- ✅ Create a new branch and PR if the original is closed
-Phase‑2 back-tests shall emit an Excel workbook with one tab per period formatted identically to the Phase‑1 summary sheet. CSV and JSON outputs produce one file per period in the same table form. In addition, a `summary` tab (and `_summary` file) aggregates portfolio returns across all periods using the identical layout. Implementation is underway in `export.export_multi_period_metrics`.
-
-### 2025-08-10 UPDATE — MULTI-PERIOD PHASE‑1 METRICS EXPORT
-
-Finalise the design goal that each period of a rolling back‑test mirrors the
-Phase‑1 metrics sheet. The exporter shall generate a workbook with one tab per
-period and a `summary` tab combining portfolio returns. CSV and JSON formats
-receive one file per period plus a `_summary` file. Implementation has started in
-`export.workbook_frames_from_results` and the updated
-`export.export_multi_period_metrics` helper.
-
-### 2025-08-20 UPDATE — CONSOLIDATED CSV/JSON OUTPUT
-
-The design goal remains one Excel worksheet per period plus a `summary` tab.
-CSV and JSON exports now consolidate all period tables into **one** file named
-`*_periods.*` with an accompanying `*_summary.*` file holding the aggregated
-portfolio returns. Raw metrics, when requested, follow the same pattern under
-`metrics.*` and `metrics_summary.*`.
-
-### 2025-08-30 UPDATE — PER-PERIOD PHASE‑1 WORKBOOK
-
-Implementation work continues to realise the original goal: multi‑period runs
-should emit an Excel workbook with **one tab per period** using the exact
-Phase‑1 summary layout. A final `summary` sheet aggregates portfolio returns
-across all periods in the same format. CSV and JSON outputs mirror this by
-bundling all period tables into a single `*_periods.*` file alongside a
-`*_summary.*` file. Helper `export_phase1_workbook()` now begins the build
-out of this feature.
-
-### 2025-09-05 UPDATE — PHASE‑1 MULTI-PERIOD OUTPUT
-
-The design intent remains unchanged: a rolling back‑test should deliver an
-Excel workbook with one tab per period plus a final `summary` tab combining
-portfolio results across all periods. CSV and JSON formats must consolidate the
-same tables into a single `*_periods.*` file and a companion `*_summary.*`
-file. Work continues in `export_phase1_workbook()` and the revised
-`export_multi_period_metrics` function.
-
-### 2025-09-15 UPDATE — PHASE‑1 METRICS GOAL
-
-The original requirement is still outstanding: metrics from each period of a
-multi-period run should appear in an Excel workbook with one sheet per period
-using the **exact** Phase‑1 formatting, plus a `summary` sheet combining the
-portfolio across periods. CSV and JSON outputs must present the same tables as
-a single `*_periods.*` file with a matching `*_summary.*` file. Implementation
-has begun but is not yet complete.
-
-
-### 2025-09-30 UPDATE — MULTI-PERIOD WORKBOOK TARGET
-
-Development continues on the export layer. The goal remains to emit a Phase‑1
-style workbook with **one sheet per period** and a final `summary` sheet
-combining portfolio returns across all periods. Each sheet must apply the same
-formatters as the original single-period export. CSV and JSON outputs will
-bundle all period tables into a single `*_periods.*` file with a companion
-`*_summary.*` file. The new helpers `export_phase1_workbook()` and
-`export_phase1_multi_metrics()` begin this implementation.
-
-### 2025-10-05 UPDATE — MULTI-PERIOD PHASE-1 EXPORT PROGRESS
-
-`export_phase1_workbook()` now builds its sheet mapping via
-`workbook_frames_from_results()` so each period tab and the combined
-`summary` tab share the exact Phase‑1 layout. `export_phase1_multi_metrics()`
-uses the same helper to emit a single ``*_periods.*`` file plus a matching
-``*_summary.*`` for CSV/JSON users. Development continues to keep these
-outputs identical to the single-period workbook.
-
-### 2025-10-12 UPDATE — MULTI-PERIOD PHASE-1 METRICS GOAL
-
-The export layer must emit a Phase-1 style workbook with one sheet per period and a final `summary` sheet combining portfolio returns. Each sheet uses `make_period_formatter` so formatting matches the single-period output. CSV and JSON outputs consolidate these tables into a single `*_periods.*` file with a companion `*_summary.*` file. Implementation now begins in `export_phase1_workbook()` and `export_phase1_multi_metrics()` to realise this design.
-
-### 2025-10-20 UPDATE — PHASE-1 WORKBOOK TARGET
-
-Multi-period exports shall produce an Excel workbook with one sheet per period plus a final `summary` sheet combining portfolio returns. Formatting and columns must match the Phase-1 output. CSV and JSON formats consolidate all period tables into a single `*_periods.*` file with a corresponding `*_summary.*` file. Development now adds `flat_frames_from_results()` to build these consolidated tables for the exporters.
-
-### 2025-10-31 UPDATE — MULTI-PERIOD PHASE-1 EXPORT DESIGN
-
-The Phase-1 metrics export shall produce one Excel worksheet per period using the exact same formatting as the single-period report. A final `summary` tab aggregates portfolio performance across all periods in the identical layout. CSV and JSON exports consolidate all period tables into a single `*_periods.*` file and place the combined results in a matching `*_summary.*` file. Implementation starts in `export_phase1_workbook()` and `export_phase1_multi_metrics()` which build their frames via `workbook_frames_from_results()`.
-
-### 2025-11-08 UPDATE — PHASE-1 MULTI-PERIOD WORKBOOK GOAL
-
-The outstanding goal is unchanged: each multi-period run should yield a Phase‑1 style workbook with **one sheet per period** and a final `summary` sheet aggregating portfolio returns. The summary tab must mirror the columns and formatting of the individual period sheets. CSV and JSON outputs bundle all period tables into a single `*_periods.*` file alongside a matching `*_summary.*` file. Current work focuses on finishing `export_phase1_workbook()` and surfacing these helpers via the public API.
-
-### 2025-11-22 UPDATE — MULTI-PERIOD METRICS EXPORT TARGET
-
-The original Phase‑1 metrics layout shall extend to multi‑period runs. The exporter must generate an Excel workbook with one tab per period and a final `summary` tab aggregating portfolio returns in the **exact** same column order and formatting. CSV and JSON outputs deliver the period tables in a single `*_periods.*` file and the combined returns in a matching `*_summary.*` file. Implementation has begun in the export helpers; keep working toward full CLI integration and parity across formats.
-
-
-### 2025-12-01 UPDATE — PHASE-1 MULTI-PERIOD OUTPUT SPEC
-
-The original design has not yet been fully realised. Each rolling run should
-produce an Excel workbook with one sheet per period, formatted exactly like the
-Phase-1 summary table. A final `summary` sheet must combine portfolio returns
-with the same columns and formatting. CSV and JSON exports shall mirror this by
-bundling all period tables into a single `_periods.*` file together with
-`_summary.*` for the aggregated results. Implementation work continues in
-`export_phase1_workbook()` and `export_phase1_multi_metrics()`.
-
-### 2025-12-08 UPDATE — MULTI-PERIOD PHASE-1 EXPORT INITIATIVE
-
-The long-standing objective is for rolling runs to emit a Phase‑1 style
-workbook with **one tab per period** and a final `summary` sheet combining
-portfolio returns. Each sheet must share the identical columns and
-formatting. CSV and JSON outputs should deliver a single `_periods.*`
-file plus `_summary.*` to aggregate the results. Implementation work
-begins in `export_phase1_workbook()` and `flat_frames_from_results()` to shape
-these consolidated tables.
-
-### 2025-12-15 UPDATE — PHASE-1 MULTI-PERIOD WORKBOOK GOAL
-
-The original requirement remains: metrics from each period must be exported as
-an Excel workbook with **one sheet per period** using the exact Phase‑1 summary
-formatting. A final `summary` sheet combines portfolio returns across all
-periods in the same layout. CSV and JSON outputs should consolidate all period
-tables into a single `_periods.*` file with a matching
-`_summary.*` file. Implementation continues in
-`export_phase1_workbook()` and `export_phase1_multi_metrics()`.
-
-### 2025-12-22 UPDATE — PHASE-1 EXPORT ROADMAP
-
-Implementation is ongoing to deliver a multi-period Excel workbook with one sheet per period and a final `summary` sheet of combined portfolio returns. Each sheet must use the exact Phase‑1 formatting. CSV and JSON exports shall output a single `_periods.*` file and a companion `_summary.*` file. Work continues in `export_phase1_workbook()` and `export_phase1_multi_metrics()`.
-
-### 2026-01-05 UPDATE — MULTI-PERIOD OUTPUT OBJECTIVE
-
-The project still needs a full Phase‑1 workbook for rolling runs. Every period
-should populate its own tab with identical formatting, and a final `summary`
-tab must aggregate portfolio returns in the very same layout. CSV and JSON
-exports must bundle the per-period tables into one `_periods.*` file
-with a matching `_summary.*` companion file. Implementation will finish
-linking these helpers into the public CLI.
-
-### 2026-01-15 UPDATE — WORKBOOK DATA HELPER
-
-Phase‑1 metrics should export one sheet per period plus a summary tab. CSV and
-JSON outputs must consolidate all periods into a single file. A new helper
-`phase1_workbook_data()` starts this implementation by returning the ordered
-sheet mapping for a multi‑period workbook, optionally including raw metrics per
-period and for the combined summary. `export_phase1_workbook()` now builds its
-workbook from this mapping.
-
-## Fund Selection Debugging Protocol
-
-### Problem Context
-When debugging multi-period portfolio analysis where the same managers are selected every period despite changing performance rankings, follow this systematic approach.
-
-### Debugging Workflow
-
-1. **Environment Setup**
- - Ensure virtual environment is activated: `source venv/bin/activate`
- - Verify all dependencies installed: `python -m venv .venv && source .venv/bin/activate && pip install --upgrade pip uv && uv pip sync requirements.lock && pip install --no-deps -e .[dev]`
- - Confirm working on correct branch (use `chore/demo-pipeline` for debugging)
-
-2. **Data Completeness Analysis**
- ```python
- # Run the debug script to check data availability
- python debug_fund_selection.py
- ```
-
-This script will reveal:
-
-- Which managers get filtered out due to missing data in in-sample periods
-
-- Which managers get filtered out due to missing data in out-of-sample periods
-- Final available manager pool for selection
-- Actual ranking results for available managers
-
-Expected Issues to Check
-
-- Data Gap Issue: Only 8 managers have complete data across all periods
-- Ranking Bug: Selection logic not sorting by performance metrics
-- Configuration Issue: Wrong parameters passed to rank_select_funds
-- Period Definition: Incorrect date parsing or period boundaries
+---
-Workflow Compliance
+## Export Guard-Rails
-- DO NOT make ad-hoc changes to core modules from demo branch
-- DO document findings clearly before proposing fixes
-- DO follow the phase2-dev → chore/demo-pipeline workflow for fixes
-- DO NOT merge anything with main branch
+> 🛡️ Codex removed the pretty reporting layer once; it shall not happen again.
-Core Module Fix Process
-If debugging reveals bugs in core selection logic:
+1. **Call the canonical exporters**: After `pipeline.run()` completes, pipe results into exactly one of `export_to_excel`, `export_to_csv`, or `export_to_json`.
-- Document the Issue
- - Specific function with the bug (e.g., rank_select_funds)
- - Expected vs actual behavior
- - Root cause analysis
- - Test case demonstrating the problem
-- Implement Fix on phase2-dev
+2. **Excel format contract**:
+ - Bold title row
+ - `0.00%` for CAGR & Vol, `0.00` for Sharpe & Sortino
+ - Red numerals for MaxDD
+ - Freeze panes on header, auto-filter
+ - Column width = `max(len(header)) + 2`
-Common Pitfalls to Avoid
+3. **Column order is law**: Tests must fail if column order mutates.
-- DON'T assume the ranking algorithm is wrong without checking data completeness first
-- DON'T make changes to core modules without switching to phase2-dev
-- DON'T run analysis commands that take more than 2-3 minutes without progress updates
-- DON'T ignore the virtual environment setup - module imports will fail
-- DO trace through the actual data filtering pipeline step by step
+4. **Back-compat**: Silent config = drop fully formatted Excel workbook into `outputs/` exactly as v1.0 did.
-## Development Workflow
+---
-### Git and PR Management
+## Multi-Period Export (Phase-2)
-**CRITICAL: Always verify PR status before pushing**
+✅ **Implemented**: Multi-period runs now emit Phase-1 style exports via:
+- `export_phase1_workbook()` - Excel with one sheet per period + summary tab
+- `export_phase1_multi_metrics()` - CSV/JSON with `*_periods.*` and `*_summary.*` files
+- `export_multi_period_metrics()` - General multi-period export helper
-Before running `git push` (or `git push --force-with-lease`), **always** check that the target PR is still open:
+Helper functions:
+- `workbook_frames_from_results()` - Builds sheet mapping
+- `period_frames_from_results()` - Converts result sequence to export format
+- `combined_summary_result()` - Aggregates portfolio returns across periods
-```bash
-# Check PR status before pushing
-gh pr view --json state,mergedAt,closed
+---
-# Or check the current branch's PR
-gh pr view --json state,mergedAt,closed
-```
+## Feature Status
-**Rules:**
-- ❌ **NEVER** push to a PR that has `"state": "MERGED"` or `"state": "CLOSED"`
-- ❌ **NEVER** push to a branch whose PR was already merged (check `mergedAt` is not null)
-- ✅ **ONLY** push to PRs with `"state": "OPEN"` and `"closed": false`
-- ✅ If the PR is closed/merged, create a new branch and new PR instead
+### Implemented ✅
+- Rank-based manager selection mode (`mode: rank`)
+- Blended scoring with z-score normalization
+- Scalar metric memoization (opt-in via `performance.cache.metrics: true`)
+- PR draft toggle for Codex bootstrap (`codex_pr_draft` input)
+- Multi-period Phase-1 style exports
+- Selector and weighting plugin classes
-**Why this matters:**
-- Pushing to merged PRs pollutes git history
-- It bypasses code review and CI checks
-- Changes won't be picked up by the target branch
-- It creates confusion about what code is actually deployed
+### Backlog 📋
+- Preview score frame in UI
+- Weight heatmap visualization
+- Expected shortfall metric
+- Diversification value metric
+- Export commit hash in outputs
-### Multi-Period Analysis Debugging
+---
-When debugging multi-period issues:
+## Debugging
-1. **Start with data completeness check** - Most "selection not changing" issues are due to insufficient data for additional managers
-2. **Use the debug_fund_selection.py script** - Provides systematic analysis of the selection pipeline
-3. **Check both in-sample AND out-of-sample data requirements** - Both periods must have complete data for a manager to be eligible
-4. **Verify configuration parameters** - Ensure rank_select_funds is getting correct inclusion_approach, n, score_by parameters
+### Fund Selection Issues
-Example debug workflow:
+Use the debug script in `examples/`:
```bash
-# 1. Run systematic debugging
-python debug_fund_selection.py
-
-# 2. If data issue found: investigate data generation
-# 3. If logic issue found: follow core fix workflow
-# 4. If config issue found: update configuration files
+python examples/debug_fund_selection.py
```
-## Code Quality Guidelines
-
-### Debugging Script Standards
-
-- Debugging scripts should be self-contained and clearly document their purpose
-- Include comprehensive output showing each step of the analysis
-- Distinguish between data issues vs. logic bugs vs. configuration problems
-- Provide clear conclusions and next steps based on findings
-- Follow the same code quality standards as production code
+This reveals:
+- Which managers get filtered due to missing data
+- Available manager pool for selection
+- Actual ranking results
-### Rank Selection Bug Fix Protocol
+### Common Pitfalls
+- Don't assume ranking is wrong without checking data completeness first
+- Both in-sample AND out-of-sample periods must have complete data
+- Verify configuration parameters match intended behavior
-**Problem**: The `rank_select_funds` function in `src/trend_analysis/core/rank_selection.py` selects funds by DataFrame column order instead of by performance ranking.
-
-**Root Cause**: The function calls `scores.head(n)` without sorting the scores first, so it returns the first N funds in the original DataFrame order rather than the top N performers.
-
-**Workflow**:
-1. Switch to `phase2-dev` branch
-2. Fix the core ranking logic
-3. Add comprehensive tests
-4. Commit and push to `phase2-dev`
-5. Switch back to `chore/demo-pipeline` and merge the fix
+---
-**Critical Requirements**:
-- DO NOT modify core modules from the demo branch
-- All core fixes must be done on `phase2-dev`
-- Ensure virtual environment is active before making changes
-- Test both ascending (MaxDrawdown) and descending (Sharpe) metrics
+## Related Documentation
+- [docs/phase-2/Agents.md](docs/phase-2/Agents.md) - Complete implementation spec
+- [docs/DemoMaintenance.md](docs/DemoMaintenance.md) - Demo pipeline checklist
+- [docs/archive/ARCHIVE_WORKFLOWS.md](docs/archive/ARCHIVE_WORKFLOWS.md) - Retired workflows
+- [docs/metric_cache.md](docs/metric_cache.md) - Metric memoization details
diff --git a/README.md b/README.md
index 6fe61ba887..bcf6ea5de1 100644
--- a/README.md
+++ b/README.md
@@ -1,309 +1,214 @@
-actionlint
-## Trend Model quickstart
+# Trend Model Project
-Install the packages in editable mode to expose the CLI entry points. The
-legacy `sitecustomize.py` shim has been removed, so editable installs (or a
-wheel install) are now the only supported way to run code—`PYTHONPATH`
-modifications are intentionally ignored:
+A Python-based financial analysis application for volatility-adjusted trend portfolio construction and backtesting. The project provides a command-line interface, interactive Streamlit web application, and Jupyter notebook support for analyzing fund manager performance and constructing optimized portfolios.
+
+## What This Project Does
+
+The Trend Model Project helps you:
+
+- **Analyze fund returns** – Load CSV/Excel data and compute risk-adjusted metrics (CAGR, Sharpe, Sortino, max drawdown, information ratio)
+- **Select top performers** – Rank funds by configurable scoring criteria and apply filters
+- **Construct portfolios** – Weight selected funds using equal-weight, score-proportional, risk parity, or Bayesian methods
+- **Backtest strategies** – Run single-period or multi-period analyses with walk-forward validation
+- **Generate reports** – Export results to Excel, CSV, JSON, HTML, and PDF formats
+
+## Quick Start
+
+### 1. Install
```bash
+# Clone the repository
+git clone https://github.com/stranske/Trend_Model_Project.git
+cd Trend_Model_Project
+
+# Set up virtual environment and install dependencies
+./scripts/setup_env.sh
+
+# Or manually:
python3 -m venv .venv
source .venv/bin/activate
-pip install --upgrade pip
pip install -e .[app]
```
-Verify the installation and explore the available commands (this also proves
-`trend --help` works as required by Issue #3582):
+### 2. Verify Installation
```bash
trend --help
-trend run -c config/demo.yml --returns demo/demo_returns.csv
```
-The `trend` command is the unified launcher for day-to-day workflows:
-
-- `trend run` executes the single-period pipeline
-- `trend report` produces summary artefacts
-- `trend quick-report` turns existing run artefacts into a compact HTML + heatmap bundle
-- `trend stress` replays canned stress windows
-- `trend app` starts the Streamlit interface
+### 3. Run the Demo
-Need to know where the sample CSV inputs come from? See
-[README_DATA.md](README_DATA.md) for provenance, intended use, and limitations
-before wiring them into demos or documentation.
+```bash
+# Generate synthetic demo data
+python scripts/generate_demo.py
-Every demo/backtest script (for example `scripts/run_multi_demo.py`) shells out
-to these console entry points instead of adjusting `sys.path`. If you skip the
-editable install, the scripts will raise a friendly error explaining how to fix
-your environment.
+# Run analysis with demo configuration
+trend run -c config/demo.yml --returns demo/demo_returns.csv
+```
-Run the demo pipeline end-to-end using the helper script:
+### 4. Launch the Web App
```bash
-python scripts/run_multi_demo.py
+trend app
```
-### Generate a markdown tear sheet
-
-Create a quick report from the most recent demo results (writes both Markdown and a PNG chart bundle):
+Then open http://localhost:8501 in your browser.
-```bash
-python -m src.cli report --last-run demo/portfolio_test_results/last_run_results.json
-cat reports/tearsheet.md
-```
+## Usage Options
-### Named universes
+| Interface | Command | Best For |
+|-----------|---------|----------|
+| **CLI** | `trend run -c config.yml` | Scripted/automated analysis |
+| **Streamlit App** | `trend app` | Interactive exploration |
+| **Jupyter GUI** | `from trend_analysis.gui import launch; launch()` | Notebook workflows |
-Predefined membership sets live under `config/universe`. Swap between them at
-runtime by adding the `--universe` flag to `trend-model run`:
+### Command-Line Examples
```bash
-trend-model run \
- --config config/trend_universe_2004.yml \
- --input "Trend Universe Data.csv" \
- --universe core
-```
+# Basic analysis with config file
+trend run -c config/demo.yml --returns data/returns.csv
-Change the universe key (for example, `managed_futures_min`) to rerun the same
-configuration against a different membership timeline without editing the
-config file.
+# Use a preset strategy
+trend run -c config/demo.yml --returns data/returns.csv --preset conservative
-### Walk-forward stability harness
+# Generate a report from previous results
+trend report --last-run demo/portfolio_test_results/last_run_results.json
-Use the lightweight walk-forward sweep when you need deterministic train/test
-splits across a small parameter grid:
+# Run stress test scenarios
+trend stress -c config/demo.yml
-```bash
+# Walk-forward analysis
python scripts/walk_forward.py --config config/walk_forward.yml
```
-The helper emits per-fold metrics, a consolidated summary CSV/JSONL, and an
-optional heatmap under `perf/wf/`. See [docs/walk_forward.md](docs/walk_forward.md)
-for the configuration layout and sample output.
-### Demo data provenance
+## Configuration
-The repository ships a handful of CSV fixtures so demos and automated tests
-run without external downloads. Before using those files, review
-[README_DATA.md](README_DATA.md) for provenance, permitted use, and
-limitations—every bundled dataset is synthetic or derived from public
-benchmarks and must not be treated as production-quality market data.
-### Run logs
+Analysis parameters are controlled via YAML configuration files. The key sections are:
-All user-facing scripts now call ``trend_analysis.logging_setup.setup_logging`` to
-initialize logging in a single place. Each invocation writes a timestamped log
-such as ``perf/runs/20250205-131500/app.log`` and mirrors the same output to the
-console. The ``perf/runs`` directory is git-ignored, so ad-hoc investigations no
-longer pollute ``git status``.
+```yaml
+data:
+ returns_file: "data/returns.csv"
+ risk_free_column: "T-Bill" # Optional cash proxy
+ missing_policy: "ffill" # Handle gaps: drop, ffill, or zero
+
+portfolio:
+ selection_mode: "rank" # all, random, manual, or rank
+ top_n: 10 # Number of funds to select
+ weighting:
+ method: "equal" # equal, score_prop, risk_parity, hrp, etc.
+
+vol_adjust:
+ target_vol: 0.10 # 10% annualized volatility target
+
+output:
+ format: "excel" # excel, csv, or json
+ path: "outputs/results"
+```
-Need to inspect a run later? Point your favourite log viewer at the newest
-folder:
+See `config/defaults.yml` for the complete schema and `config/presets/` for ready-made strategies.
+
+## Project Structure
-```bash
-ls -1 perf/runs | tail -n 1
-tail -f perf/runs//app.log
+```
+Trend_Model_Project/
+├── src/trend_analysis/ # Core analysis package
+│ ├── pipeline.py # Main orchestration
+│ ├── metrics.py # Financial metrics
+│ ├── export/ # Output formatters
+│ └── config/ # Configuration models
+├── src/trend_portfolio_app/ # Streamlit application
+├── streamlit_app/ # Streamlit pages
+├── config/ # YAML configuration files
+│ ├── defaults.yml
+│ └── presets/ # Conservative, balanced, aggressive
+├── scripts/ # Utility scripts
+├── tests/ # Unit tests
+├── docs/ # Documentation
+└── demo/ # Generated demo datasets
```
-Ad-hoc helpers that live under `scripts/` now share a tiny utility
-(`trend_analysis.script_logging.setup_script_logging`) so every run initialises the same
-perf logger before doing real work. Custom scripts can reuse the helper to get
-consistent logs without re-implementing any wiring:
+## Documentation
-```python
-from trend_analysis.script_logging import setup_script_logging
+| Document | Purpose |
+|----------|---------|
+| **[User Guide](docs/UserGuide.md)** | Complete feature walkthrough with examples |
+| **[README_APP.md](README_APP.md)** | Streamlit app layout and features |
+| **[README_DATA.md](README_DATA.md)** | Demo data provenance and limitations |
+| **[docs/INDEX.md](docs/INDEX.md)** | Full documentation index |
+| **[docs/CLI.md](docs/CLI.md)** | Command-line interface reference |
+| **[docs/ConfigMap.md](docs/ConfigMap.md)** | Configuration parameter reference |
+| **[docs/PresetStrategies.md](docs/PresetStrategies.md)** | Strategy preset descriptions |
-if __name__ == "__main__":
- setup_script_logging(app_name="rebalance-helper")
-```
+## Key Features
-### Root-level file inventory and placement
-
-| File | Category | Purpose / placement rationale |
-| --- | --- | --- |
-| `README.md` | Reference doc | CLI quickstart and inventory anchor kept at the repository entrypoint. |
-| `AGENTS_APP.md` | Reference doc | App + simulator work instructions surfaced at the top level for quick discovery. |
-| `Agents.md` | Reference doc | Guard-rails and workflow guidance that govern repository changes. |
-| `README_APP.md` | Reference doc | Streamlit app install, layout, and preset notes for maintainers. |
-| `README_DATA.md` | Reference doc | Provenance and usage notes for bundled datasets referenced during onboarding. |
-| `CHANGELOG.md` | Reference doc | Release notes held at the root per conventional placement. |
-| `CODE_OF_CONDUCT.md` | Reference doc | Community policy required at repository entry. |
-| `CONTRIBUTING.md` | Reference doc | Contribution workflow and review expectations. |
-| `DEPENDENCY_QUICKSTART.md` | Reference doc | Dependency setup cheat sheet kept beside the main README. |
-| `DOCKER_QUICKSTART.md` | Reference doc | Docker usage guide co-located with `docker-compose.yml` and the root Dockerfile. |
-| `ROBUSTNESS_GUIDE.md` | Reference doc | Legacy robustness pointer retained until a refreshed guide lands. |
-| `SECURITY.md` | Reference doc | Security policy and disclosure expectations. |
-| `Trend Universe Data.csv` | Data sample | Primary demo return matrix; universe configs reference the root path directly. |
-| `Trend Universe Membership.csv` | Data sample | Membership ledger paired with the return matrix; loaders expect the root location. |
-| `hedge_fund_returns_with_indexes.csv` | Data sample | Benchmark demo input consumed by example configs and docs. |
-| `test_multi_period_selection.py` | Active script | Standalone regression harness kept at root for quick manual selection checks. |
-| `test_upload_app.py` | Active script | Upload-app smoke harness invoked directly outside the pytest tree. |
-| `coverage-summary.md` | Historical | Symlink to `archives/generated/2025/2025-11-22_coverage-summary.md` (CI snapshot). |
-| `gate-summary.md` | Historical | Symlink to `archives/generated/2025/2025-11-22_gate-summary.md` (CI gate snapshot). |
-| `keepalive_status.md` | Historical | Symlink to `archives/generated/2025/2025-11-22_keepalive_status.md` (keepalive snapshot). |
-
-See `archives/ROOT_FILE_INDEX.md` for the dated archive index covering additional root-level artefacts moved out of the entrypoint.
-
-### Archived generated artifacts
-
-Historical CI outputs (for example `coverage-summary.md`, `gate-summary.md`,
-and `keepalive_status.md`) are date-stamped under
-`archives/generated/2025/` with root-level symlinks pointing at the current
-copies. Additional legacy reports such as `Portfolio_Test_Results_Summary.md`,
-`TESTING_SUMMARY.md`, and `Issues.txt` moved to the `archives/reports/`
-and `archives/docs/` trees on 2025-11-22; use `archives/ROOT_FILE_INDEX.md`
-to find the precise paths.
-
----
-
-==========
-[![CI Badge][]][CI]
-[![API Document][api-badge]][apidoc]
-
-[actionlint][repo] is a static checker for GitHub Actions workflow files. [Try it online!][playground]
-
-Features:
-
-- **Syntax check for workflow files** to check unexpected or missing keys following [workflow syntax][syntax-doc]
-- **Strong type check for `${{ }}` expressions** to catch several semantic errors like access to not existing property,
- type mismatches, ...
-- **Actions usage check** to check that inputs at `with:` and outputs in `steps.{id}.outputs` are correct
-- **Reusable workflow check** to check inputs/outputs/secrets of reusable workflows and workflow calls
-- **[shellcheck][] and [pyflakes][] integrations** for scripts at `run:`
-- **Security checks**; [script injection][script-injection-doc] by untrusted inputs, hard-coded credentials
-- **Other several useful checks**; [glob syntax][filter-pattern-doc] validation, dependencies check for `needs:`,
- runner label validation, cron syntax validation, ...
-
-See [the full list](docs/checks.md) of checks done by actionlint.
-
-
-
-**Example of broken workflow:**
+### Selection Modes
-```yaml
-on:
- push:
- branch: main
- tags:
- - 'v\d+'
-jobs:
- test:
- strategy:
- matrix:
- os: [macos-latest, linux-latest]
- runs-on: ${{ matrix.os }}
- steps:
- - run: echo "Checking commit '${{ github.event.head_commit.message }}'"
- - uses: actions/checkout@v4
- - uses: actions/setup-node@v4
- with:
- node_version: 18.x
- - uses: actions/cache@v4
- with:
- path: ~/.npm
- key: ${{ matrix.platform }}-node-${{ hashFiles('**/package-lock.json') }}
- if: ${{ github.repository.permissions.admin == true }}
- - run: npm install && npm test
-```
+- **all** – Include every fund in the portfolio
+- **rank** – Select top N funds by score
+- **random** – Randomly sample funds (for Monte Carlo analysis)
+- **manual** – Hand-pick funds via GUI
-**actionlint reports 7 errors:**
+### Weighting Methods
-```
-test.yaml:3:5: unexpected key "branch" for "push" section. expected one of "branches", "branches-ignore", "paths", "paths-ignore", "tags", "tags-ignore", "types", "workflows" [syntax-check]
- |
-3 | branch: main
- | ^~~~~~~
-test.yaml:5:11: character '\' is invalid for branch and tag names. only special characters [, ?, +, *, \, ! can be escaped with \. see `man git-check-ref-format` for more details. note that regular expression is unavailable. note: filter pattern syntax is explained at https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#filter-pattern-cheat-sheet [glob]
- |
-5 | - 'v\d+'
- | ^~~~
-test.yaml:10:28: label "linux-latest" is unknown. available labels are "windows-latest", "windows-2022", "windows-2019", "windows-2016", "ubuntu-latest", "ubuntu-22.04", "ubuntu-20.04", "ubuntu-18.04", "macos-latest", "macos-12", "macos-12.0", "macos-11", "macos-11.0", "macos-10.15", "self-hosted", "x64", "arm", "arm64", "linux", "macos", "windows". if it is a custom label for self-hosted runner, set list of labels in actionlint.yaml config file [runner-label]
- |
-10 | os: [macos-latest, linux-latest]
- | ^~~~~~~~~~~~~
-test.yaml:13:41: "github.event.head_commit.message" is potentially untrusted. avoid using it directly in inline scripts. instead, pass it through an environment variable. see https://docs.github.com/en/actions/learn-github-actions/security-hardening-for-github-actions for more details [expression]
- |
-13 | - run: echo "Checking commit '${{ github.event.head_commit.message }}'"
- | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-test.yaml:17:11: input "node_version" is not defined in action "actions/setup-node@v4". available inputs are "always-auth", "architecture", "cache", "cache-dependency-path", "check-latest", "node-version", "node-version-file", "registry-url", "scope", "token" [action]
- |
-17 | node_version: 18.x
- | ^~~~~~~~~~~~~
-test.yaml:21:20: property "platform" is not defined in object type {os: string} [expression]
- |
-21 | key: ${{ matrix.platform }}-node-${{ hashFiles('**/package-lock.json') }}
- | ^~~~~~~~~~~~~~~
-test.yaml:22:17: receiver of object dereference "permissions" must be type of object but got "string" [expression]
- |
-22 | if: ${{ github.repository.permissions.admin == true }}
- | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-```
+| Method | Description |
+|--------|-------------|
+| `equal` | Simple 1/N allocation |
+| `score_prop` | Weights proportional to scores |
+| `score_prop_bayes` | Bayesian shrinkage of scores |
+| `adaptive_bayes` | Cross-period learning |
+| `risk_parity` | Equal risk contribution |
+| `hrp` | Hierarchical risk parity |
-## Why?
+### Risk Controls
-- **Running a workflow is time consuming.** You need to push the changes and wait until the workflow runs on GitHub even if
- it contains some trivial mistakes. [act][] is useful to debug the workflow locally. But it is not suitable for CI and still
- time consuming when your workflow gets larger.
-- **Checks of workflow files by GitHub are very loose.** It reports no error even if unexpected keys are in mappings
- (meant that some typos in keys). And also it reports no error when accessing to property which is actually not existing.
- For example `matrix.foo` when no `foo` is defined in `matrix:` section, it is evaluated to `null` and causes no error.
-- **Some mistakes silently break a workflow.** Most common case I saw is specifying missing property to cache key. In the
- case cache silently does not work properly but a workflow itself runs without error. So you might not notice the mistake
- forever.
+- Volatility targeting with configurable lookback windows
+- Maximum weight constraints per asset
+- Group-level allocation caps
+- Turnover limits and transaction cost modeling
-## Quick start
+### Output Formats
-Install `actionlint` command by downloading [the released binary][releases] or by Homebrew or by `go install`. See
-[the installation document](docs/install.md) for more details like how to manage the command with several package managers
-or run via Docker container.
+- **Excel** – Formatted workbook with summary sheet
+- **CSV** – Machine-readable metrics
+- **JSON** – Structured data for programmatic consumption
+- **HTML/PDF** – Tear sheets and reports (via `trend report`)
-```sh
-go install github.com/rhysd/actionlint/cmd/actionlint@latest
-```
+## Development
-Basically all you need to do is run the `actionlint` command in your repository. actionlint automatically detects workflows and
-checks errors. actionlint focuses on finding out mistakes. It tries to catch errors as much as possible and make false positives
-as minimal as possible.
+### Run Tests
-```sh
-actionlint
+```bash
+./scripts/run_tests.sh
```
-Another option to try actionlint is [the online playground][playground]. Your browser can run actionlint through WebAssembly.
+### Validation
-See [the usage document](docs/usage.md) for more details.
+```bash
+# Quick check during development
+./scripts/dev_check.sh --fix
+
+# Comprehensive pre-commit validation
+./scripts/validate_fast.sh --fix
+
+# Full CI-equivalent check
+./scripts/check_branch.sh --fast --fix
+```
-## Documents
+### Contributing
-- [Checks](docs/checks.md): Full list of all checks done by actionlint with example inputs, outputs, and playground links.
-- [Installation](docs/install.md): Installation instructions. Prebuilt binaries, Homebrew package, a Docker image, building from
- source, a download script (for CI) are available.
-- [Usage](docs/usage.md): How to use `actionlint` command locally or on GitHub Actions, the online playground, an official Docker
- image, and integrations with reviewdog, Problem Matchers, super-linter, pre-commit, VS Code.
-- [Configuration](docs/config.md): How to configure actionlint behavior. Currently only labels of self-hosted runners can be
- configured.
-- [Go API](docs/api.md): How to use actionlint as Go library.
-- [References](docs/reference.md): Links to resources.
+See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
-## Bug reporting
+## File Inventory
-When you see some bugs or false positives, it is helpful to [file a new issue][issue-form] with a minimal example
-of input. Giving me some feedbacks like feature requests or ideas of additional checks is also welcome.
+| File | Purpose |
+|------|---------|
+| `Agents.md` | Guard-rails and workflow guidance for contributors |
+| `CHANGELOG.md` | Release notes |
+| `CONTRIBUTING.md` | Contribution guidelines |
+| `DEPENDENCY_QUICKSTART.md` | Dependency setup cheat sheet |
+| `DOCKER_QUICKSTART.md` | Docker usage guide |
+| `SECURITY.md` | Security policy |
## License
-actionlint is distributed under [the MIT license](./LICENSE.txt).
-
-[CI Badge]: https://github.com/rhysd/actionlint/workflows/CI/badge.svg?branch=main&event=push
-[CI]: https://github.com/rhysd/actionlint/actions?query=workflow%3ACI+branch%3Amain
-[api-badge]: https://pkg.go.dev/badge/github.com/rhysd/actionlint.svg
-[apidoc]: https://pkg.go.dev/github.com/rhysd/actionlint
-[repo]: https://github.com/rhysd/actionlint
-[playground]: https://rhysd.github.io/actionlint/
-[shellcheck]: https://github.com/koalaman/shellcheck
-[pyflakes]: https://github.com/PyCQA/pyflakes
-[act]: https://github.com/nektos/act
-[syntax-doc]: https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions
-[filter-pattern-doc]: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#filter-pattern-cheat-sheet
-[script-injection-doc]: https://docs.github.com/en/actions/learn-github-actions/security-hardening-for-github-actions#understanding-the-risk-of-script-injections
-[issue-form]: https://github.com/rhysd/actionlint/issues/new
-[releases]: https://github.com/rhysd/actionlint/releases
+[MIT License](LICENSE)
diff --git a/README_DATA.md b/README_DATA.md
index 09e7859a58..f3d3e3df34 100644
--- a/README_DATA.md
+++ b/README_DATA.md
@@ -1,6 +1,6 @@
# Demo dataset provenance
-This repository bundles a few small CSV files (for example `hedge_fund_returns_with_indexes.csv`, `Trend Universe Data.csv`, and other samples under `demo/`). They exist solely to support tests, docs, and interactive demos.
+This repository bundles a few small CSV files in the `data/` folder (for example `data/hedge_fund_returns_with_indexes.csv`, `data/Trend Universe Data.csv`, and other samples under `demo/`). They exist solely to support tests, docs, and interactive demos.
- **Provenance:** All bundled data is synthetic or derived from public benchmark series. No confidential client information or proprietary hedge fund records are present.
- **Intended use:** The files allow contributors to exercise the Trend Model demos, verify export pipelines, and run automated tests without reaching out to live data providers.
@@ -42,7 +42,7 @@ not meant to inform live investment decisions.
## Risk-free columns in sample data
-- The long backtest sample (`hedge_fund_returns_with_indexes.csv`) includes a
+- The long backtest sample (`data/hedge_fund_returns_with_indexes.csv`) includes a
**"Risk-Free Rate"** column. Configs such as `config/long_backtest.yml`
specify `data.risk_free_column: "Risk-Free Rate"` and set
`allow_risk_free_fallback: false` to ensure the cash proxy is used and
diff --git a/ROBUSTNESS_GUIDE.md b/ROBUSTNESS_GUIDE.md
deleted file mode 100644
index 1d9ca21412..0000000000
--- a/ROBUSTNESS_GUIDE.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# Robustness guide (archived)
-
-This robustness how-to has moved to `archives/docs/ROBUSTNESS_GUIDE.md`. See `docs/INDEX.md` for the current testing and workflow guides and for links to the archived copy when you need historical context.
diff --git a/agents/README.md b/agents/README.md
index edf56917de..fb0b5a5682 100644
--- a/agents/README.md
+++ b/agents/README.md
@@ -1,9 +1,27 @@
# Agents instruction index
-The files in this directory house the active Codex/agent guidance tied to in-flight issues. Historical bootstraps that referenced retired workflows now live in `../archives/agents/`.
+This directory contains **active** Codex/agent guidance tied to **open** issues only.
+Files for closed issues are archived to `archives/agents/` with a date prefix.
-Use the following canonical sources when adding or updating instructions:
-- `.github/workflows/README.md` — current workflow inventory and naming rules.
-- `docs/ci/AGENTS_POLICY.md` — protection contract and verification steps for the agents workflows.
+## Current active files
-If an issue-specific instruction file no longer matches the live workflow names or policy versions, move it to `archives/agents/` with a date prefix and note the replacement source.
+| File | Issue | Status |
+|------|-------|--------|
+| `codex-3572.md` | [#3572](https://github.com/stranske/Trend_Model_Project/issues/3572) | OPEN |
+
+## Maintenance
+
+See `MAINTENANCE.md` for archival criteria. When an issue closes:
+
+1. Run `scripts/archive_agents.sh` to auto-archive closed-issue files
+2. Or manually: `mv codex-NNNN.md ../archives/agents/$(date +%Y-%m-%d)-codex-NNNN.md`
+
+## Canonical sources
+
+- `.github/workflows/README.md` — workflow inventory and naming rules
+- `docs/ci/AGENTS_POLICY.md` — protection contract and verification steps
+
+## Archive history
+
+- **2025-11-30**: Bulk archived 415 codex files and 18 ledger files (all closed issues)
+- **2025-11-22**: Archived 4 files referencing retired workflow names
diff --git a/analysis/results.py b/analysis/results.py
index 8b3117da54..cf9767f4de 100644
--- a/analysis/results.py
+++ b/analysis/results.py
@@ -14,8 +14,8 @@
__all__ = ["Results", "build_metadata", "compute_universe_fingerprint"]
_ROOT = Path(__file__).resolve().parents[1]
-_DEFAULT_DATA_PATH = _ROOT / "Trend Universe Data.csv"
-_DEFAULT_MEMBERSHIP_PATH = _ROOT / "Trend Universe Membership.csv"
+_DEFAULT_DATA_PATH = _ROOT / "data" / "Trend Universe Data.csv"
+_DEFAULT_MEMBERSHIP_PATH = _ROOT / "data" / "Trend Universe Membership.csv"
def _coerce_series(obj: Any) -> pd.Series:
diff --git a/agents/codex-1018.md b/archives/agents/2025-11-30-codex-1018.md
similarity index 100%
rename from agents/codex-1018.md
rename to archives/agents/2025-11-30-codex-1018.md
diff --git a/agents/codex-1064.md b/archives/agents/2025-11-30-codex-1064.md
similarity index 100%
rename from agents/codex-1064.md
rename to archives/agents/2025-11-30-codex-1064.md
diff --git a/agents/codex-1137.md b/archives/agents/2025-11-30-codex-1137.md
similarity index 100%
rename from agents/codex-1137.md
rename to archives/agents/2025-11-30-codex-1137.md
diff --git a/agents/codex-1140.md b/archives/agents/2025-11-30-codex-1140.md
similarity index 100%
rename from agents/codex-1140.md
rename to archives/agents/2025-11-30-codex-1140.md
diff --git a/agents/codex-1142.md b/archives/agents/2025-11-30-codex-1142.md
similarity index 100%
rename from agents/codex-1142.md
rename to archives/agents/2025-11-30-codex-1142.md
diff --git a/agents/codex-1156.md b/archives/agents/2025-11-30-codex-1156.md
similarity index 100%
rename from agents/codex-1156.md
rename to archives/agents/2025-11-30-codex-1156.md
diff --git a/agents/codex-1157.md b/archives/agents/2025-11-30-codex-1157.md
similarity index 100%
rename from agents/codex-1157.md
rename to archives/agents/2025-11-30-codex-1157.md
diff --git a/agents/codex-1159.md b/archives/agents/2025-11-30-codex-1159.md
similarity index 100%
rename from agents/codex-1159.md
rename to archives/agents/2025-11-30-codex-1159.md
diff --git a/agents/codex-1161.md b/archives/agents/2025-11-30-codex-1161.md
similarity index 100%
rename from agents/codex-1161.md
rename to archives/agents/2025-11-30-codex-1161.md
diff --git a/agents/codex-1205.md b/archives/agents/2025-11-30-codex-1205.md
similarity index 100%
rename from agents/codex-1205.md
rename to archives/agents/2025-11-30-codex-1205.md
diff --git a/agents/codex-1207.md b/archives/agents/2025-11-30-codex-1207.md
similarity index 100%
rename from agents/codex-1207.md
rename to archives/agents/2025-11-30-codex-1207.md
diff --git a/agents/codex-1259.md b/archives/agents/2025-11-30-codex-1259.md
similarity index 100%
rename from agents/codex-1259.md
rename to archives/agents/2025-11-30-codex-1259.md
diff --git a/agents/codex-1342.md b/archives/agents/2025-11-30-codex-1342.md
similarity index 100%
rename from agents/codex-1342.md
rename to archives/agents/2025-11-30-codex-1342.md
diff --git a/agents/codex-1344.md b/archives/agents/2025-11-30-codex-1344.md
similarity index 100%
rename from agents/codex-1344.md
rename to archives/agents/2025-11-30-codex-1344.md
diff --git a/agents/codex-1345.md b/archives/agents/2025-11-30-codex-1345.md
similarity index 100%
rename from agents/codex-1345.md
rename to archives/agents/2025-11-30-codex-1345.md
diff --git a/agents/codex-1346.md b/archives/agents/2025-11-30-codex-1346.md
similarity index 100%
rename from agents/codex-1346.md
rename to archives/agents/2025-11-30-codex-1346.md
diff --git a/agents/codex-1347.md b/archives/agents/2025-11-30-codex-1347.md
similarity index 100%
rename from agents/codex-1347.md
rename to archives/agents/2025-11-30-codex-1347.md
diff --git a/agents/codex-1348.md b/archives/agents/2025-11-30-codex-1348.md
similarity index 100%
rename from agents/codex-1348.md
rename to archives/agents/2025-11-30-codex-1348.md
diff --git a/agents/codex-1350.md b/archives/agents/2025-11-30-codex-1350.md
similarity index 100%
rename from agents/codex-1350.md
rename to archives/agents/2025-11-30-codex-1350.md
diff --git a/agents/codex-1351.md b/archives/agents/2025-11-30-codex-1351.md
similarity index 100%
rename from agents/codex-1351.md
rename to archives/agents/2025-11-30-codex-1351.md
diff --git a/agents/codex-1386.md b/archives/agents/2025-11-30-codex-1386.md
similarity index 100%
rename from agents/codex-1386.md
rename to archives/agents/2025-11-30-codex-1386.md
diff --git a/agents/codex-1414.md b/archives/agents/2025-11-30-codex-1414.md
similarity index 100%
rename from agents/codex-1414.md
rename to archives/agents/2025-11-30-codex-1414.md
diff --git a/agents/codex-1415.md b/archives/agents/2025-11-30-codex-1415.md
similarity index 100%
rename from agents/codex-1415.md
rename to archives/agents/2025-11-30-codex-1415.md
diff --git a/agents/codex-1417.md b/archives/agents/2025-11-30-codex-1417.md
similarity index 100%
rename from agents/codex-1417.md
rename to archives/agents/2025-11-30-codex-1417.md
diff --git a/agents/codex-1418.md b/archives/agents/2025-11-30-codex-1418.md
similarity index 100%
rename from agents/codex-1418.md
rename to archives/agents/2025-11-30-codex-1418.md
diff --git a/agents/codex-1419.md b/archives/agents/2025-11-30-codex-1419.md
similarity index 100%
rename from agents/codex-1419.md
rename to archives/agents/2025-11-30-codex-1419.md
diff --git a/agents/codex-1420.md b/archives/agents/2025-11-30-codex-1420.md
similarity index 100%
rename from agents/codex-1420.md
rename to archives/agents/2025-11-30-codex-1420.md
diff --git a/agents/codex-1421.md b/archives/agents/2025-11-30-codex-1421.md
similarity index 100%
rename from agents/codex-1421.md
rename to archives/agents/2025-11-30-codex-1421.md
diff --git a/agents/codex-1422.md b/archives/agents/2025-11-30-codex-1422.md
similarity index 100%
rename from agents/codex-1422.md
rename to archives/agents/2025-11-30-codex-1422.md
diff --git a/agents/codex-1426.md b/archives/agents/2025-11-30-codex-1426.md
similarity index 100%
rename from agents/codex-1426.md
rename to archives/agents/2025-11-30-codex-1426.md
diff --git a/agents/codex-1436.md b/archives/agents/2025-11-30-codex-1436.md
similarity index 100%
rename from agents/codex-1436.md
rename to archives/agents/2025-11-30-codex-1436.md
diff --git a/agents/codex-1437.md b/archives/agents/2025-11-30-codex-1437.md
similarity index 100%
rename from agents/codex-1437.md
rename to archives/agents/2025-11-30-codex-1437.md
diff --git a/agents/codex-1438.md b/archives/agents/2025-11-30-codex-1438.md
similarity index 100%
rename from agents/codex-1438.md
rename to archives/agents/2025-11-30-codex-1438.md
diff --git a/agents/codex-1439.md b/archives/agents/2025-11-30-codex-1439.md
similarity index 100%
rename from agents/codex-1439.md
rename to archives/agents/2025-11-30-codex-1439.md
diff --git a/agents/codex-1440.md b/archives/agents/2025-11-30-codex-1440.md
similarity index 100%
rename from agents/codex-1440.md
rename to archives/agents/2025-11-30-codex-1440.md
diff --git a/agents/codex-1441.md b/archives/agents/2025-11-30-codex-1441.md
similarity index 100%
rename from agents/codex-1441.md
rename to archives/agents/2025-11-30-codex-1441.md
diff --git a/agents/codex-1491.md b/archives/agents/2025-11-30-codex-1491.md
similarity index 100%
rename from agents/codex-1491.md
rename to archives/agents/2025-11-30-codex-1491.md
diff --git a/agents/codex-1610.md b/archives/agents/2025-11-30-codex-1610.md
similarity index 100%
rename from agents/codex-1610.md
rename to archives/agents/2025-11-30-codex-1610.md
diff --git a/agents/codex-1630.md b/archives/agents/2025-11-30-codex-1630.md
similarity index 100%
rename from agents/codex-1630.md
rename to archives/agents/2025-11-30-codex-1630.md
diff --git a/agents/codex-1655.md b/archives/agents/2025-11-30-codex-1655.md
similarity index 100%
rename from agents/codex-1655.md
rename to archives/agents/2025-11-30-codex-1655.md
diff --git a/agents/codex-1656.md b/archives/agents/2025-11-30-codex-1656.md
similarity index 100%
rename from agents/codex-1656.md
rename to archives/agents/2025-11-30-codex-1656.md
diff --git a/agents/codex-1657.md b/archives/agents/2025-11-30-codex-1657.md
similarity index 100%
rename from agents/codex-1657.md
rename to archives/agents/2025-11-30-codex-1657.md
diff --git a/agents/codex-1658.md b/archives/agents/2025-11-30-codex-1658.md
similarity index 100%
rename from agents/codex-1658.md
rename to archives/agents/2025-11-30-codex-1658.md
diff --git a/agents/codex-1659.md b/archives/agents/2025-11-30-codex-1659.md
similarity index 100%
rename from agents/codex-1659.md
rename to archives/agents/2025-11-30-codex-1659.md
diff --git a/agents/codex-1660.md b/archives/agents/2025-11-30-codex-1660.md
similarity index 100%
rename from agents/codex-1660.md
rename to archives/agents/2025-11-30-codex-1660.md
diff --git a/agents/codex-1661.md b/archives/agents/2025-11-30-codex-1661.md
similarity index 100%
rename from agents/codex-1661.md
rename to archives/agents/2025-11-30-codex-1661.md
diff --git a/agents/codex-1662.md b/archives/agents/2025-11-30-codex-1662.md
similarity index 100%
rename from agents/codex-1662.md
rename to archives/agents/2025-11-30-codex-1662.md
diff --git a/agents/codex-1663.md b/archives/agents/2025-11-30-codex-1663.md
similarity index 100%
rename from agents/codex-1663.md
rename to archives/agents/2025-11-30-codex-1663.md
diff --git a/agents/codex-1664.md b/archives/agents/2025-11-30-codex-1664.md
similarity index 100%
rename from agents/codex-1664.md
rename to archives/agents/2025-11-30-codex-1664.md
diff --git a/agents/codex-1665.md b/archives/agents/2025-11-30-codex-1665.md
similarity index 100%
rename from agents/codex-1665.md
rename to archives/agents/2025-11-30-codex-1665.md
diff --git a/agents/codex-1666.md b/archives/agents/2025-11-30-codex-1666.md
similarity index 100%
rename from agents/codex-1666.md
rename to archives/agents/2025-11-30-codex-1666.md
diff --git a/agents/codex-1667.md b/archives/agents/2025-11-30-codex-1667.md
similarity index 100%
rename from agents/codex-1667.md
rename to archives/agents/2025-11-30-codex-1667.md
diff --git a/agents/codex-1668.md b/archives/agents/2025-11-30-codex-1668.md
similarity index 100%
rename from agents/codex-1668.md
rename to archives/agents/2025-11-30-codex-1668.md
diff --git a/agents/codex-1669.md b/archives/agents/2025-11-30-codex-1669.md
similarity index 100%
rename from agents/codex-1669.md
rename to archives/agents/2025-11-30-codex-1669.md
diff --git a/agents/codex-1674.md b/archives/agents/2025-11-30-codex-1674.md
similarity index 100%
rename from agents/codex-1674.md
rename to archives/agents/2025-11-30-codex-1674.md
diff --git a/agents/codex-1675.md b/archives/agents/2025-11-30-codex-1675.md
similarity index 100%
rename from agents/codex-1675.md
rename to archives/agents/2025-11-30-codex-1675.md
diff --git a/agents/codex-1676.md b/archives/agents/2025-11-30-codex-1676.md
similarity index 100%
rename from agents/codex-1676.md
rename to archives/agents/2025-11-30-codex-1676.md
diff --git a/agents/codex-1677.md b/archives/agents/2025-11-30-codex-1677.md
similarity index 100%
rename from agents/codex-1677.md
rename to archives/agents/2025-11-30-codex-1677.md
diff --git a/agents/codex-1678.md b/archives/agents/2025-11-30-codex-1678.md
similarity index 100%
rename from agents/codex-1678.md
rename to archives/agents/2025-11-30-codex-1678.md
diff --git a/agents/codex-1679.md b/archives/agents/2025-11-30-codex-1679.md
similarity index 100%
rename from agents/codex-1679.md
rename to archives/agents/2025-11-30-codex-1679.md
diff --git a/agents/codex-1680.md b/archives/agents/2025-11-30-codex-1680.md
similarity index 100%
rename from agents/codex-1680.md
rename to archives/agents/2025-11-30-codex-1680.md
diff --git a/agents/codex-1681.md b/archives/agents/2025-11-30-codex-1681.md
similarity index 100%
rename from agents/codex-1681.md
rename to archives/agents/2025-11-30-codex-1681.md
diff --git a/agents/codex-1682.md b/archives/agents/2025-11-30-codex-1682.md
similarity index 100%
rename from agents/codex-1682.md
rename to archives/agents/2025-11-30-codex-1682.md
diff --git a/agents/codex-1683.md b/archives/agents/2025-11-30-codex-1683.md
similarity index 100%
rename from agents/codex-1683.md
rename to archives/agents/2025-11-30-codex-1683.md
diff --git a/agents/codex-1684.md b/archives/agents/2025-11-30-codex-1684.md
similarity index 100%
rename from agents/codex-1684.md
rename to archives/agents/2025-11-30-codex-1684.md
diff --git a/agents/codex-1685.md b/archives/agents/2025-11-30-codex-1685.md
similarity index 100%
rename from agents/codex-1685.md
rename to archives/agents/2025-11-30-codex-1685.md
diff --git a/agents/codex-1686.md b/archives/agents/2025-11-30-codex-1686.md
similarity index 100%
rename from agents/codex-1686.md
rename to archives/agents/2025-11-30-codex-1686.md
diff --git a/agents/codex-1687.md b/archives/agents/2025-11-30-codex-1687.md
similarity index 100%
rename from agents/codex-1687.md
rename to archives/agents/2025-11-30-codex-1687.md
diff --git a/agents/codex-1688.md b/archives/agents/2025-11-30-codex-1688.md
similarity index 100%
rename from agents/codex-1688.md
rename to archives/agents/2025-11-30-codex-1688.md
diff --git a/agents/codex-2190.md b/archives/agents/2025-11-30-codex-2190.md
similarity index 100%
rename from agents/codex-2190.md
rename to archives/agents/2025-11-30-codex-2190.md
diff --git a/agents/codex-2191.md b/archives/agents/2025-11-30-codex-2191.md
similarity index 100%
rename from agents/codex-2191.md
rename to archives/agents/2025-11-30-codex-2191.md
diff --git a/agents/codex-2192.md b/archives/agents/2025-11-30-codex-2192.md
similarity index 100%
rename from agents/codex-2192.md
rename to archives/agents/2025-11-30-codex-2192.md
diff --git a/agents/codex-2193.md b/archives/agents/2025-11-30-codex-2193.md
similarity index 100%
rename from agents/codex-2193.md
rename to archives/agents/2025-11-30-codex-2193.md
diff --git a/agents/codex-2194.md b/archives/agents/2025-11-30-codex-2194.md
similarity index 100%
rename from agents/codex-2194.md
rename to archives/agents/2025-11-30-codex-2194.md
diff --git a/agents/codex-2195.md b/archives/agents/2025-11-30-codex-2195.md
similarity index 100%
rename from agents/codex-2195.md
rename to archives/agents/2025-11-30-codex-2195.md
diff --git a/agents/codex-2196.md b/archives/agents/2025-11-30-codex-2196.md
similarity index 100%
rename from agents/codex-2196.md
rename to archives/agents/2025-11-30-codex-2196.md
diff --git a/agents/codex-2197.md b/archives/agents/2025-11-30-codex-2197.md
similarity index 100%
rename from agents/codex-2197.md
rename to archives/agents/2025-11-30-codex-2197.md
diff --git a/agents/codex-2198.md b/archives/agents/2025-11-30-codex-2198.md
similarity index 100%
rename from agents/codex-2198.md
rename to archives/agents/2025-11-30-codex-2198.md
diff --git a/agents/codex-2199.md b/archives/agents/2025-11-30-codex-2199.md
similarity index 100%
rename from agents/codex-2199.md
rename to archives/agents/2025-11-30-codex-2199.md
diff --git a/agents/codex-2200.md b/archives/agents/2025-11-30-codex-2200.md
similarity index 100%
rename from agents/codex-2200.md
rename to archives/agents/2025-11-30-codex-2200.md
diff --git a/agents/codex-2201.md b/archives/agents/2025-11-30-codex-2201.md
similarity index 100%
rename from agents/codex-2201.md
rename to archives/agents/2025-11-30-codex-2201.md
diff --git a/agents/codex-2202.md b/archives/agents/2025-11-30-codex-2202.md
similarity index 100%
rename from agents/codex-2202.md
rename to archives/agents/2025-11-30-codex-2202.md
diff --git a/agents/codex-2376.md b/archives/agents/2025-11-30-codex-2376.md
similarity index 100%
rename from agents/codex-2376.md
rename to archives/agents/2025-11-30-codex-2376.md
diff --git a/agents/codex-2377.md b/archives/agents/2025-11-30-codex-2377.md
similarity index 100%
rename from agents/codex-2377.md
rename to archives/agents/2025-11-30-codex-2377.md
diff --git a/agents/codex-2378.md b/archives/agents/2025-11-30-codex-2378.md
similarity index 100%
rename from agents/codex-2378.md
rename to archives/agents/2025-11-30-codex-2378.md
diff --git a/agents/codex-2379.md b/archives/agents/2025-11-30-codex-2379.md
similarity index 100%
rename from agents/codex-2379.md
rename to archives/agents/2025-11-30-codex-2379.md
diff --git a/agents/codex-2380.md b/archives/agents/2025-11-30-codex-2380.md
similarity index 100%
rename from agents/codex-2380.md
rename to archives/agents/2025-11-30-codex-2380.md
diff --git a/agents/codex-2381.md b/archives/agents/2025-11-30-codex-2381.md
similarity index 100%
rename from agents/codex-2381.md
rename to archives/agents/2025-11-30-codex-2381.md
diff --git a/agents/codex-2382.md b/archives/agents/2025-11-30-codex-2382.md
similarity index 100%
rename from agents/codex-2382.md
rename to archives/agents/2025-11-30-codex-2382.md
diff --git a/agents/codex-2383.md b/archives/agents/2025-11-30-codex-2383.md
similarity index 100%
rename from agents/codex-2383.md
rename to archives/agents/2025-11-30-codex-2383.md
diff --git a/agents/codex-2384.md b/archives/agents/2025-11-30-codex-2384.md
similarity index 100%
rename from agents/codex-2384.md
rename to archives/agents/2025-11-30-codex-2384.md
diff --git a/agents/codex-2385.md b/archives/agents/2025-11-30-codex-2385.md
similarity index 100%
rename from agents/codex-2385.md
rename to archives/agents/2025-11-30-codex-2385.md
diff --git a/agents/codex-2386.md b/archives/agents/2025-11-30-codex-2386.md
similarity index 100%
rename from agents/codex-2386.md
rename to archives/agents/2025-11-30-codex-2386.md
diff --git a/agents/codex-2433.md b/archives/agents/2025-11-30-codex-2433.md
similarity index 100%
rename from agents/codex-2433.md
rename to archives/agents/2025-11-30-codex-2433.md
diff --git a/agents/codex-2434.md b/archives/agents/2025-11-30-codex-2434.md
similarity index 100%
rename from agents/codex-2434.md
rename to archives/agents/2025-11-30-codex-2434.md
diff --git a/agents/codex-2435.md b/archives/agents/2025-11-30-codex-2435.md
similarity index 100%
rename from agents/codex-2435.md
rename to archives/agents/2025-11-30-codex-2435.md
diff --git a/agents/codex-2436.md b/archives/agents/2025-11-30-codex-2436.md
similarity index 100%
rename from agents/codex-2436.md
rename to archives/agents/2025-11-30-codex-2436.md
diff --git a/agents/codex-2437.md b/archives/agents/2025-11-30-codex-2437.md
similarity index 100%
rename from agents/codex-2437.md
rename to archives/agents/2025-11-30-codex-2437.md
diff --git a/agents/codex-2438.md b/archives/agents/2025-11-30-codex-2438.md
similarity index 100%
rename from agents/codex-2438.md
rename to archives/agents/2025-11-30-codex-2438.md
diff --git a/agents/codex-2439.md b/archives/agents/2025-11-30-codex-2439.md
similarity index 100%
rename from agents/codex-2439.md
rename to archives/agents/2025-11-30-codex-2439.md
diff --git a/agents/codex-2461.md b/archives/agents/2025-11-30-codex-2461.md
similarity index 100%
rename from agents/codex-2461.md
rename to archives/agents/2025-11-30-codex-2461.md
diff --git a/agents/codex-2462.md b/archives/agents/2025-11-30-codex-2462.md
similarity index 100%
rename from agents/codex-2462.md
rename to archives/agents/2025-11-30-codex-2462.md
diff --git a/agents/codex-2463.md b/archives/agents/2025-11-30-codex-2463.md
similarity index 100%
rename from agents/codex-2463.md
rename to archives/agents/2025-11-30-codex-2463.md
diff --git a/agents/codex-2464.md b/archives/agents/2025-11-30-codex-2464.md
similarity index 100%
rename from agents/codex-2464.md
rename to archives/agents/2025-11-30-codex-2464.md
diff --git a/agents/codex-2465.md b/archives/agents/2025-11-30-codex-2465.md
similarity index 100%
rename from agents/codex-2465.md
rename to archives/agents/2025-11-30-codex-2465.md
diff --git a/agents/codex-2466.md b/archives/agents/2025-11-30-codex-2466.md
similarity index 100%
rename from agents/codex-2466.md
rename to archives/agents/2025-11-30-codex-2466.md
diff --git a/agents/codex-2492.md b/archives/agents/2025-11-30-codex-2492.md
similarity index 100%
rename from agents/codex-2492.md
rename to archives/agents/2025-11-30-codex-2492.md
diff --git a/agents/codex-2493.md b/archives/agents/2025-11-30-codex-2493.md
similarity index 100%
rename from agents/codex-2493.md
rename to archives/agents/2025-11-30-codex-2493.md
diff --git a/agents/codex-2494.md b/archives/agents/2025-11-30-codex-2494.md
similarity index 100%
rename from agents/codex-2494.md
rename to archives/agents/2025-11-30-codex-2494.md
diff --git a/agents/codex-2495.md b/archives/agents/2025-11-30-codex-2495.md
similarity index 100%
rename from agents/codex-2495.md
rename to archives/agents/2025-11-30-codex-2495.md
diff --git a/agents/codex-2496.md b/archives/agents/2025-11-30-codex-2496.md
similarity index 100%
rename from agents/codex-2496.md
rename to archives/agents/2025-11-30-codex-2496.md
diff --git a/agents/codex-2497.md b/archives/agents/2025-11-30-codex-2497.md
similarity index 100%
rename from agents/codex-2497.md
rename to archives/agents/2025-11-30-codex-2497.md
diff --git a/agents/codex-2498.md b/archives/agents/2025-11-30-codex-2498.md
similarity index 100%
rename from agents/codex-2498.md
rename to archives/agents/2025-11-30-codex-2498.md
diff --git a/agents/codex-2523.md b/archives/agents/2025-11-30-codex-2523.md
similarity index 100%
rename from agents/codex-2523.md
rename to archives/agents/2025-11-30-codex-2523.md
diff --git a/agents/codex-2524.md b/archives/agents/2025-11-30-codex-2524.md
similarity index 100%
rename from agents/codex-2524.md
rename to archives/agents/2025-11-30-codex-2524.md
diff --git a/agents/codex-2525.md b/archives/agents/2025-11-30-codex-2525.md
similarity index 100%
rename from agents/codex-2525.md
rename to archives/agents/2025-11-30-codex-2525.md
diff --git a/agents/codex-2526.md b/archives/agents/2025-11-30-codex-2526.md
similarity index 100%
rename from agents/codex-2526.md
rename to archives/agents/2025-11-30-codex-2526.md
diff --git a/agents/codex-2527.md b/archives/agents/2025-11-30-codex-2527.md
similarity index 100%
rename from agents/codex-2527.md
rename to archives/agents/2025-11-30-codex-2527.md
diff --git a/agents/codex-2528.md b/archives/agents/2025-11-30-codex-2528.md
similarity index 100%
rename from agents/codex-2528.md
rename to archives/agents/2025-11-30-codex-2528.md
diff --git a/agents/codex-2529.md b/archives/agents/2025-11-30-codex-2529.md
similarity index 100%
rename from agents/codex-2529.md
rename to archives/agents/2025-11-30-codex-2529.md
diff --git a/agents/codex-2560.md b/archives/agents/2025-11-30-codex-2560.md
similarity index 100%
rename from agents/codex-2560.md
rename to archives/agents/2025-11-30-codex-2560.md
diff --git a/agents/codex-2561.md b/archives/agents/2025-11-30-codex-2561.md
similarity index 100%
rename from agents/codex-2561.md
rename to archives/agents/2025-11-30-codex-2561.md
diff --git a/agents/codex-2562.md b/archives/agents/2025-11-30-codex-2562.md
similarity index 100%
rename from agents/codex-2562.md
rename to archives/agents/2025-11-30-codex-2562.md
diff --git a/agents/codex-2563.md b/archives/agents/2025-11-30-codex-2563.md
similarity index 100%
rename from agents/codex-2563.md
rename to archives/agents/2025-11-30-codex-2563.md
diff --git a/agents/codex-2564.md b/archives/agents/2025-11-30-codex-2564.md
similarity index 100%
rename from agents/codex-2564.md
rename to archives/agents/2025-11-30-codex-2564.md
diff --git a/agents/codex-2565.md b/archives/agents/2025-11-30-codex-2565.md
similarity index 100%
rename from agents/codex-2565.md
rename to archives/agents/2025-11-30-codex-2565.md
diff --git a/agents/codex-2566.md b/archives/agents/2025-11-30-codex-2566.md
similarity index 100%
rename from agents/codex-2566.md
rename to archives/agents/2025-11-30-codex-2566.md
diff --git a/agents/codex-2567.md b/archives/agents/2025-11-30-codex-2567.md
similarity index 100%
rename from agents/codex-2567.md
rename to archives/agents/2025-11-30-codex-2567.md
diff --git a/agents/codex-2609.md b/archives/agents/2025-11-30-codex-2609.md
similarity index 100%
rename from agents/codex-2609.md
rename to archives/agents/2025-11-30-codex-2609.md
diff --git a/agents/codex-2610.md b/archives/agents/2025-11-30-codex-2610.md
similarity index 100%
rename from agents/codex-2610.md
rename to archives/agents/2025-11-30-codex-2610.md
diff --git a/agents/codex-2611.md b/archives/agents/2025-11-30-codex-2611.md
similarity index 100%
rename from agents/codex-2611.md
rename to archives/agents/2025-11-30-codex-2611.md
diff --git a/agents/codex-2612.md b/archives/agents/2025-11-30-codex-2612.md
similarity index 100%
rename from agents/codex-2612.md
rename to archives/agents/2025-11-30-codex-2612.md
diff --git a/agents/codex-2614.md b/archives/agents/2025-11-30-codex-2614.md
similarity index 100%
rename from agents/codex-2614.md
rename to archives/agents/2025-11-30-codex-2614.md
diff --git a/agents/codex-2615.md b/archives/agents/2025-11-30-codex-2615.md
similarity index 100%
rename from agents/codex-2615.md
rename to archives/agents/2025-11-30-codex-2615.md
diff --git a/agents/codex-2616.md b/archives/agents/2025-11-30-codex-2616.md
similarity index 100%
rename from agents/codex-2616.md
rename to archives/agents/2025-11-30-codex-2616.md
diff --git a/agents/codex-2617.md b/archives/agents/2025-11-30-codex-2617.md
similarity index 100%
rename from agents/codex-2617.md
rename to archives/agents/2025-11-30-codex-2617.md
diff --git a/agents/codex-2618.md b/archives/agents/2025-11-30-codex-2618.md
similarity index 100%
rename from agents/codex-2618.md
rename to archives/agents/2025-11-30-codex-2618.md
diff --git a/agents/codex-2649.md b/archives/agents/2025-11-30-codex-2649.md
similarity index 100%
rename from agents/codex-2649.md
rename to archives/agents/2025-11-30-codex-2649.md
diff --git a/agents/codex-2650.md b/archives/agents/2025-11-30-codex-2650.md
similarity index 100%
rename from agents/codex-2650.md
rename to archives/agents/2025-11-30-codex-2650.md
diff --git a/agents/codex-2651.md b/archives/agents/2025-11-30-codex-2651.md
similarity index 100%
rename from agents/codex-2651.md
rename to archives/agents/2025-11-30-codex-2651.md
diff --git a/agents/codex-2652.md b/archives/agents/2025-11-30-codex-2652.md
similarity index 100%
rename from agents/codex-2652.md
rename to archives/agents/2025-11-30-codex-2652.md
diff --git a/agents/codex-2653.md b/archives/agents/2025-11-30-codex-2653.md
similarity index 100%
rename from agents/codex-2653.md
rename to archives/agents/2025-11-30-codex-2653.md
diff --git a/agents/codex-2654.md b/archives/agents/2025-11-30-codex-2654.md
similarity index 100%
rename from agents/codex-2654.md
rename to archives/agents/2025-11-30-codex-2654.md
diff --git a/agents/codex-2655.md b/archives/agents/2025-11-30-codex-2655.md
similarity index 100%
rename from agents/codex-2655.md
rename to archives/agents/2025-11-30-codex-2655.md
diff --git a/agents/codex-2656.md b/archives/agents/2025-11-30-codex-2656.md
similarity index 100%
rename from agents/codex-2656.md
rename to archives/agents/2025-11-30-codex-2656.md
diff --git a/agents/codex-2680.md b/archives/agents/2025-11-30-codex-2680.md
similarity index 100%
rename from agents/codex-2680.md
rename to archives/agents/2025-11-30-codex-2680.md
diff --git a/agents/codex-2681.md b/archives/agents/2025-11-30-codex-2681.md
similarity index 100%
rename from agents/codex-2681.md
rename to archives/agents/2025-11-30-codex-2681.md
diff --git a/agents/codex-2683.md b/archives/agents/2025-11-30-codex-2683.md
similarity index 100%
rename from agents/codex-2683.md
rename to archives/agents/2025-11-30-codex-2683.md
diff --git a/agents/codex-2685.md b/archives/agents/2025-11-30-codex-2685.md
similarity index 100%
rename from agents/codex-2685.md
rename to archives/agents/2025-11-30-codex-2685.md
diff --git a/agents/codex-2686.md b/archives/agents/2025-11-30-codex-2686.md
similarity index 100%
rename from agents/codex-2686.md
rename to archives/agents/2025-11-30-codex-2686.md
diff --git a/agents/codex-2687.md b/archives/agents/2025-11-30-codex-2687.md
similarity index 100%
rename from agents/codex-2687.md
rename to archives/agents/2025-11-30-codex-2687.md
diff --git a/agents/codex-2688.md b/archives/agents/2025-11-30-codex-2688.md
similarity index 100%
rename from agents/codex-2688.md
rename to archives/agents/2025-11-30-codex-2688.md
diff --git a/agents/codex-2718.md b/archives/agents/2025-11-30-codex-2718.md
similarity index 100%
rename from agents/codex-2718.md
rename to archives/agents/2025-11-30-codex-2718.md
diff --git a/agents/codex-2719.md b/archives/agents/2025-11-30-codex-2719.md
similarity index 100%
rename from agents/codex-2719.md
rename to archives/agents/2025-11-30-codex-2719.md
diff --git a/agents/codex-2720.md b/archives/agents/2025-11-30-codex-2720.md
similarity index 100%
rename from agents/codex-2720.md
rename to archives/agents/2025-11-30-codex-2720.md
diff --git a/agents/codex-2721.md b/archives/agents/2025-11-30-codex-2721.md
similarity index 100%
rename from agents/codex-2721.md
rename to archives/agents/2025-11-30-codex-2721.md
diff --git a/agents/codex-2722.md b/archives/agents/2025-11-30-codex-2722.md
similarity index 100%
rename from agents/codex-2722.md
rename to archives/agents/2025-11-30-codex-2722.md
diff --git a/agents/codex-2723.md b/archives/agents/2025-11-30-codex-2723.md
similarity index 100%
rename from agents/codex-2723.md
rename to archives/agents/2025-11-30-codex-2723.md
diff --git a/agents/codex-2724.md b/archives/agents/2025-11-30-codex-2724.md
similarity index 100%
rename from agents/codex-2724.md
rename to archives/agents/2025-11-30-codex-2724.md
diff --git a/agents/codex-2727.md b/archives/agents/2025-11-30-codex-2727.md
similarity index 100%
rename from agents/codex-2727.md
rename to archives/agents/2025-11-30-codex-2727.md
diff --git a/agents/codex-2728.md b/archives/agents/2025-11-30-codex-2728.md
similarity index 100%
rename from agents/codex-2728.md
rename to archives/agents/2025-11-30-codex-2728.md
diff --git a/agents/codex-2730.md b/archives/agents/2025-11-30-codex-2730.md
similarity index 100%
rename from agents/codex-2730.md
rename to archives/agents/2025-11-30-codex-2730.md
diff --git a/agents/codex-2731.md b/archives/agents/2025-11-30-codex-2731.md
similarity index 100%
rename from agents/codex-2731.md
rename to archives/agents/2025-11-30-codex-2731.md
diff --git a/agents/codex-2732.md b/archives/agents/2025-11-30-codex-2732.md
similarity index 100%
rename from agents/codex-2732.md
rename to archives/agents/2025-11-30-codex-2732.md
diff --git a/agents/codex-2733.md b/archives/agents/2025-11-30-codex-2733.md
similarity index 100%
rename from agents/codex-2733.md
rename to archives/agents/2025-11-30-codex-2733.md
diff --git a/agents/codex-2736.md b/archives/agents/2025-11-30-codex-2736.md
similarity index 100%
rename from agents/codex-2736.md
rename to archives/agents/2025-11-30-codex-2736.md
diff --git a/agents/codex-2739.md b/archives/agents/2025-11-30-codex-2739.md
similarity index 100%
rename from agents/codex-2739.md
rename to archives/agents/2025-11-30-codex-2739.md
diff --git a/agents/codex-2740.md b/archives/agents/2025-11-30-codex-2740.md
similarity index 100%
rename from agents/codex-2740.md
rename to archives/agents/2025-11-30-codex-2740.md
diff --git a/agents/codex-2801.md b/archives/agents/2025-11-30-codex-2801.md
similarity index 100%
rename from agents/codex-2801.md
rename to archives/agents/2025-11-30-codex-2801.md
diff --git a/agents/codex-2802.md b/archives/agents/2025-11-30-codex-2802.md
similarity index 100%
rename from agents/codex-2802.md
rename to archives/agents/2025-11-30-codex-2802.md
diff --git a/agents/codex-2811.md b/archives/agents/2025-11-30-codex-2811.md
similarity index 100%
rename from agents/codex-2811.md
rename to archives/agents/2025-11-30-codex-2811.md
diff --git a/agents/codex-2812.md b/archives/agents/2025-11-30-codex-2812.md
similarity index 100%
rename from agents/codex-2812.md
rename to archives/agents/2025-11-30-codex-2812.md
diff --git a/agents/codex-2813.md b/archives/agents/2025-11-30-codex-2813.md
similarity index 100%
rename from agents/codex-2813.md
rename to archives/agents/2025-11-30-codex-2813.md
diff --git a/agents/codex-2814.md b/archives/agents/2025-11-30-codex-2814.md
similarity index 100%
rename from agents/codex-2814.md
rename to archives/agents/2025-11-30-codex-2814.md
diff --git a/agents/codex-2815.md b/archives/agents/2025-11-30-codex-2815.md
similarity index 100%
rename from agents/codex-2815.md
rename to archives/agents/2025-11-30-codex-2815.md
diff --git a/agents/codex-2816.md b/archives/agents/2025-11-30-codex-2816.md
similarity index 100%
rename from agents/codex-2816.md
rename to archives/agents/2025-11-30-codex-2816.md
diff --git a/agents/codex-2820.md b/archives/agents/2025-11-30-codex-2820.md
similarity index 100%
rename from agents/codex-2820.md
rename to archives/agents/2025-11-30-codex-2820.md
diff --git a/agents/codex-2821.md b/archives/agents/2025-11-30-codex-2821.md
similarity index 100%
rename from agents/codex-2821.md
rename to archives/agents/2025-11-30-codex-2821.md
diff --git a/agents/codex-2822.md b/archives/agents/2025-11-30-codex-2822.md
similarity index 100%
rename from agents/codex-2822.md
rename to archives/agents/2025-11-30-codex-2822.md
diff --git a/agents/codex-2823.md b/archives/agents/2025-11-30-codex-2823.md
similarity index 100%
rename from agents/codex-2823.md
rename to archives/agents/2025-11-30-codex-2823.md
diff --git a/agents/codex-2846.md b/archives/agents/2025-11-30-codex-2846.md
similarity index 100%
rename from agents/codex-2846.md
rename to archives/agents/2025-11-30-codex-2846.md
diff --git a/agents/codex-2847.md b/archives/agents/2025-11-30-codex-2847.md
similarity index 100%
rename from agents/codex-2847.md
rename to archives/agents/2025-11-30-codex-2847.md
diff --git a/agents/codex-2848.md b/archives/agents/2025-11-30-codex-2848.md
similarity index 100%
rename from agents/codex-2848.md
rename to archives/agents/2025-11-30-codex-2848.md
diff --git a/agents/codex-2849.md b/archives/agents/2025-11-30-codex-2849.md
similarity index 100%
rename from agents/codex-2849.md
rename to archives/agents/2025-11-30-codex-2849.md
diff --git a/agents/codex-2850.md b/archives/agents/2025-11-30-codex-2850.md
similarity index 100%
rename from agents/codex-2850.md
rename to archives/agents/2025-11-30-codex-2850.md
diff --git a/agents/codex-2851.md b/archives/agents/2025-11-30-codex-2851.md
similarity index 100%
rename from agents/codex-2851.md
rename to archives/agents/2025-11-30-codex-2851.md
diff --git a/agents/codex-2853.md b/archives/agents/2025-11-30-codex-2853.md
similarity index 100%
rename from agents/codex-2853.md
rename to archives/agents/2025-11-30-codex-2853.md
diff --git a/agents/codex-2854.md b/archives/agents/2025-11-30-codex-2854.md
similarity index 100%
rename from agents/codex-2854.md
rename to archives/agents/2025-11-30-codex-2854.md
diff --git a/agents/codex-2878.md b/archives/agents/2025-11-30-codex-2878.md
similarity index 100%
rename from agents/codex-2878.md
rename to archives/agents/2025-11-30-codex-2878.md
diff --git a/agents/codex-2882.md b/archives/agents/2025-11-30-codex-2882.md
similarity index 100%
rename from agents/codex-2882.md
rename to archives/agents/2025-11-30-codex-2882.md
diff --git a/agents/codex-2883.md b/archives/agents/2025-11-30-codex-2883.md
similarity index 100%
rename from agents/codex-2883.md
rename to archives/agents/2025-11-30-codex-2883.md
diff --git a/agents/codex-2884.md b/archives/agents/2025-11-30-codex-2884.md
similarity index 100%
rename from agents/codex-2884.md
rename to archives/agents/2025-11-30-codex-2884.md
diff --git a/agents/codex-2885.md b/archives/agents/2025-11-30-codex-2885.md
similarity index 100%
rename from agents/codex-2885.md
rename to archives/agents/2025-11-30-codex-2885.md
diff --git a/agents/codex-2886.md b/archives/agents/2025-11-30-codex-2886.md
similarity index 100%
rename from agents/codex-2886.md
rename to archives/agents/2025-11-30-codex-2886.md
diff --git a/agents/codex-2913.md b/archives/agents/2025-11-30-codex-2913.md
similarity index 100%
rename from agents/codex-2913.md
rename to archives/agents/2025-11-30-codex-2913.md
diff --git a/agents/codex-2914.md b/archives/agents/2025-11-30-codex-2914.md
similarity index 100%
rename from agents/codex-2914.md
rename to archives/agents/2025-11-30-codex-2914.md
diff --git a/agents/codex-2915.md b/archives/agents/2025-11-30-codex-2915.md
similarity index 100%
rename from agents/codex-2915.md
rename to archives/agents/2025-11-30-codex-2915.md
diff --git a/agents/codex-2916.md b/archives/agents/2025-11-30-codex-2916.md
similarity index 100%
rename from agents/codex-2916.md
rename to archives/agents/2025-11-30-codex-2916.md
diff --git a/agents/codex-2917.md b/archives/agents/2025-11-30-codex-2917.md
similarity index 100%
rename from agents/codex-2917.md
rename to archives/agents/2025-11-30-codex-2917.md
diff --git a/agents/codex-2918.md b/archives/agents/2025-11-30-codex-2918.md
similarity index 100%
rename from agents/codex-2918.md
rename to archives/agents/2025-11-30-codex-2918.md
diff --git a/agents/codex-2919.md b/archives/agents/2025-11-30-codex-2919.md
similarity index 100%
rename from agents/codex-2919.md
rename to archives/agents/2025-11-30-codex-2919.md
diff --git a/agents/codex-2940.md b/archives/agents/2025-11-30-codex-2940.md
similarity index 100%
rename from agents/codex-2940.md
rename to archives/agents/2025-11-30-codex-2940.md
diff --git a/agents/codex-2942.md b/archives/agents/2025-11-30-codex-2942.md
similarity index 100%
rename from agents/codex-2942.md
rename to archives/agents/2025-11-30-codex-2942.md
diff --git a/agents/codex-2945.md b/archives/agents/2025-11-30-codex-2945.md
similarity index 100%
rename from agents/codex-2945.md
rename to archives/agents/2025-11-30-codex-2945.md
diff --git a/agents/codex-2955.md b/archives/agents/2025-11-30-codex-2955.md
similarity index 100%
rename from agents/codex-2955.md
rename to archives/agents/2025-11-30-codex-2955.md
diff --git a/agents/codex-2957.md b/archives/agents/2025-11-30-codex-2957.md
similarity index 100%
rename from agents/codex-2957.md
rename to archives/agents/2025-11-30-codex-2957.md
diff --git a/agents/codex-2958.md b/archives/agents/2025-11-30-codex-2958.md
similarity index 100%
rename from agents/codex-2958.md
rename to archives/agents/2025-11-30-codex-2958.md
diff --git a/agents/codex-2959.md b/archives/agents/2025-11-30-codex-2959.md
similarity index 100%
rename from agents/codex-2959.md
rename to archives/agents/2025-11-30-codex-2959.md
diff --git a/agents/codex-2960.md b/archives/agents/2025-11-30-codex-2960.md
similarity index 100%
rename from agents/codex-2960.md
rename to archives/agents/2025-11-30-codex-2960.md
diff --git a/agents/codex-2961.md b/archives/agents/2025-11-30-codex-2961.md
similarity index 100%
rename from agents/codex-2961.md
rename to archives/agents/2025-11-30-codex-2961.md
diff --git a/agents/codex-2962.md b/archives/agents/2025-11-30-codex-2962.md
similarity index 100%
rename from agents/codex-2962.md
rename to archives/agents/2025-11-30-codex-2962.md
diff --git a/agents/codex-2963.md b/archives/agents/2025-11-30-codex-2963.md
similarity index 100%
rename from agents/codex-2963.md
rename to archives/agents/2025-11-30-codex-2963.md
diff --git a/agents/codex-2964.md b/archives/agents/2025-11-30-codex-2964.md
similarity index 100%
rename from agents/codex-2964.md
rename to archives/agents/2025-11-30-codex-2964.md
diff --git a/agents/codex-2994.md b/archives/agents/2025-11-30-codex-2994.md
similarity index 100%
rename from agents/codex-2994.md
rename to archives/agents/2025-11-30-codex-2994.md
diff --git a/agents/codex-2995.md b/archives/agents/2025-11-30-codex-2995.md
similarity index 100%
rename from agents/codex-2995.md
rename to archives/agents/2025-11-30-codex-2995.md
diff --git a/agents/codex-2996.md b/archives/agents/2025-11-30-codex-2996.md
similarity index 100%
rename from agents/codex-2996.md
rename to archives/agents/2025-11-30-codex-2996.md
diff --git a/agents/codex-2997.md b/archives/agents/2025-11-30-codex-2997.md
similarity index 100%
rename from agents/codex-2997.md
rename to archives/agents/2025-11-30-codex-2997.md
diff --git a/agents/codex-2998.md b/archives/agents/2025-11-30-codex-2998.md
similarity index 100%
rename from agents/codex-2998.md
rename to archives/agents/2025-11-30-codex-2998.md
diff --git a/agents/codex-3006.md b/archives/agents/2025-11-30-codex-3006.md
similarity index 100%
rename from agents/codex-3006.md
rename to archives/agents/2025-11-30-codex-3006.md
diff --git a/agents/codex-3007.md b/archives/agents/2025-11-30-codex-3007.md
similarity index 100%
rename from agents/codex-3007.md
rename to archives/agents/2025-11-30-codex-3007.md
diff --git a/agents/codex-3008.md b/archives/agents/2025-11-30-codex-3008.md
similarity index 100%
rename from agents/codex-3008.md
rename to archives/agents/2025-11-30-codex-3008.md
diff --git a/agents/codex-3009.md b/archives/agents/2025-11-30-codex-3009.md
similarity index 100%
rename from agents/codex-3009.md
rename to archives/agents/2025-11-30-codex-3009.md
diff --git a/agents/codex-3010.md b/archives/agents/2025-11-30-codex-3010.md
similarity index 100%
rename from agents/codex-3010.md
rename to archives/agents/2025-11-30-codex-3010.md
diff --git a/agents/codex-3011.md b/archives/agents/2025-11-30-codex-3011.md
similarity index 100%
rename from agents/codex-3011.md
rename to archives/agents/2025-11-30-codex-3011.md
diff --git a/agents/codex-3013.md b/archives/agents/2025-11-30-codex-3013.md
similarity index 100%
rename from agents/codex-3013.md
rename to archives/agents/2025-11-30-codex-3013.md
diff --git a/agents/codex-3017.md b/archives/agents/2025-11-30-codex-3017.md
similarity index 100%
rename from agents/codex-3017.md
rename to archives/agents/2025-11-30-codex-3017.md
diff --git a/agents/codex-3019.md b/archives/agents/2025-11-30-codex-3019.md
similarity index 100%
rename from agents/codex-3019.md
rename to archives/agents/2025-11-30-codex-3019.md
diff --git a/agents/codex-3038.md b/archives/agents/2025-11-30-codex-3038.md
similarity index 100%
rename from agents/codex-3038.md
rename to archives/agents/2025-11-30-codex-3038.md
diff --git a/agents/codex-3039.md b/archives/agents/2025-11-30-codex-3039.md
similarity index 100%
rename from agents/codex-3039.md
rename to archives/agents/2025-11-30-codex-3039.md
diff --git a/agents/codex-3040.md b/archives/agents/2025-11-30-codex-3040.md
similarity index 100%
rename from agents/codex-3040.md
rename to archives/agents/2025-11-30-codex-3040.md
diff --git a/agents/codex-3041.md b/archives/agents/2025-11-30-codex-3041.md
similarity index 100%
rename from agents/codex-3041.md
rename to archives/agents/2025-11-30-codex-3041.md
diff --git a/agents/codex-3042.md b/archives/agents/2025-11-30-codex-3042.md
similarity index 100%
rename from agents/codex-3042.md
rename to archives/agents/2025-11-30-codex-3042.md
diff --git a/agents/codex-3053.md b/archives/agents/2025-11-30-codex-3053.md
similarity index 100%
rename from agents/codex-3053.md
rename to archives/agents/2025-11-30-codex-3053.md
diff --git a/agents/codex-3054.md b/archives/agents/2025-11-30-codex-3054.md
similarity index 100%
rename from agents/codex-3054.md
rename to archives/agents/2025-11-30-codex-3054.md
diff --git a/agents/codex-3055.md b/archives/agents/2025-11-30-codex-3055.md
similarity index 100%
rename from agents/codex-3055.md
rename to archives/agents/2025-11-30-codex-3055.md
diff --git a/agents/codex-3056.md b/archives/agents/2025-11-30-codex-3056.md
similarity index 100%
rename from agents/codex-3056.md
rename to archives/agents/2025-11-30-codex-3056.md
diff --git a/agents/codex-3057.md b/archives/agents/2025-11-30-codex-3057.md
similarity index 100%
rename from agents/codex-3057.md
rename to archives/agents/2025-11-30-codex-3057.md
diff --git a/agents/codex-3058.md b/archives/agents/2025-11-30-codex-3058.md
similarity index 100%
rename from agents/codex-3058.md
rename to archives/agents/2025-11-30-codex-3058.md
diff --git a/agents/codex-3073.md b/archives/agents/2025-11-30-codex-3073.md
similarity index 100%
rename from agents/codex-3073.md
rename to archives/agents/2025-11-30-codex-3073.md
diff --git a/agents/codex-3074.md b/archives/agents/2025-11-30-codex-3074.md
similarity index 100%
rename from agents/codex-3074.md
rename to archives/agents/2025-11-30-codex-3074.md
diff --git a/agents/codex-3075.md b/archives/agents/2025-11-30-codex-3075.md
similarity index 100%
rename from agents/codex-3075.md
rename to archives/agents/2025-11-30-codex-3075.md
diff --git a/agents/codex-3076.md b/archives/agents/2025-11-30-codex-3076.md
similarity index 100%
rename from agents/codex-3076.md
rename to archives/agents/2025-11-30-codex-3076.md
diff --git a/agents/codex-3077.md b/archives/agents/2025-11-30-codex-3077.md
similarity index 100%
rename from agents/codex-3077.md
rename to archives/agents/2025-11-30-codex-3077.md
diff --git a/agents/codex-3078.md b/archives/agents/2025-11-30-codex-3078.md
similarity index 100%
rename from agents/codex-3078.md
rename to archives/agents/2025-11-30-codex-3078.md
diff --git a/agents/codex-3085.md b/archives/agents/2025-11-30-codex-3085.md
similarity index 100%
rename from agents/codex-3085.md
rename to archives/agents/2025-11-30-codex-3085.md
diff --git a/agents/codex-3092.md b/archives/agents/2025-11-30-codex-3092.md
similarity index 100%
rename from agents/codex-3092.md
rename to archives/agents/2025-11-30-codex-3092.md
diff --git a/agents/codex-3093.md b/archives/agents/2025-11-30-codex-3093.md
similarity index 100%
rename from agents/codex-3093.md
rename to archives/agents/2025-11-30-codex-3093.md
diff --git a/agents/codex-3094.md b/archives/agents/2025-11-30-codex-3094.md
similarity index 100%
rename from agents/codex-3094.md
rename to archives/agents/2025-11-30-codex-3094.md
diff --git a/agents/codex-3095.md b/archives/agents/2025-11-30-codex-3095.md
similarity index 100%
rename from agents/codex-3095.md
rename to archives/agents/2025-11-30-codex-3095.md
diff --git a/agents/codex-3096.md b/archives/agents/2025-11-30-codex-3096.md
similarity index 100%
rename from agents/codex-3096.md
rename to archives/agents/2025-11-30-codex-3096.md
diff --git a/agents/codex-3098.md b/archives/agents/2025-11-30-codex-3098.md
similarity index 100%
rename from agents/codex-3098.md
rename to archives/agents/2025-11-30-codex-3098.md
diff --git a/agents/codex-3099.md b/archives/agents/2025-11-30-codex-3099.md
similarity index 100%
rename from agents/codex-3099.md
rename to archives/agents/2025-11-30-codex-3099.md
diff --git a/agents/codex-3100.md b/archives/agents/2025-11-30-codex-3100.md
similarity index 100%
rename from agents/codex-3100.md
rename to archives/agents/2025-11-30-codex-3100.md
diff --git a/agents/codex-3101.md b/archives/agents/2025-11-30-codex-3101.md
similarity index 100%
rename from agents/codex-3101.md
rename to archives/agents/2025-11-30-codex-3101.md
diff --git a/agents/codex-3118.md b/archives/agents/2025-11-30-codex-3118.md
similarity index 100%
rename from agents/codex-3118.md
rename to archives/agents/2025-11-30-codex-3118.md
diff --git a/agents/codex-3119.md b/archives/agents/2025-11-30-codex-3119.md
similarity index 100%
rename from agents/codex-3119.md
rename to archives/agents/2025-11-30-codex-3119.md
diff --git a/agents/codex-3122.md b/archives/agents/2025-11-30-codex-3122.md
similarity index 100%
rename from agents/codex-3122.md
rename to archives/agents/2025-11-30-codex-3122.md
diff --git a/agents/codex-3126.md b/archives/agents/2025-11-30-codex-3126.md
similarity index 100%
rename from agents/codex-3126.md
rename to archives/agents/2025-11-30-codex-3126.md
diff --git a/agents/codex-3129.md b/archives/agents/2025-11-30-codex-3129.md
similarity index 100%
rename from agents/codex-3129.md
rename to archives/agents/2025-11-30-codex-3129.md
diff --git a/agents/codex-3131.md b/archives/agents/2025-11-30-codex-3131.md
similarity index 100%
rename from agents/codex-3131.md
rename to archives/agents/2025-11-30-codex-3131.md
diff --git a/agents/codex-3135.md b/archives/agents/2025-11-30-codex-3135.md
similarity index 100%
rename from agents/codex-3135.md
rename to archives/agents/2025-11-30-codex-3135.md
diff --git a/agents/codex-3138.md b/archives/agents/2025-11-30-codex-3138.md
similarity index 100%
rename from agents/codex-3138.md
rename to archives/agents/2025-11-30-codex-3138.md
diff --git a/agents/codex-3139.md b/archives/agents/2025-11-30-codex-3139.md
similarity index 100%
rename from agents/codex-3139.md
rename to archives/agents/2025-11-30-codex-3139.md
diff --git a/agents/codex-3144.md b/archives/agents/2025-11-30-codex-3144.md
similarity index 100%
rename from agents/codex-3144.md
rename to archives/agents/2025-11-30-codex-3144.md
diff --git a/agents/codex-3149.md b/archives/agents/2025-11-30-codex-3149.md
similarity index 100%
rename from agents/codex-3149.md
rename to archives/agents/2025-11-30-codex-3149.md
diff --git a/agents/codex-3150.md b/archives/agents/2025-11-30-codex-3150.md
similarity index 100%
rename from agents/codex-3150.md
rename to archives/agents/2025-11-30-codex-3150.md
diff --git a/agents/codex-3154.md b/archives/agents/2025-11-30-codex-3154.md
similarity index 100%
rename from agents/codex-3154.md
rename to archives/agents/2025-11-30-codex-3154.md
diff --git a/agents/codex-3158.md b/archives/agents/2025-11-30-codex-3158.md
similarity index 100%
rename from agents/codex-3158.md
rename to archives/agents/2025-11-30-codex-3158.md
diff --git a/agents/codex-3160.md b/archives/agents/2025-11-30-codex-3160.md
similarity index 100%
rename from agents/codex-3160.md
rename to archives/agents/2025-11-30-codex-3160.md
diff --git a/agents/codex-3166.md b/archives/agents/2025-11-30-codex-3166.md
similarity index 100%
rename from agents/codex-3166.md
rename to archives/agents/2025-11-30-codex-3166.md
diff --git a/agents/codex-3171.md b/archives/agents/2025-11-30-codex-3171.md
similarity index 100%
rename from agents/codex-3171.md
rename to archives/agents/2025-11-30-codex-3171.md
diff --git a/agents/codex-3176.md b/archives/agents/2025-11-30-codex-3176.md
similarity index 100%
rename from agents/codex-3176.md
rename to archives/agents/2025-11-30-codex-3176.md
diff --git a/agents/codex-3179.md b/archives/agents/2025-11-30-codex-3179.md
similarity index 100%
rename from agents/codex-3179.md
rename to archives/agents/2025-11-30-codex-3179.md
diff --git a/agents/codex-3183.md b/archives/agents/2025-11-30-codex-3183.md
similarity index 100%
rename from agents/codex-3183.md
rename to archives/agents/2025-11-30-codex-3183.md
diff --git a/agents/codex-3190.md b/archives/agents/2025-11-30-codex-3190.md
similarity index 100%
rename from agents/codex-3190.md
rename to archives/agents/2025-11-30-codex-3190.md
diff --git a/agents/codex-3193.md b/archives/agents/2025-11-30-codex-3193.md
similarity index 100%
rename from agents/codex-3193.md
rename to archives/agents/2025-11-30-codex-3193.md
diff --git a/agents/codex-3196.md b/archives/agents/2025-11-30-codex-3196.md
similarity index 100%
rename from agents/codex-3196.md
rename to archives/agents/2025-11-30-codex-3196.md
diff --git a/agents/codex-3203.md b/archives/agents/2025-11-30-codex-3203.md
similarity index 100%
rename from agents/codex-3203.md
rename to archives/agents/2025-11-30-codex-3203.md
diff --git a/agents/codex-3209.md b/archives/agents/2025-11-30-codex-3209.md
similarity index 100%
rename from agents/codex-3209.md
rename to archives/agents/2025-11-30-codex-3209.md
diff --git a/agents/codex-3213.md b/archives/agents/2025-11-30-codex-3213.md
similarity index 100%
rename from agents/codex-3213.md
rename to archives/agents/2025-11-30-codex-3213.md
diff --git a/agents/codex-3216.md b/archives/agents/2025-11-30-codex-3216.md
similarity index 100%
rename from agents/codex-3216.md
rename to archives/agents/2025-11-30-codex-3216.md
diff --git a/agents/codex-3218.md b/archives/agents/2025-11-30-codex-3218.md
similarity index 100%
rename from agents/codex-3218.md
rename to archives/agents/2025-11-30-codex-3218.md
diff --git a/agents/codex-3219.md b/archives/agents/2025-11-30-codex-3219.md
similarity index 100%
rename from agents/codex-3219.md
rename to archives/agents/2025-11-30-codex-3219.md
diff --git a/agents/codex-3225.md b/archives/agents/2025-11-30-codex-3225.md
similarity index 100%
rename from agents/codex-3225.md
rename to archives/agents/2025-11-30-codex-3225.md
diff --git a/agents/codex-3227.md b/archives/agents/2025-11-30-codex-3227.md
similarity index 100%
rename from agents/codex-3227.md
rename to archives/agents/2025-11-30-codex-3227.md
diff --git a/agents/codex-3228.md b/archives/agents/2025-11-30-codex-3228.md
similarity index 100%
rename from agents/codex-3228.md
rename to archives/agents/2025-11-30-codex-3228.md
diff --git a/agents/codex-3233.md b/archives/agents/2025-11-30-codex-3233.md
similarity index 100%
rename from agents/codex-3233.md
rename to archives/agents/2025-11-30-codex-3233.md
diff --git a/agents/codex-3235.md b/archives/agents/2025-11-30-codex-3235.md
similarity index 100%
rename from agents/codex-3235.md
rename to archives/agents/2025-11-30-codex-3235.md
diff --git a/agents/codex-3237.md b/archives/agents/2025-11-30-codex-3237.md
similarity index 100%
rename from agents/codex-3237.md
rename to archives/agents/2025-11-30-codex-3237.md
diff --git a/agents/codex-3238.md b/archives/agents/2025-11-30-codex-3238.md
similarity index 100%
rename from agents/codex-3238.md
rename to archives/agents/2025-11-30-codex-3238.md
diff --git a/agents/codex-3249.md b/archives/agents/2025-11-30-codex-3249.md
similarity index 100%
rename from agents/codex-3249.md
rename to archives/agents/2025-11-30-codex-3249.md
diff --git a/agents/codex-3253.md b/archives/agents/2025-11-30-codex-3253.md
similarity index 100%
rename from agents/codex-3253.md
rename to archives/agents/2025-11-30-codex-3253.md
diff --git a/agents/codex-3254.md b/archives/agents/2025-11-30-codex-3254.md
similarity index 100%
rename from agents/codex-3254.md
rename to archives/agents/2025-11-30-codex-3254.md
diff --git a/agents/codex-3255.md b/archives/agents/2025-11-30-codex-3255.md
similarity index 100%
rename from agents/codex-3255.md
rename to archives/agents/2025-11-30-codex-3255.md
diff --git a/agents/codex-3260.md b/archives/agents/2025-11-30-codex-3260.md
similarity index 100%
rename from agents/codex-3260.md
rename to archives/agents/2025-11-30-codex-3260.md
diff --git a/agents/codex-3261.md b/archives/agents/2025-11-30-codex-3261.md
similarity index 100%
rename from agents/codex-3261.md
rename to archives/agents/2025-11-30-codex-3261.md
diff --git a/agents/codex-3266.md b/archives/agents/2025-11-30-codex-3266.md
similarity index 100%
rename from agents/codex-3266.md
rename to archives/agents/2025-11-30-codex-3266.md
diff --git a/agents/codex-3279.md b/archives/agents/2025-11-30-codex-3279.md
similarity index 100%
rename from agents/codex-3279.md
rename to archives/agents/2025-11-30-codex-3279.md
diff --git a/agents/codex-3284.md b/archives/agents/2025-11-30-codex-3284.md
similarity index 100%
rename from agents/codex-3284.md
rename to archives/agents/2025-11-30-codex-3284.md
diff --git a/agents/codex-3309.md b/archives/agents/2025-11-30-codex-3309.md
similarity index 100%
rename from agents/codex-3309.md
rename to archives/agents/2025-11-30-codex-3309.md
diff --git a/agents/codex-3318.md b/archives/agents/2025-11-30-codex-3318.md
similarity index 100%
rename from agents/codex-3318.md
rename to archives/agents/2025-11-30-codex-3318.md
diff --git a/agents/codex-3319.md b/archives/agents/2025-11-30-codex-3319.md
similarity index 100%
rename from agents/codex-3319.md
rename to archives/agents/2025-11-30-codex-3319.md
diff --git a/agents/codex-3321.md b/archives/agents/2025-11-30-codex-3321.md
similarity index 100%
rename from agents/codex-3321.md
rename to archives/agents/2025-11-30-codex-3321.md
diff --git a/agents/codex-3331.md b/archives/agents/2025-11-30-codex-3331.md
similarity index 100%
rename from agents/codex-3331.md
rename to archives/agents/2025-11-30-codex-3331.md
diff --git a/agents/codex-3333.md b/archives/agents/2025-11-30-codex-3333.md
similarity index 100%
rename from agents/codex-3333.md
rename to archives/agents/2025-11-30-codex-3333.md
diff --git a/agents/codex-3335.md b/archives/agents/2025-11-30-codex-3335.md
similarity index 100%
rename from agents/codex-3335.md
rename to archives/agents/2025-11-30-codex-3335.md
diff --git a/agents/codex-3352.md b/archives/agents/2025-11-30-codex-3352.md
similarity index 100%
rename from agents/codex-3352.md
rename to archives/agents/2025-11-30-codex-3352.md
diff --git a/agents/codex-3363.md b/archives/agents/2025-11-30-codex-3363.md
similarity index 100%
rename from agents/codex-3363.md
rename to archives/agents/2025-11-30-codex-3363.md
diff --git a/agents/codex-3364.md b/archives/agents/2025-11-30-codex-3364.md
similarity index 100%
rename from agents/codex-3364.md
rename to archives/agents/2025-11-30-codex-3364.md
diff --git a/agents/codex-3377.md b/archives/agents/2025-11-30-codex-3377.md
similarity index 100%
rename from agents/codex-3377.md
rename to archives/agents/2025-11-30-codex-3377.md
diff --git a/agents/codex-3380.md b/archives/agents/2025-11-30-codex-3380.md
similarity index 100%
rename from agents/codex-3380.md
rename to archives/agents/2025-11-30-codex-3380.md
diff --git a/agents/codex-3384.md b/archives/agents/2025-11-30-codex-3384.md
similarity index 100%
rename from agents/codex-3384.md
rename to archives/agents/2025-11-30-codex-3384.md
diff --git a/agents/codex-3391.md b/archives/agents/2025-11-30-codex-3391.md
similarity index 100%
rename from agents/codex-3391.md
rename to archives/agents/2025-11-30-codex-3391.md
diff --git a/agents/codex-3393.md b/archives/agents/2025-11-30-codex-3393.md
similarity index 100%
rename from agents/codex-3393.md
rename to archives/agents/2025-11-30-codex-3393.md
diff --git a/agents/codex-3397.md b/archives/agents/2025-11-30-codex-3397.md
similarity index 100%
rename from agents/codex-3397.md
rename to archives/agents/2025-11-30-codex-3397.md
diff --git a/agents/codex-3401.md b/archives/agents/2025-11-30-codex-3401.md
similarity index 100%
rename from agents/codex-3401.md
rename to archives/agents/2025-11-30-codex-3401.md
diff --git a/agents/codex-3404.md b/archives/agents/2025-11-30-codex-3404.md
similarity index 100%
rename from agents/codex-3404.md
rename to archives/agents/2025-11-30-codex-3404.md
diff --git a/agents/codex-3408.md b/archives/agents/2025-11-30-codex-3408.md
similarity index 100%
rename from agents/codex-3408.md
rename to archives/agents/2025-11-30-codex-3408.md
diff --git a/agents/codex-3412.md b/archives/agents/2025-11-30-codex-3412.md
similarity index 100%
rename from agents/codex-3412.md
rename to archives/agents/2025-11-30-codex-3412.md
diff --git a/agents/codex-3415.md b/archives/agents/2025-11-30-codex-3415.md
similarity index 100%
rename from agents/codex-3415.md
rename to archives/agents/2025-11-30-codex-3415.md
diff --git a/agents/codex-3418.md b/archives/agents/2025-11-30-codex-3418.md
similarity index 100%
rename from agents/codex-3418.md
rename to archives/agents/2025-11-30-codex-3418.md
diff --git a/agents/codex-3420.md b/archives/agents/2025-11-30-codex-3420.md
similarity index 100%
rename from agents/codex-3420.md
rename to archives/agents/2025-11-30-codex-3420.md
diff --git a/agents/codex-3424.md b/archives/agents/2025-11-30-codex-3424.md
similarity index 100%
rename from agents/codex-3424.md
rename to archives/agents/2025-11-30-codex-3424.md
diff --git a/agents/codex-3428.md b/archives/agents/2025-11-30-codex-3428.md
similarity index 100%
rename from agents/codex-3428.md
rename to archives/agents/2025-11-30-codex-3428.md
diff --git a/agents/codex-3431.md b/archives/agents/2025-11-30-codex-3431.md
similarity index 100%
rename from agents/codex-3431.md
rename to archives/agents/2025-11-30-codex-3431.md
diff --git a/agents/codex-3442.md b/archives/agents/2025-11-30-codex-3442.md
similarity index 100%
rename from agents/codex-3442.md
rename to archives/agents/2025-11-30-codex-3442.md
diff --git a/agents/codex-3488.md b/archives/agents/2025-11-30-codex-3488.md
similarity index 100%
rename from agents/codex-3488.md
rename to archives/agents/2025-11-30-codex-3488.md
diff --git a/agents/codex-3490.md b/archives/agents/2025-11-30-codex-3490.md
similarity index 100%
rename from agents/codex-3490.md
rename to archives/agents/2025-11-30-codex-3490.md
diff --git a/agents/codex-3498.md b/archives/agents/2025-11-30-codex-3498.md
similarity index 100%
rename from agents/codex-3498.md
rename to archives/agents/2025-11-30-codex-3498.md
diff --git a/agents/codex-3499.md b/archives/agents/2025-11-30-codex-3499.md
similarity index 100%
rename from agents/codex-3499.md
rename to archives/agents/2025-11-30-codex-3499.md
diff --git a/agents/codex-3500.md b/archives/agents/2025-11-30-codex-3500.md
similarity index 100%
rename from agents/codex-3500.md
rename to archives/agents/2025-11-30-codex-3500.md
diff --git a/agents/codex-3504.md b/archives/agents/2025-11-30-codex-3504.md
similarity index 100%
rename from agents/codex-3504.md
rename to archives/agents/2025-11-30-codex-3504.md
diff --git a/agents/codex-3505.md b/archives/agents/2025-11-30-codex-3505.md
similarity index 100%
rename from agents/codex-3505.md
rename to archives/agents/2025-11-30-codex-3505.md
diff --git a/agents/codex-3511.md b/archives/agents/2025-11-30-codex-3511.md
similarity index 100%
rename from agents/codex-3511.md
rename to archives/agents/2025-11-30-codex-3511.md
diff --git a/agents/codex-3523.md b/archives/agents/2025-11-30-codex-3523.md
similarity index 100%
rename from agents/codex-3523.md
rename to archives/agents/2025-11-30-codex-3523.md
diff --git a/agents/codex-3525.md b/archives/agents/2025-11-30-codex-3525.md
similarity index 100%
rename from agents/codex-3525.md
rename to archives/agents/2025-11-30-codex-3525.md
diff --git a/agents/codex-3527.md b/archives/agents/2025-11-30-codex-3527.md
similarity index 100%
rename from agents/codex-3527.md
rename to archives/agents/2025-11-30-codex-3527.md
diff --git a/agents/codex-3532.md b/archives/agents/2025-11-30-codex-3532.md
similarity index 100%
rename from agents/codex-3532.md
rename to archives/agents/2025-11-30-codex-3532.md
diff --git a/agents/codex-3533.md b/archives/agents/2025-11-30-codex-3533.md
similarity index 100%
rename from agents/codex-3533.md
rename to archives/agents/2025-11-30-codex-3533.md
diff --git a/agents/codex-3538.md b/archives/agents/2025-11-30-codex-3538.md
similarity index 100%
rename from agents/codex-3538.md
rename to archives/agents/2025-11-30-codex-3538.md
diff --git a/agents/codex-3544.md b/archives/agents/2025-11-30-codex-3544.md
similarity index 100%
rename from agents/codex-3544.md
rename to archives/agents/2025-11-30-codex-3544.md
diff --git a/agents/codex-3545.md b/archives/agents/2025-11-30-codex-3545.md
similarity index 100%
rename from agents/codex-3545.md
rename to archives/agents/2025-11-30-codex-3545.md
diff --git a/agents/codex-3546.md b/archives/agents/2025-11-30-codex-3546.md
similarity index 100%
rename from agents/codex-3546.md
rename to archives/agents/2025-11-30-codex-3546.md
diff --git a/agents/codex-3547.md b/archives/agents/2025-11-30-codex-3547.md
similarity index 100%
rename from agents/codex-3547.md
rename to archives/agents/2025-11-30-codex-3547.md
diff --git a/agents/codex-3552.md b/archives/agents/2025-11-30-codex-3552.md
similarity index 100%
rename from agents/codex-3552.md
rename to archives/agents/2025-11-30-codex-3552.md
diff --git a/agents/codex-3557.md b/archives/agents/2025-11-30-codex-3557.md
similarity index 100%
rename from agents/codex-3557.md
rename to archives/agents/2025-11-30-codex-3557.md
diff --git a/agents/codex-3558.md b/archives/agents/2025-11-30-codex-3558.md
similarity index 100%
rename from agents/codex-3558.md
rename to archives/agents/2025-11-30-codex-3558.md
diff --git a/agents/codex-3559.md b/archives/agents/2025-11-30-codex-3559.md
similarity index 100%
rename from agents/codex-3559.md
rename to archives/agents/2025-11-30-codex-3559.md
diff --git a/agents/codex-3581.md b/archives/agents/2025-11-30-codex-3581.md
similarity index 100%
rename from agents/codex-3581.md
rename to archives/agents/2025-11-30-codex-3581.md
diff --git a/agents/codex-3582.md b/archives/agents/2025-11-30-codex-3582.md
similarity index 100%
rename from agents/codex-3582.md
rename to archives/agents/2025-11-30-codex-3582.md
diff --git a/agents/codex-3583.md b/archives/agents/2025-11-30-codex-3583.md
similarity index 100%
rename from agents/codex-3583.md
rename to archives/agents/2025-11-30-codex-3583.md
diff --git a/agents/codex-3584.md b/archives/agents/2025-11-30-codex-3584.md
similarity index 100%
rename from agents/codex-3584.md
rename to archives/agents/2025-11-30-codex-3584.md
diff --git a/agents/codex-3585.md b/archives/agents/2025-11-30-codex-3585.md
similarity index 100%
rename from agents/codex-3585.md
rename to archives/agents/2025-11-30-codex-3585.md
diff --git a/agents/codex-3586.md b/archives/agents/2025-11-30-codex-3586.md
similarity index 100%
rename from agents/codex-3586.md
rename to archives/agents/2025-11-30-codex-3586.md
diff --git a/agents/codex-3587.md b/archives/agents/2025-11-30-codex-3587.md
similarity index 100%
rename from agents/codex-3587.md
rename to archives/agents/2025-11-30-codex-3587.md
diff --git a/agents/codex-3589.md b/archives/agents/2025-11-30-codex-3589.md
similarity index 100%
rename from agents/codex-3589.md
rename to archives/agents/2025-11-30-codex-3589.md
diff --git a/agents/codex-3590.md b/archives/agents/2025-11-30-codex-3590.md
similarity index 100%
rename from agents/codex-3590.md
rename to archives/agents/2025-11-30-codex-3590.md
diff --git a/agents/codex-3592.md b/archives/agents/2025-11-30-codex-3592.md
similarity index 100%
rename from agents/codex-3592.md
rename to archives/agents/2025-11-30-codex-3592.md
diff --git a/agents/codex-3593.md b/archives/agents/2025-11-30-codex-3593.md
similarity index 100%
rename from agents/codex-3593.md
rename to archives/agents/2025-11-30-codex-3593.md
diff --git a/agents/codex-3594.md b/archives/agents/2025-11-30-codex-3594.md
similarity index 100%
rename from agents/codex-3594.md
rename to archives/agents/2025-11-30-codex-3594.md
diff --git a/agents/codex-3595.md b/archives/agents/2025-11-30-codex-3595.md
similarity index 100%
rename from agents/codex-3595.md
rename to archives/agents/2025-11-30-codex-3595.md
diff --git a/agents/codex-3637.md b/archives/agents/2025-11-30-codex-3637.md
similarity index 100%
rename from agents/codex-3637.md
rename to archives/agents/2025-11-30-codex-3637.md
diff --git a/agents/codex-3638.md b/archives/agents/2025-11-30-codex-3638.md
similarity index 100%
rename from agents/codex-3638.md
rename to archives/agents/2025-11-30-codex-3638.md
diff --git a/agents/codex-3639.md b/archives/agents/2025-11-30-codex-3639.md
similarity index 100%
rename from agents/codex-3639.md
rename to archives/agents/2025-11-30-codex-3639.md
diff --git a/agents/codex-3640.md b/archives/agents/2025-11-30-codex-3640.md
similarity index 100%
rename from agents/codex-3640.md
rename to archives/agents/2025-11-30-codex-3640.md
diff --git a/agents/codex-3641.md b/archives/agents/2025-11-30-codex-3641.md
similarity index 100%
rename from agents/codex-3641.md
rename to archives/agents/2025-11-30-codex-3641.md
diff --git a/agents/codex-3642.md b/archives/agents/2025-11-30-codex-3642.md
similarity index 100%
rename from agents/codex-3642.md
rename to archives/agents/2025-11-30-codex-3642.md
diff --git a/agents/codex-3643.md b/archives/agents/2025-11-30-codex-3643.md
similarity index 100%
rename from agents/codex-3643.md
rename to archives/agents/2025-11-30-codex-3643.md
diff --git a/agents/codex-3645.md b/archives/agents/2025-11-30-codex-3645.md
similarity index 100%
rename from agents/codex-3645.md
rename to archives/agents/2025-11-30-codex-3645.md
diff --git a/agents/codex-3646.md b/archives/agents/2025-11-30-codex-3646.md
similarity index 100%
rename from agents/codex-3646.md
rename to archives/agents/2025-11-30-codex-3646.md
diff --git a/agents/codex-3647.md b/archives/agents/2025-11-30-codex-3647.md
similarity index 100%
rename from agents/codex-3647.md
rename to archives/agents/2025-11-30-codex-3647.md
diff --git a/agents/codex-3648.md b/archives/agents/2025-11-30-codex-3648.md
similarity index 100%
rename from agents/codex-3648.md
rename to archives/agents/2025-11-30-codex-3648.md
diff --git a/agents/codex-3649.md b/archives/agents/2025-11-30-codex-3649.md
similarity index 100%
rename from agents/codex-3649.md
rename to archives/agents/2025-11-30-codex-3649.md
diff --git a/agents/codex-3650.md b/archives/agents/2025-11-30-codex-3650.md
similarity index 100%
rename from agents/codex-3650.md
rename to archives/agents/2025-11-30-codex-3650.md
diff --git a/agents/codex-3651.md b/archives/agents/2025-11-30-codex-3651.md
similarity index 100%
rename from agents/codex-3651.md
rename to archives/agents/2025-11-30-codex-3651.md
diff --git a/agents/codex-3679.md b/archives/agents/2025-11-30-codex-3679.md
similarity index 100%
rename from agents/codex-3679.md
rename to archives/agents/2025-11-30-codex-3679.md
diff --git a/agents/codex-3680.md b/archives/agents/2025-11-30-codex-3680.md
similarity index 100%
rename from agents/codex-3680.md
rename to archives/agents/2025-11-30-codex-3680.md
diff --git a/agents/codex-3681.md b/archives/agents/2025-11-30-codex-3681.md
similarity index 100%
rename from agents/codex-3681.md
rename to archives/agents/2025-11-30-codex-3681.md
diff --git a/agents/codex-3682.md b/archives/agents/2025-11-30-codex-3682.md
similarity index 100%
rename from agents/codex-3682.md
rename to archives/agents/2025-11-30-codex-3682.md
diff --git a/agents/codex-3683.md b/archives/agents/2025-11-30-codex-3683.md
similarity index 100%
rename from agents/codex-3683.md
rename to archives/agents/2025-11-30-codex-3683.md
diff --git a/agents/codex-3684.md b/archives/agents/2025-11-30-codex-3684.md
similarity index 100%
rename from agents/codex-3684.md
rename to archives/agents/2025-11-30-codex-3684.md
diff --git a/agents/codex-3685.md b/archives/agents/2025-11-30-codex-3685.md
similarity index 100%
rename from agents/codex-3685.md
rename to archives/agents/2025-11-30-codex-3685.md
diff --git a/agents/codex-3687.md b/archives/agents/2025-11-30-codex-3687.md
similarity index 100%
rename from agents/codex-3687.md
rename to archives/agents/2025-11-30-codex-3687.md
diff --git a/agents/codex-3688.md b/archives/agents/2025-11-30-codex-3688.md
similarity index 100%
rename from agents/codex-3688.md
rename to archives/agents/2025-11-30-codex-3688.md
diff --git a/agents/codex-3689.md b/archives/agents/2025-11-30-codex-3689.md
similarity index 100%
rename from agents/codex-3689.md
rename to archives/agents/2025-11-30-codex-3689.md
diff --git a/agents/codex-3690.md b/archives/agents/2025-11-30-codex-3690.md
similarity index 100%
rename from agents/codex-3690.md
rename to archives/agents/2025-11-30-codex-3690.md
diff --git a/agents/codex-3691.md b/archives/agents/2025-11-30-codex-3691.md
similarity index 100%
rename from agents/codex-3691.md
rename to archives/agents/2025-11-30-codex-3691.md
diff --git a/agents/codex-3692.md b/archives/agents/2025-11-30-codex-3692.md
similarity index 100%
rename from agents/codex-3692.md
rename to archives/agents/2025-11-30-codex-3692.md
diff --git a/agents/codex-3693.md b/archives/agents/2025-11-30-codex-3693.md
similarity index 100%
rename from agents/codex-3693.md
rename to archives/agents/2025-11-30-codex-3693.md
diff --git a/agents/codex-3701.md b/archives/agents/2025-11-30-codex-3701.md
similarity index 100%
rename from agents/codex-3701.md
rename to archives/agents/2025-11-30-codex-3701.md
diff --git a/agents/codex-3737.md b/archives/agents/2025-11-30-codex-3737.md
similarity index 100%
rename from agents/codex-3737.md
rename to archives/agents/2025-11-30-codex-3737.md
diff --git a/agents/codex-3738.md b/archives/agents/2025-11-30-codex-3738.md
similarity index 100%
rename from agents/codex-3738.md
rename to archives/agents/2025-11-30-codex-3738.md
diff --git a/agents/codex-3739.md b/archives/agents/2025-11-30-codex-3739.md
similarity index 100%
rename from agents/codex-3739.md
rename to archives/agents/2025-11-30-codex-3739.md
diff --git a/agents/codex-3754.md b/archives/agents/2025-11-30-codex-3754.md
similarity index 100%
rename from agents/codex-3754.md
rename to archives/agents/2025-11-30-codex-3754.md
diff --git a/agents/codex-3756.md b/archives/agents/2025-11-30-codex-3756.md
similarity index 100%
rename from agents/codex-3756.md
rename to archives/agents/2025-11-30-codex-3756.md
diff --git a/agents/codex-3770.md b/archives/agents/2025-11-30-codex-3770.md
similarity index 100%
rename from agents/codex-3770.md
rename to archives/agents/2025-11-30-codex-3770.md
diff --git a/agents/codex-3771.md b/archives/agents/2025-11-30-codex-3771.md
similarity index 100%
rename from agents/codex-3771.md
rename to archives/agents/2025-11-30-codex-3771.md
diff --git a/agents/codex-3773.md b/archives/agents/2025-11-30-codex-3773.md
similarity index 100%
rename from agents/codex-3773.md
rename to archives/agents/2025-11-30-codex-3773.md
diff --git a/agents/codex-3784.md b/archives/agents/2025-11-30-codex-3784.md
similarity index 100%
rename from agents/codex-3784.md
rename to archives/agents/2025-11-30-codex-3784.md
diff --git a/agents/codex-3797.md b/archives/agents/2025-11-30-codex-3797.md
similarity index 100%
rename from agents/codex-3797.md
rename to archives/agents/2025-11-30-codex-3797.md
diff --git a/agents/codex-3798.md b/archives/agents/2025-11-30-codex-3798.md
similarity index 100%
rename from agents/codex-3798.md
rename to archives/agents/2025-11-30-codex-3798.md
diff --git a/agents/codex-3799.md b/archives/agents/2025-11-30-codex-3799.md
similarity index 100%
rename from agents/codex-3799.md
rename to archives/agents/2025-11-30-codex-3799.md
diff --git a/agents/codex-3800.md b/archives/agents/2025-11-30-codex-3800.md
similarity index 100%
rename from agents/codex-3800.md
rename to archives/agents/2025-11-30-codex-3800.md
diff --git a/agents/codex-3801.md b/archives/agents/2025-11-30-codex-3801.md
similarity index 100%
rename from agents/codex-3801.md
rename to archives/agents/2025-11-30-codex-3801.md
diff --git a/agents/codex-3817.md b/archives/agents/2025-11-30-codex-3817.md
similarity index 100%
rename from agents/codex-3817.md
rename to archives/agents/2025-11-30-codex-3817.md
diff --git a/agents/codex-3818.md b/archives/agents/2025-11-30-codex-3818.md
similarity index 100%
rename from agents/codex-3818.md
rename to archives/agents/2025-11-30-codex-3818.md
diff --git a/agents/codex-3819.md b/archives/agents/2025-11-30-codex-3819.md
similarity index 100%
rename from agents/codex-3819.md
rename to archives/agents/2025-11-30-codex-3819.md
diff --git a/agents/codex-3820.md b/archives/agents/2025-11-30-codex-3820.md
similarity index 100%
rename from agents/codex-3820.md
rename to archives/agents/2025-11-30-codex-3820.md
diff --git a/agents/codex-3861.md b/archives/agents/2025-11-30-codex-3861.md
similarity index 100%
rename from agents/codex-3861.md
rename to archives/agents/2025-11-30-codex-3861.md
diff --git a/agents/codex-3878.md b/archives/agents/2025-11-30-codex-3878.md
similarity index 100%
rename from agents/codex-3878.md
rename to archives/agents/2025-11-30-codex-3878.md
diff --git a/agents/codex-3879.md b/archives/agents/2025-11-30-codex-3879.md
similarity index 100%
rename from agents/codex-3879.md
rename to archives/agents/2025-11-30-codex-3879.md
diff --git a/agents/codex-721.md b/archives/agents/2025-11-30-codex-721.md
similarity index 100%
rename from agents/codex-721.md
rename to archives/agents/2025-11-30-codex-721.md
diff --git a/agents/codex-730.md b/archives/agents/2025-11-30-codex-730.md
similarity index 100%
rename from agents/codex-730.md
rename to archives/agents/2025-11-30-codex-730.md
diff --git a/agents/codex-732.md b/archives/agents/2025-11-30-codex-732.md
similarity index 100%
rename from agents/codex-732.md
rename to archives/agents/2025-11-30-codex-732.md
diff --git a/agents/codex-734.md b/archives/agents/2025-11-30-codex-734.md
similarity index 100%
rename from agents/codex-734.md
rename to archives/agents/2025-11-30-codex-734.md
diff --git a/archives/agents/README.md b/archives/agents/README.md
index c1eaafc615..8add20e463 100644
--- a/archives/agents/README.md
+++ b/archives/agents/README.md
@@ -1,10 +1,29 @@
# Archived agent instruction files
-Legacy agent guidance that referenced retired workflow names or superseded policy drafts lives here for historical context. Current automation and protection rules are documented in:
+Legacy agent guidance that referenced retired workflow names, superseded policy drafts, or **closed issues** lives here for historical context. Current automation and protection rules are documented in:
- `.github/workflows/README.md` for the active workflow topology and naming policy.
- `docs/ci/AGENTS_POLICY.md` for the protection contract covering the agents workflows.
+## Archive structure
+
+```
+archives/agents/
+├── ledgers/ # YAML task-tracking ledgers from .agents/
+│ └── issue-NNNN-ledger.yml
+├── YYYY-MM-DD-codex-NNNN.md # Archived instruction files
+└── README.md
+```
+
+## Archived on 2025-11-30 (bulk cleanup)
+
+**Ledgers** (18 files → `ledgers/`):
+- All issue ledgers from `.agents/` — issues #3011, #3203, #3209, #3213, #3218, #3219, #3279, #3284, #3309, #3318, #3321, #3333, #3352, #3363, #3428, #3442, #3490, #3498 — all CLOSED
+
+**Instruction files** (415 files):
+- All codex-*.md files referencing closed issues, numbered #721 through #3879
+- Excludes only `codex-3572.md` which remains active (issue OPEN)
+
## Archived on 2025-11-22
- `2025-11-22-codex-2682.md` – instructions focused on removing the old Agents 61/62 consumer workflows, which are no longer present.
- `2025-11-22-codex-2684.md` – bootstrap plan for drafting the original agents policy file that has since been published and maintained elsewhere.
diff --git a/.agents/issue-3011-ledger.yml b/archives/agents/ledgers/issue-3011-ledger.yml
similarity index 100%
rename from .agents/issue-3011-ledger.yml
rename to archives/agents/ledgers/issue-3011-ledger.yml
diff --git a/.agents/issue-3203-ledger.yml b/archives/agents/ledgers/issue-3203-ledger.yml
similarity index 100%
rename from .agents/issue-3203-ledger.yml
rename to archives/agents/ledgers/issue-3203-ledger.yml
diff --git a/.agents/issue-3209-ledger.yml b/archives/agents/ledgers/issue-3209-ledger.yml
similarity index 100%
rename from .agents/issue-3209-ledger.yml
rename to archives/agents/ledgers/issue-3209-ledger.yml
diff --git a/.agents/issue-3213-ledger.yml b/archives/agents/ledgers/issue-3213-ledger.yml
similarity index 100%
rename from .agents/issue-3213-ledger.yml
rename to archives/agents/ledgers/issue-3213-ledger.yml
diff --git a/.agents/issue-3218-ledger.yml b/archives/agents/ledgers/issue-3218-ledger.yml
similarity index 100%
rename from .agents/issue-3218-ledger.yml
rename to archives/agents/ledgers/issue-3218-ledger.yml
diff --git a/.agents/issue-3219-ledger.yml b/archives/agents/ledgers/issue-3219-ledger.yml
similarity index 100%
rename from .agents/issue-3219-ledger.yml
rename to archives/agents/ledgers/issue-3219-ledger.yml
diff --git a/.agents/issue-3279-ledger.yml b/archives/agents/ledgers/issue-3279-ledger.yml
similarity index 100%
rename from .agents/issue-3279-ledger.yml
rename to archives/agents/ledgers/issue-3279-ledger.yml
diff --git a/.agents/issue-3284-ledger.yml b/archives/agents/ledgers/issue-3284-ledger.yml
similarity index 100%
rename from .agents/issue-3284-ledger.yml
rename to archives/agents/ledgers/issue-3284-ledger.yml
diff --git a/.agents/issue-3309-ledger.yml b/archives/agents/ledgers/issue-3309-ledger.yml
similarity index 100%
rename from .agents/issue-3309-ledger.yml
rename to archives/agents/ledgers/issue-3309-ledger.yml
diff --git a/.agents/issue-3318-ledger.yml b/archives/agents/ledgers/issue-3318-ledger.yml
similarity index 100%
rename from .agents/issue-3318-ledger.yml
rename to archives/agents/ledgers/issue-3318-ledger.yml
diff --git a/.agents/issue-3321-ledger.yml b/archives/agents/ledgers/issue-3321-ledger.yml
similarity index 100%
rename from .agents/issue-3321-ledger.yml
rename to archives/agents/ledgers/issue-3321-ledger.yml
diff --git a/.agents/issue-3333-ledger.yml b/archives/agents/ledgers/issue-3333-ledger.yml
similarity index 100%
rename from .agents/issue-3333-ledger.yml
rename to archives/agents/ledgers/issue-3333-ledger.yml
diff --git a/.agents/issue-3352-ledger.yml b/archives/agents/ledgers/issue-3352-ledger.yml
similarity index 100%
rename from .agents/issue-3352-ledger.yml
rename to archives/agents/ledgers/issue-3352-ledger.yml
diff --git a/.agents/issue-3363-ledger.yml b/archives/agents/ledgers/issue-3363-ledger.yml
similarity index 100%
rename from .agents/issue-3363-ledger.yml
rename to archives/agents/ledgers/issue-3363-ledger.yml
diff --git a/.agents/issue-3428-ledger.yml b/archives/agents/ledgers/issue-3428-ledger.yml
similarity index 100%
rename from .agents/issue-3428-ledger.yml
rename to archives/agents/ledgers/issue-3428-ledger.yml
diff --git a/.agents/issue-3442-ledger.yml b/archives/agents/ledgers/issue-3442-ledger.yml
similarity index 100%
rename from .agents/issue-3442-ledger.yml
rename to archives/agents/ledgers/issue-3442-ledger.yml
diff --git a/.agents/issue-3490-ledger.yml b/archives/agents/ledgers/issue-3490-ledger.yml
similarity index 100%
rename from .agents/issue-3490-ledger.yml
rename to archives/agents/ledgers/issue-3490-ledger.yml
diff --git a/.agents/issue-3498-ledger.yml b/archives/agents/ledgers/issue-3498-ledger.yml
similarity index 100%
rename from .agents/issue-3498-ledger.yml
rename to archives/agents/ledgers/issue-3498-ledger.yml
diff --git a/analysis/health44-pr-run-review.md b/archives/analysis/2025-11-30-investigation-notes/health44-pr-run-review.md
similarity index 100%
rename from analysis/health44-pr-run-review.md
rename to archives/analysis/2025-11-30-investigation-notes/health44-pr-run-review.md
diff --git a/archives/analysis/README.md b/archives/analysis/README.md
new file mode 100644
index 0000000000..81f4dec958
--- /dev/null
+++ b/archives/analysis/README.md
@@ -0,0 +1,7 @@
+# Archived Analysis Files
+
+Investigation notes and temporary analysis files archived from the `analysis/` folder.
+
+## 2025-11-30-investigation-notes
+
+- `health44-pr-run-review.md` - Investigation of Health 44 workflow hang issue (PR #3822)
diff --git a/.github/actions/apply-autofix/action.yml b/archives/github-actions/2025-11-30-apply-autofix/action.yml
similarity index 100%
rename from .github/actions/apply-autofix/action.yml
rename to archives/github-actions/2025-11-30-apply-autofix/action.yml
diff --git a/.github/actions/autofix-commit-push/action.yml b/archives/github-actions/2025-11-30-autofix-commit-push/action.yml
similarity index 100%
rename from .github/actions/autofix-commit-push/action.yml
rename to archives/github-actions/2025-11-30-autofix-commit-push/action.yml
diff --git a/.github/actions/codex-bootstrap/action.yml b/archives/github-actions/2025-11-30-codex-bootstrap/action.yml
similarity index 99%
rename from .github/actions/codex-bootstrap/action.yml
rename to archives/github-actions/2025-11-30-codex-bootstrap/action.yml
index abc2cb6054..239de27a04 100644
--- a/.github/actions/codex-bootstrap/action.yml
+++ b/archives/github-actions/2025-11-30-codex-bootstrap/action.yml
@@ -25,8 +25,8 @@ inputs:
description: "Allow fallback to GITHUB_TOKEN when PAT is missing"
default: "true"
required: false
- codex_command:
- description: "Command to post to the PR to trigger Codex"
+ codex_command_legacy:
+ description: "Command to post to the PR to trigger Codex (duplicate key, kept for archive)"
default: "codex: start"
required: false
base_branch:
diff --git a/.github/actions/update-residual-history/action.yml b/archives/github-actions/2025-11-30-update-residual-history/action.yml
similarity index 100%
rename from .github/actions/update-residual-history/action.yml
rename to archives/github-actions/2025-11-30-update-residual-history/action.yml
diff --git a/archives/github-actions/README.md b/archives/github-actions/README.md
new file mode 100644
index 0000000000..d573e38406
--- /dev/null
+++ b/archives/github-actions/README.md
@@ -0,0 +1,43 @@
+# Archived GitHub Actions
+
+Composite actions that are no longer referenced by any active workflow.
+
+## Archive Structure
+
+```
+archives/github-actions/
+├── YYYY-MM-DD-action-name/ # Archived action directory
+│ └── action.yml
+└── README.md
+```
+
+## Archived on 2025-11-30
+
+### `apply-autofix`
+- **Purpose**: Wrapper that ran `autofix` action then committed/pushed changes
+- **Reason**: Not used by any workflow; `reusable-18-autofix.yml` handles the full autofix loop directly
+- **Dependencies**: Used `autofix` action internally
+
+### `autofix-commit-push`
+- **Purpose**: Similar to `apply-autofix` but with simpler commit message handling
+- **Reason**: Not used by any workflow; duplicate of `apply-autofix` functionality
+- **Dependencies**: Used `autofix` action internally
+
+### `update-residual-history`
+- **Purpose**: Appended autofix residual classification to `ci/autofix/history.json`
+- **Reason**: Not used by any workflow; residual tracking was removed from autofix flow
+- **Dependencies**: Required `scripts/update_residual_history.py` (also unused)
+
+### `codex-bootstrap`
+- **Purpose**: Original verbose Codex bootstrap action with complex fallback logic
+- **Reason**: Replaced by `codex-bootstrap-lite` which is simpler and actively used
+- **Superseded by**: `.github/actions/codex-bootstrap-lite/`
+
+## Active Actions (kept in `.github/actions/`)
+
+| Action | Used By | Purpose |
+|--------|---------|---------|
+| `autofix` | `build-pr-comment`, archived wrappers | Core formatting action (ruff, black, isort) |
+| `build-pr-comment` | `reusable-18-autofix.yml` | Builds PR comment from autofix results |
+| `codex-bootstrap-lite` | `reusable-16-agents.yml` | Minimal Codex PR bootstrap |
+| `signature-verify` | `health-43-ci-signature-guard.yml` | Verify CI signature files |
diff --git a/.github/labeler.yml b/archives/github-config/2025-11-30-orphaned/labeler.yml
similarity index 100%
rename from .github/labeler.yml
rename to archives/github-config/2025-11-30-orphaned/labeler.yml
diff --git a/archives/github-config/README.md b/archives/github-config/README.md
new file mode 100644
index 0000000000..8c61bd9b73
--- /dev/null
+++ b/archives/github-config/README.md
@@ -0,0 +1,9 @@
+# Archived GitHub Config Files
+
+Configuration files archived from `.github/` folder.
+
+## 2025-11-30-orphaned
+
+- `labeler.yml` - Path-based label configuration that was orphaned (no workflow used it)
+ - The README.md referenced a `pr-path-labeler.yml` workflow that doesn't exist
+ - To restore: create a workflow using `actions/labeler` that references this config
diff --git a/reports/code_review.md b/archives/reports/2025-11-29-code-review/code_review.md
similarity index 100%
rename from reports/code_review.md
rename to archives/reports/2025-11-29-code-review/code_review.md
diff --git a/scripts/demo_export_fix.py b/archives/scripts/2025-11-30-one-off/demo_export_fix.py
similarity index 100%
rename from scripts/demo_export_fix.py
rename to archives/scripts/2025-11-30-one-off/demo_export_fix.py
diff --git a/archives/scripts/README.md b/archives/scripts/README.md
index 309d2868fa..2ecf7ee7b9 100644
--- a/archives/scripts/README.md
+++ b/archives/scripts/README.md
@@ -13,5 +13,10 @@ directories, so they are treated as rarely used or historical utilities.
unified `trend` CLI. Last-known use: pre-unified CLI era; retained only as
a reference.
+## 2025-11-30-one-off
+
+- `demo_export_fix.py` — one-off demo export fix script; no references found
+ in workflows, docs, or other scripts.
+
If you need to revive any of these scripts, please re-home them under a
supported workflow and add the appropriate documentation and ownership notes.
diff --git a/assets/screenshots/README.md b/assets/screenshots/README.md
index 903364f9ab..4a791abbd2 100644
--- a/assets/screenshots/README.md
+++ b/assets/screenshots/README.md
@@ -1,42 +1,43 @@
-# Screenshot Placeholders
+# Screenshots
-This directory contains screenshots for the quickstart guide and documentation.
+This directory holds screenshots for the quickstart guide and documentation.
-## Screenshots Needed:
+## Screenshots Needed
-1. **upload-interface.png** - Shows the file upload interface and column mapping
-2. **template-section.png** - Shows the template download section
-3. **preset-selection.png** - Shows the three preset options (Conservative, Balanced, Aggressive)
-4. **results-dashboard.png** - Shows the analysis results with charts and metrics
+| Screenshot | Description |
+|------------|-------------|
+| `upload-interface.png` | File upload interface with drag-and-drop and column mapping |
+| `template-section.png` | Template download section with sample data preview |
+| `preset-selection.png` | Configuration page showing Conservative/Balanced/Aggressive presets |
+| `results-dashboard.png` | Analysis results with charts, metrics, and download buttons |
-## Creating Screenshots:
-
-To create these screenshots:
+## Creating Screenshots
1. Start the Streamlit app: `./scripts/run_streamlit.sh`
2. Navigate through the workflow
3. Take screenshots at key steps
4. Save as PNG files in this directory
-## Workflow to Screenshot:
+## Screenshot Details
-### Upload Interface (upload-interface.png):
-- Main page with file uploader
-- Drag-and-drop interface visible
-- Upload button and file selection dialog
+### upload-interface.png
+- Streamlit file uploader interface
+- CSV file selection dialog
+- Column mapping showing Date and Fund columns
-### Template Section (template-section.png):
+### template-section.png
- Template download section expanded
- Sample data preview visible
- Download button for CSV template
-### Preset Selection (preset-selection.png):
-- Configuration page with preset dropdown
-- Conservative, Balanced, Aggressive options visible
-- Description text for each preset
-
-### Results Dashboard (results-dashboard.png):
-- Analysis results page
-- Portfolio performance chart
-- Key metrics displayed (Sharpe ratio, returns, etc.)
-- Download buttons visible
\ No newline at end of file
+### preset-selection.png
+- Configuration page dropdown with preset options
+- Conservative (8% risk target, 60 month lookback)
+- Balanced (10% risk target, 36 month lookback)
+- Aggressive (15% risk target, 24 month lookback)
+
+### results-dashboard.png
+- Portfolio performance line chart
+- Key metrics: Sharpe Ratio, Annual Return, Max Drawdown
+- Selected funds list with individual performance
+- Download buttons for CSV, Excel, JSON exports
\ No newline at end of file
diff --git a/assets/screenshots/preset-selection-placeholder.md b/assets/screenshots/preset-selection-placeholder.md
deleted file mode 100644
index 99f60cdd6f..0000000000
--- a/assets/screenshots/preset-selection-placeholder.md
+++ /dev/null
@@ -1,8 +0,0 @@
-Preset Selection Screenshot Placeholder
-
-This would show:
-- Configuration page dropdown with preset options
-- Conservative (8% risk target, 60 month lookback)
-- Balanced (10% risk target, 36 month lookback)
-- Aggressive (15% risk target, 24 month lookback)
-- Description text for each preset explaining strategy
\ No newline at end of file
diff --git a/assets/screenshots/results-dashboard-placeholder.md b/assets/screenshots/results-dashboard-placeholder.md
deleted file mode 100644
index 9a3d3b9204..0000000000
--- a/assets/screenshots/results-dashboard-placeholder.md
+++ /dev/null
@@ -1,8 +0,0 @@
-Results Dashboard Screenshot Placeholder
-
-This would show:
-- Portfolio performance line chart over time
-- Key metrics box: Sharpe Ratio, Annual Return, Max Drawdown
-- Selected funds list with individual performance
-- Benchmark comparison chart
-- Download buttons for CSV, Excel, JSON exports
\ No newline at end of file
diff --git a/assets/screenshots/upload-interface.png b/assets/screenshots/upload-interface.png
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/assets/screenshots/upload-process-placeholder.md b/assets/screenshots/upload-process-placeholder.md
deleted file mode 100644
index b2d1877fa5..0000000000
--- a/assets/screenshots/upload-process-placeholder.md
+++ /dev/null
@@ -1,7 +0,0 @@
-Upload Process Screenshot Placeholder
-
-This would show:
-- Streamlit file uploader interface
-- CSV file selection dialog
-- Column mapping interface showing Date, Fund columns
-- Upload confirmation message
\ No newline at end of file
diff --git a/config/long_backtest.yml b/config/long_backtest.yml
index 2104eaffaf..bdf8e5ddb2 100644
--- a/config/long_backtest.yml
+++ b/config/long_backtest.yml
@@ -2,7 +2,7 @@
version: "1"
data:
- csv_path: hedge_fund_returns_with_indexes.csv
+ csv_path: data/hedge_fund_returns_with_indexes.csv
date_column: Date
frequency: ME # monthly data (month-end)
risk_free_column: "Risk-Free Rate"
diff --git a/config/universe/core.yml b/config/universe/core.yml
index 74533096a0..5470f8e661 100644
--- a/config/universe/core.yml
+++ b/config/universe/core.yml
@@ -2,8 +2,8 @@ version: 1
key: core
name: Core trend cohort
description: Concentrated set of diversified trend managers with long live track records.
-data_csv: Trend Universe Data.csv
-membership_csv: Trend Universe Membership.csv
+data_csv: data/Trend Universe Data.csv
+membership_csv: data/Trend Universe Membership.csv
date_column: Date
members:
- AHL Dimension
diff --git a/config/universe/core_plus_benchmarks.yml b/config/universe/core_plus_benchmarks.yml
index 50cf39f168..f776af291d 100644
--- a/config/universe/core_plus_benchmarks.yml
+++ b/config/universe/core_plus_benchmarks.yml
@@ -2,8 +2,8 @@ version: 1
key: core_plus_benchmarks
name: Core + benchmark overlays
description: Core trend managers plus representative benchmark series for comparison.
-data_csv: Trend Universe Data.csv
-membership_csv: Trend Universe Membership.csv
+data_csv: data/Trend Universe Data.csv
+membership_csv: data/Trend Universe Membership.csv
date_column: Date
members:
- AHL Dimension
diff --git a/config/universe/managed_futures_min.yml b/config/universe/managed_futures_min.yml
index 664842bf47..947dbe47e1 100644
--- a/config/universe/managed_futures_min.yml
+++ b/config/universe/managed_futures_min.yml
@@ -2,8 +2,8 @@ version: 1
key: managed_futures_min
name: Managed futures mini
description: Small comparison set for quick smoke tests and doc examples.
-data_csv: Trend Universe Data.csv
-membership_csv: Trend Universe Membership.csv
+data_csv: data/Trend Universe Data.csv
+membership_csv: data/Trend Universe Membership.csv
date_column: Date
members:
- Crabel Advanced Trend Program
diff --git a/data/README.md b/data/README.md
index c0cd90d484..3089a3bcfe 100644
--- a/data/README.md
+++ b/data/README.md
@@ -1,12 +1,12 @@
# Reference datasets
-Only active reference inputs remain in the root or `data/` directories. Demo outputs and ad-hoc analyses are archived under `archives/data_snapshots/`.
+All active reference datasets are now consolidated in this `data/` directory. Demo outputs and ad-hoc analyses are archived under `archives/data_snapshots/`.
| Dataset | Location | Size | Schema | Owner | Purpose |
| --- | --- | --- | --- | --- | --- |
-| Trend Universe total returns | `Trend Universe Data.csv` | 136 KB | [Schema](#trend-universe-data) | Demo data maintainers (Research Ops) | Primary monthly total return matrix for Trend universe configs and tests. |
-| Trend Universe membership ledger | `Trend Universe Membership.csv` | 4 KB | [Schema](#trend-universe-membership) | Demo data maintainers (Research Ops) | Effective-date windows for each Trend universe column used by loaders and configs. See `docs/data/Trend_Universe_Data.md` for stewardship notes. |
-| Hedge fund returns with benchmarks | `hedge_fund_returns_with_indexes.csv` | 140 KB | [Schema](#hedge-fund-returns-with-indexes) | Demo data maintainers (Research Ops) | Input for long backtest and rolling-hold configs plus legacy demo notebooks. |
+| Trend Universe total returns | `data/Trend Universe Data.csv` | 136 KB | [Schema](#trend-universe-data) | Demo data maintainers (Research Ops) | Primary monthly total return matrix for Trend universe configs and tests. |
+| Trend Universe membership ledger | `data/Trend Universe Membership.csv` | 4 KB | [Schema](#trend-universe-membership) | Demo data maintainers (Research Ops) | Effective-date windows for each Trend universe column used by loaders and configs. See `docs/data/Trend_Universe_Data.md` for stewardship notes. |
+| Hedge fund returns with benchmarks | `data/hedge_fund_returns_with_indexes.csv` | 140 KB | [Schema](#hedge-fund-returns-with-indexes) | Demo data maintainers (Research Ops) | Input for long backtest and rolling-hold configs plus legacy demo notebooks. |
| Sample manager returns | `data/raw/managers/sample_manager.csv` | 4 KB | [Schema](#sample-manager-returns) | Demo data maintainers (Research Ops) | Minimal fixture used by CLI preset tests. |
| Sample benchmark index | `data/raw/indices/sample_index.csv` | 4 KB | [Schema](#sample-benchmark-index) | Demo data maintainers (Research Ops) | Lightweight index series kept for parity with sample manager inputs. |
diff --git a/Trend Universe Data.csv b/data/Trend Universe Data.csv
similarity index 100%
rename from Trend Universe Data.csv
rename to data/Trend Universe Data.csv
diff --git a/Trend Universe Membership.csv b/data/Trend Universe Membership.csv
similarity index 100%
rename from Trend Universe Membership.csv
rename to data/Trend Universe Membership.csv
diff --git a/hedge_fund_returns_with_indexes.csv b/data/hedge_fund_returns_with_indexes.csv
similarity index 100%
rename from hedge_fund_returns_with_indexes.csv
rename to data/hedge_fund_returns_with_indexes.csv
diff --git a/docs/DEPENDENCY_ENFORCEMENT.md b/docs/DEPENDENCY_ENFORCEMENT.md
index 79804becfb..c85f7e331d 100644
--- a/docs/DEPENDENCY_ENFORCEMENT.md
+++ b/docs/DEPENDENCY_ENFORCEMENT.md
@@ -150,7 +150,7 @@ The test will fail if `some_tool` is not in either `REQUIRED_CLI_TOOLS` or `OPTI
## Required Dependencies
-### Python Packages (requirements.txt)
+### Python Packages (pyproject.toml)
**Core**:
- pandas
@@ -194,9 +194,13 @@ The test will fail if `some_tool` is not in either `REQUIRED_CLI_TOOLS` or `OPTI
### For Python Packages
-1. Add the package to `requirements.txt`:
- ```
- new-package>=1.0.0
+1. Add the package to `pyproject.toml` under `[project.optional-dependencies].dev`:
+ ```toml
+ [project.optional-dependencies]
+ dev = [
+ # ... existing deps ...
+ "new-package>=1.0.0",
+ ]
```
2. If it's optional (tests can skip gracefully), add to `test_test_dependencies.py`:
@@ -285,9 +289,9 @@ uv pip compile pyproject.toml -o requirements.lock
The enforcement tests run automatically with every test suite execution. If they fail, it means:
-1. A new Python package is imported but not in `requirements.txt`
+1. A new Python package is imported but not in `pyproject.toml`
2. A new CLI tool is used but not documented
-3. Requirements.txt has invalid package specifications
+3. `pyproject.toml` has invalid package specifications
Fix by adding the missing dependency to the appropriate configuration file.
@@ -295,7 +299,7 @@ Fix by adding the missing dependency to the appropriate configuration file.
The CI workflow validates dependencies in this order:
-1. **Install Python dependencies** from requirements.txt
+1. **Install Python dependencies** from `requirements.lock`
2. **Install Node.js and npm** via GitHub Actions
3. **Install uv** via official installer
4. **Validate test dependencies** via test suite
diff --git a/docs/DEPENDENCY_MANAGEMENT.md b/docs/DEPENDENCY_MANAGEMENT.md
index 143f89f2fe..08d721863a 100644
--- a/docs/DEPENDENCY_MANAGEMENT.md
+++ b/docs/DEPENDENCY_MANAGEMENT.md
@@ -1,379 +1,50 @@
# Test Dependency Management
-This document describes the comprehensive dependency validation system for the Trend Analysis Project test suite.
+> **Canonical documentation**: See [DEPENDENCY_ENFORCEMENT.md](DEPENDENCY_ENFORCEMENT.md) for complete implementation details.
-> **Update (2025-10):** Dependency declarations are now maintained in
-> `pyproject.toml` (including the dev extra) with pins captured in
-> `requirements.lock`. Historical references to `requirements.txt` in this
-> document refer to the legacy workflow.
+## Quick Reference
-# Test Dependency Management
-
-## System Status: ENFORCED ✅
+### System Status: ENFORCED ✅
-As of 2025-10-28, this project implements **automatic dependency enforcement** that prevents tests from running with missing dependencies.
+The project implements **automatic dependency enforcement** that prevents tests from running with missing dependencies.
-**Current Status**: 2,102 tests pass, 0 skipped
+**Current Status**: All tests pass with 0 skipped
### Key Features
-1. **Automatic Installation**: CI automatically installs all required dependencies (Python packages, Node.js, npm, uv)
+1. **Automatic Installation**: CI installs all required dependencies (Python packages, Node.js, npm, uv)
2. **Validation Tests**: Test suite validates all dependencies are present before running
3. **Enforcement Tests**: Build fails if new dependencies are used without being declared
4. **Zero Skipped Tests**: All tests must run; skipping due to missing dependencies is not allowed
-See [DEPENDENCY_ENFORCEMENT.md](DEPENDENCY_ENFORCEMENT.md) for complete implementation details.
-
----
-
-## Overview
-
-The project uses a multi-layered approach to ensure all test dependencies are available:
-
-1. **Automated CI validation** - Dependencies checked automatically in CI workflows
-2. **Test suite validation** - Comprehensive test suite validates all dependencies
-3. **Manual check script** - Quick command-line tool for local validation
-4. **Configuration files** - Explicit dependency declarations with documentation
-
-## Quick Start
-
-### Check Dependencies Locally
+### Quick Commands
```bash
-# Quick check with color-coded output
+# Check dependencies locally
./scripts/check_test_dependencies.sh
# Run dependency validation tests
pytest tests/test_test_dependencies.py -v
-```
-
-### Install Missing Dependencies
-
-```bash
-# Install Python dependencies from the lock file
-uv pip sync requirements.lock
-
-# Then install the package (no dependency resolution needed)
-pip install --no-deps -e '.[dev]'
-
-# External tools (must be installed separately)
-# - Node.js v20+: https://nodejs.org/
-# - uv: https://github.com/astral-sh/uv
-```
-
-## Dependency Categories
-
-### Required Python Packages
-
-These packages are **required** for the test suite to run. Tests will fail if these are missing:
-
-- **Python 3.11+** (enforced minimum version)
-- **pytest** >= 8.0 - Test framework
-- **coverage** >= 7.0 - Coverage measurement
-- **hypothesis** >= 6.0 - Property-based testing
-- **pandas** - Data manipulation
-- **numpy** - Numerical computing
-- **pydantic** - Data validation
-- **PyYAML** - Configuration parsing
-- **requests** - HTTP library
-- **jsonschema** - JSON validation
-- **streamlit** - Web application framework
-- **fastapi** - API framework
-- **httpx** >= 0.25 - Async HTTP client
-
-### Optional Python Packages
-
-These packages are **optional**. Tests requiring them will skip gracefully if missing:
-
-- **black** - Code formatting
-- **ruff** - Linting
-- **mypy** - Type checking
-- **pre-commit** - Git hooks
-
-### External CLI Tools
-
-These tools are **not pip-installable** and must be installed separately. Tests requiring them will skip with clear messages:
-
-- **Node.js v20+** and **npm** - Required for JavaScript workflow tests
- - 19 tests skip without Node.js
- - Install from: https://nodejs.org/
-
-- **uv** - Optional, for lockfile consistency tests
- - 1 test skips without uv
- - Install from: https://github.com/astral-sh/uv
-
-## Configuration Files
-
-### requirements.txt
-
-Primary dependency file with explicit test dependencies section:
-
-```txt
-# Testing dependencies
-pytest>=8.0
-pytest-cov
-pytest-rerunfailures
-hypothesis
-coverage
-jsonschema
-
-# Note: Node.js (v20+) and npm are required for JavaScript workflow tests
-# Install from: https://nodejs.org/
-# Optional: uv (for lockfile tests) - https://github.com/astral-sh/uv
-```
-
-### pyproject.toml
-
-Build configuration with optional dev dependencies:
-
-```toml
-[project.optional-dependencies]
-dev = [
- "pytest==9.0.1",
- "pytest-cov==7.0.0",
- "pytest-rerunfailures>=13.0",
- "pytest-xdist", # parallel test execution
- "coverage>=7.0",
- "hypothesis>=6.0",
- # ... other dev tools
-]
-
-# Note: External dependencies not managed by pip:
-# - Node.js (v20+) and npm - Required for JavaScript workflow tests
-# - uv - Optional, for lockfile consistency tests
-```
-
-## Validation Tools
-
-### 1. CI Workflow Validation
-
-**File**: `.github/workflows/reusable-10-ci-python.yml`
-
-Automatically runs after installing dependencies in all CI jobs:
-
-```yaml
-- name: Validate test dependencies
- run: |
- # Runs check_test_dependencies.sh if available
- # Falls back to basic validation
- # Outputs to GitHub step summary
-```
-
-**Behavior**:
-- ✅ Runs automatically on every CI build
-- ✅ Reports results in GitHub Actions step summary
-- ✅ Documents available and missing dependencies
-- ✅ Does not fail build on missing optional dependencies
-
-### 2. Test Suite Validation
-
-**File**: `tests/test_test_dependencies.py`
-
-Comprehensive pytest test suite with 13+ validation tests:
-
-```python
-class TestDependencies:
- def test_python_version()
- def test_required_packages_importable()
- def test_optional_packages_documented()
- def test_node_available()
- def test_npm_available_if_node_present()
- def test_uv_availability_documented()
- def test_requirements_file_exists()
- def test_pytest_plugins_available()
- def test_coverage_tool_available()
- def test_github_scripts_dependencies()
- def test_streamlit_dependencies()
- # ... additional tests
-```
-
-**Run with**:
-```bash
-pytest tests/test_test_dependencies.py -v
-```
-
-**Behavior**:
-- ✅ Fails if required dependencies missing
-- ✅ Skips gracefully if optional dependencies missing
-- ✅ Provides installation instructions in skip messages
-- ✅ Validates both Python packages and CLI tools
-
-### 3. Manual Check Script
-**File**: `scripts/check_test_dependencies.sh`
-
-Quick command-line tool for local validation:
-
-```bash
-./scripts/check_test_dependencies.sh
-```
-
-**Features**:
-- ✅ Color-coded output (green ✓, red ✗, yellow ○)
-- ✅ Checks Python version
-- ✅ Validates all required packages
-- ✅ Checks optional packages
-- ✅ Validates CLI tools (node, npm, uv, coverage)
-- ✅ Provides installation instructions
-- ✅ Exit code 0 if all required present, 1 otherwise
-
-**Example output**:
-```
-=== Test Dependencies Check ===
-
-Checking Python version...
-✓ Python 3.11.14 (>=3.11 required)
-
-Checking required Python packages...
-✓ pytest
-✓ coverage
-✓ hypothesis
-...
-
-Checking optional Python packages...
-✓ black
-○ pre-commit (not found)
-
-Checking Node.js...
-○ Node.js (not found - JavaScript tests will be skipped)
- Install from: https://nodejs.org/
-
-=== Summary ===
-All required dependencies are available!
-```
-
-## Test Behavior with Missing Dependencies
-
-### Required Dependencies Missing
-
-If a **required** dependency is missing:
-- ❌ Test suite fails with clear error
-- ❌ CI build fails
-- ❌ Manual script exits with code 1
-
-### Optional Dependencies Missing
-
-If an **optional** dependency is missing:
-- ✅ Test suite runs
-- ⏭️ Tests requiring the dependency skip with message
-- ✅ CI build succeeds
-- ℹ️ Manual script documents availability
-
-**Example skip messages**:
-
-```
-SKIPPED [19] Node.js not found in PATH. JavaScript workflow tests will be skipped.
-Install Node.js: https://nodejs.org/
-
-SKIPPED [1] uv not found in PATH. Lockfile consistency tests will be skipped.
-Install uv: https://github.com/astral-sh/uv
-
-SKIPPED [1] Optional packages not available: pre-commit
-Install with: pip install pre-commit
-```
-
-## Updating Dependencies
-
-### Adding New Required Dependencies
-
-1. Add to `requirements.txt` under the testing section
-2. Add to `pyproject.toml` under `[project.optional-dependencies] dev`
-3. Add import check in `test_test_dependencies.py::test_required_packages_importable()`
-4. Update `scripts/check_test_dependencies.sh` required packages list
-5. Update this document
-
-### Adding New Optional Dependencies
-
-1. Add to `pyproject.toml` under `[project.optional-dependencies] dev`
-2. Add skip decorator to tests requiring it: `@pytest.mark.skipif(...)`
-3. Add to `test_test_dependencies.py::test_optional_packages_documented()`
-4. Add to `scripts/check_test_dependencies.sh` optional packages list
-5. Document in this file
-
-### Adding External CLI Tools
-
-1. Add skip logic to tests requiring it
-2. Document in configuration file comments
-3. Add check to `test_test_dependencies.py`
-4. Add to `scripts/check_test_dependencies.sh`
-5. Update this document with installation instructions
-
-## Troubleshooting
-
-### "Module not found" errors
-
-```bash
-# Install all dependencies
-pip install -r requirements.txt
-
-# Or with dev dependencies
-pip install -e '.[dev]'
+# Sync missing dependencies
+python scripts/sync_test_dependencies.py --fix
+uv pip compile pyproject.toml -o requirements.lock
```
-### JavaScript tests skipping
-
-```bash
-# Install Node.js v20+ from https://nodejs.org/
-# Verify installation
-node --version
-npm --version
-
-# Re-run tests
-pytest -v
-```
-
-### Lockfile tests skipping
-
-```bash
-# Install uv
-curl -LsSf https://astral.sh/uv/install.sh | sh
-
-# Verify installation
-uv --version
-
-# Re-run tests
-pytest tests/ -k lockfile -v
-```
-
-### CI validation not running
-
-1. Check that `reusable-10-ci-python.yml` has the "Validate test dependencies" step
-2. Check that `scripts/check_test_dependencies.sh` is executable (`chmod +x`)
-3. Check CI logs for step summary output
-
-## Integration with CI/CD
-
-The dependency validation is integrated into the CI pipeline as follows:
-
-1. **Gate Workflow** (`.github/workflows/pr-00-gate.yml`)
- - Calls `reusable-10-ci-python.yml` for all Python testing
-
-2. **Reusable Python CI** (`.github/workflows/reusable-10-ci-python.yml`)
- - Installs dependencies
- - **→ Validates test dependencies** (new step)
- - Runs linting, type checking
- - Runs test suite (which includes dependency tests)
- - Checks coverage
-
-3. **Test Suite** (`tests/test_test_dependencies.py`)
- - Runs as part of normal test execution
- - Validates environment programmatically
- - Provides detailed skip messages
-
-## Design Philosophy
+### Configuration Files
-The dependency management system follows these principles:
+| File | Purpose |
+|------|---------|
+| `pyproject.toml` | Authoritative dependency declarations |
+| `requirements.lock` | Generated pinned dependencies |
+| `tests/test_test_dependencies.py` | Validation tests |
+| `tests/test_dependency_enforcement.py` | Enforcement tests |
+| `scripts/sync_test_dependencies.py` | Auto-sync tool |
-1. **Fail Fast**: Required dependencies fail the build immediately
-2. **Graceful Degradation**: Optional dependencies allow tests to skip
-3. **Clear Communication**: Skip messages include installation instructions
-4. **Multiple Validation Layers**: CI, test suite, and manual checks
-5. **Developer-Friendly**: Color-coded output and helpful messages
-6. **CI-Friendly**: Outputs to step summaries for visibility
-7. **Documentation**: Inline comments in configuration files
+### Adding Dependencies
-## Related Documentation
+1. Add package to `pyproject.toml` under `[project.optional-dependencies].dev`
+2. Run `uv pip compile pyproject.toml -o requirements.lock`
+3. Commit both files
-- [Testing Guide](../archives/reports/2025-11-22_TESTING_SUMMARY.md) - Overview of test infrastructure (archived ledger)
-- [Coverage Guide](coverage-summary.md) - Coverage tracking and thresholds
-- [GitHub Copilot Instructions](../.github/copilot-instructions.md) - Development workflow
+For external CLI tools (Node.js, uv, etc.), see [DEPENDENCY_ENFORCEMENT.md](DEPENDENCY_ENFORCEMENT.md).
diff --git a/docs/INDEX.md b/docs/INDEX.md
index 4d994a2822..a29303b75c 100644
--- a/docs/INDEX.md
+++ b/docs/INDEX.md
@@ -2,6 +2,230 @@
Use this index to find the current contributor guides and to understand which overlapping docs remain for historical context.
+## Repository Structure
+
+### Core Directories
+| Directory | Purpose | Key Files |
+| --- | --- | --- |
+| `src/` | Main source code | `trend_analysis/` package, `trend_portfolio_app/` |
+| `tests/` | Unit and integration tests | See tests/ organization below |
+| `config/` | Configuration files | `defaults.yml`, `demo.yml`, `presets/`, `universe/` |
+| `scripts/` | Utility and CI scripts | `setup_env.sh`, `run_tests.sh` |
+| `docs/` | Documentation | Guides, references, CI docs |
+| `analysis/` | Analysis helpers | `cv.py`, `results.py`, `tearsheet.py` |
+| `assets/` | Static assets | `screenshots/` for documentation |
+| `demo/` | Demo data and outputs | Generated demo datasets |
+| `examples/` | Usage examples | Demo scripts, legacy streamlit app |
+| `notebooks/` | Jupyter notebooks | `Vol_Adj_Trend_Analysis1.5.TrEx.ipynb` (maintained) |
+
+### Automation & CI
+| Directory | Purpose | Key Files |
+| --- | --- | --- |
+| `.github/workflows/` | GitHub Actions workflows | 36 workflow files (see `WORKFLOW_GUIDE.md`) |
+| `.github/actions/` | Custom composite actions | `autofix/`, `build-pr-comment/`, `codex-bootstrap-lite/`, `signature-verify/` |
+| `.github/` | GitHub config | `CODEOWNERS`, `agents.json`, `copilot-instructions.md` |
+
+### Archives
+| Directory | Purpose | Contents |
+| --- | --- | --- |
+| `archives/agents/` | Archived Codex task files | 433 files from closed issues (archived 2025-11-30) |
+| `archives/github-actions/` | Retired GitHub Actions | 4 unused actions (archived 2025-11-30) |
+| `archives/github-config/` | Orphaned GitHub config | `labeler.yml` - no workflow used it (archived 2025-11-30) |
+| `archives/analysis/` | Investigation notes | `health44-pr-run-review.md` (archived 2025-11-30) |
+| `archives/notebooks/` | Superseded notebooks | Old notebook versions in `2025/` |
+| `archives/docs/` | Archived documentation | Historical guides and reports |
+| `archives/reports/` | Archived reports | Testing summaries, release notes |
+| `docs/archive/` | Documentation archive | See below |
+
+#### Documentation Archive Details (2025-11-30)
+
+**`docs/archive/` structure:**
+- `plans/` - 58 archived files (implementation plans, summaries, vendored docs)
+- `ops-issues/` - 5 operations scope docs and smoke test artifacts
+- `audits/` - 1 dated audit report
+- `ARCHIVE_WORKFLOWS.md` - Retired workflow documentation
+
+**Archived content categories:**
+- Issue-specific plans (`issue-XXXX-*.md`) for closed issues: #2523, #2528, #2562, #2566, #2649, #2683, #2688, #2963, #3260, #3261, #3646, #1683, #2812, #2883
+- Implementation plans (`*_plan.md`) for completed features: workflow renaming, gate protection, autofix, reusable agents, etc.
+- Implementation summaries (`*_SUMMARY.md`, `*_COMPLETE.md`) documenting completed work
+- Progress logs (`*_progress.md`, `*-log.md`) for finished tasks
+- Dated audit reports (`repository_housekeeping_audit_2025-*.md`)
+- Operations issue scopes from `docs/ops/` for closed issues: #2494, #2615, #2820
+- Redundant validation docs (consolidated into `fast-validation-ecosystem.md`)
+- Redundant dependency docs (consolidated into `DEPENDENCY_ENFORCEMENT.md`)
+- Vendored actionlint documentation (replaced with project-specific guides)
+
+**Documentation consolidation (2025-11-30):**
+- Archived 5 redundant validation docs → kept `fast-validation-ecosystem.md`
+- Archived 2 redundant dependency docs → kept `DEPENDENCY_ENFORCEMENT.md`
+- Replaced 6 vendored actionlint docs with project-specific content
+- Renamed walkforward docs for clarity:
+ - `walk_forward.md` → `walkforward-parameter-grid.md`
+ - `Walkforward.md` → `walkforward-analysis.md`
+- Updated all `requirements.txt` references to `pyproject.toml`/`requirements.lock`
+
+#### Archive Details (2025-11-30 Cleanup)
+
+**agents/ folder cleanup:**
+- Archived 415 `codex-*.md` task files for closed issues
+- Archived 18 `ledger-*.md` files for closed issues
+- Kept active task files for open issues
+
+**`.github/actions/` cleanup:**
+- Archived `apply-autofix/` - superseded by `autofix/`
+- Archived `autofix-commit-push/` - superseded by `autofix/`
+- Archived `codex-bootstrap/` - superseded by `codex-bootstrap-lite/`
+- Archived `update-residual-history/` - no longer referenced
+- Kept: `autofix/`, `build-pr-comment/`, `codex-bootstrap-lite/`, `signature-verify/`
+
+**`.github/` root cleanup:**
+- Fixed CODEOWNERS stale workflow references
+- Archived orphaned `labeler.yml` (no `pr-path-labeler.yml` workflow exists)
+
+**`analysis/` folder cleanup:**
+- Archived `health44-pr-run-review.md` investigation notes
+- Kept active code: `cv.py`, `results.py`, `tearsheet.py`, `__init__.py`
+
+**`assets/` folder cleanup:**
+- Removed empty placeholder PNG files (0 bytes)
+- Consolidated placeholder descriptions into `screenshots/README.md`
+
+### Folders Reviewed (No Changes Needed)
+
+**`config/` folder (reviewed 2025-11-30):**
+- Well-organized with clear structure
+- `defaults.yml` - Master configuration schema
+- `demo.yml` - Demo/test configuration
+- `presets/` - User presets: `aggressive.yml`, `balanced.yml`, `conservative.yml`, `cash_constrained.yml`
+- `universe/` - Universe definitions: `core.yml`, `core_plus_benchmarks.yml`, `managed_futures_min.yml`
+- Specialized configs for backtesting, walk-forward analysis, etc.
+- Fully documented in `docs/ConfigMap.md`
+
+**`examples/` folder (reviewed 2025-11-30):**
+- Contains usage examples with clear README documentation
+- Active scripts: `demo_robust_weighting.py`, `demo_turnover_cap.py`, `debug_fund_selection.py`, `integration_example.py`, `portfolio_analysis_report.py`
+- `legacy_streamlit_app/` - Historical prototype kept for reference (documented in README_APP.md)
+
+**`notebooks/` folder (reviewed 2025-11-30):**
+- Single maintained notebook: `Vol_Adj_Trend_Analysis1.5.TrEx.ipynb`
+- Old notebooks already archived to `archives/notebooks/2025/`
+- Clear README with maintenance expectations
+
+**`perf/` folder (reviewed 2025-11-30):**
+- `perf_baseline.json` - Placeholder for performance regression baseline (not yet generated)
+- `runs/` - Transient runtime logs (git-ignored)
+- Fixed broken symlink that pointed to non-existent archive path
+- Referenced workflow `maint-52-perf-benchmark.yml` does not exist yet
+
+**`reports/` folder (reviewed 2025-11-30):**
+- `tearsheet.md` - Generated output from `analysis/tearsheet.py` (active, referenced in README)
+- Archived `code_review.md` - one-off analysis from PR #3877
+
+**`scripts/` folder (reviewed 2025-11-30):**
+- **74 scripts** organized by category (see `scripts/README.md`)
+- CI/Workflow scripts: `ci_*.py`, `ledger_*.py`, `sync_*.py`, `coverage_*.py`
+- Core dev scripts: `setup_env.sh`, `run_tests.sh`, `run_streamlit.sh`, `generate_demo.py`
+- Validation tiers: `dev_check.sh`, `validate_fast.sh`, `check_branch.sh`
+- Performance: `benchmark_performance.py`, `compare_perf.py`
+- Utilities: `archive_agents.sh`, `git_hooks.sh`, `docker_smoke.sh`
+- Archived `demo_export_fix.py` - one-off fix script
+
+**`streamlit_app/` folder (reviewed 2025-11-30):**
+- Primary Streamlit UI for the Trend Portfolio Simulator
+- `app.py` - Main entry point with demo button
+- `components/` - Reusable UI components (charts, validation, caching)
+- `pages/` - Multipage app structure:
+ - `1_Data.py` (canonical), `1_Upload.py` (legacy shim)
+ - `2_Model.py` (canonical), `2_Configure.py` (legacy shim)
+ - `3_Run.py` (canonical), `3_Results.py` (shim → 4_Results)
+ - `4_Results.py` (canonical results display)
+- Shim files kept for backward test compatibility
+- Updated tests to remove references to old `app/streamlit/` path
+
+**`tests/` folder (reorganized 2025-11-30):**
+- **3707+ tests** across multiple categories
+- **Major reorganization**: Workflow/CI tests separated into `tests/workflows/` for future repo split
+
+| Subfolder | Purpose | Contents |
+| --- | --- | --- |
+| `tests/workflows/` | **Workflow system tests** | 33 test files, `github_scripts/`, `fixtures/` |
+| `tests/app/` | Streamlit app tests | 10 test files |
+| `tests/backtesting/` | Backtesting tests | 2 test files |
+| `tests/trend_analysis/` | Core analysis tests | 8 test files |
+| `tests/scripts/` | Script tests | 6 test files |
+| `tests/tools/` | Tool tests | 4 test files |
+| `tests/unit/` | Unit tests | `util/` subfolder |
+| `tests/smoke/` | Smoke tests | 3 test files |
+| `tests/soft_coverage/` | Coverage tracking | 4 test files |
+| `tests/golden/` | Golden master tests | 2 test files |
+| `tests/fixtures/` | Core test fixtures | `score_frame_2025-06-30.csv` |
+| `tests/data/` | Test data files | 6 files |
+| `tests/proxy/` | Proxy tests | 1 test file |
+
+**Workflow tests reorganization details:**
+- Created `tests/workflows/` to separate CI/automation tests from core tests
+- Moved 33 workflow-related test files (`test_workflow_*.py`, `test_autofix_*.py`, `test_ci_*.py`, `test_keepalive_*.py`, `test_agents_*.py`, etc.)
+- Moved `tests/github_scripts/` → `tests/workflows/github_scripts/`
+- Moved workflow fixtures (`agents_pr_meta/`, `keepalive/`, `keepalive_post_work/`, `orchestrator/`) to `tests/workflows/fixtures/`
+- Updated path references in harness JS files and test imports
+- Core fixture `score_frame_2025-06-30.csv` remains in `tests/fixtures/`
+
+**`tools/` folder (reviewed 2025-11-30):**
+- CI infrastructure utilities imported by workflows and tests
+- **14 active files** used by GitHub Actions and test suite
+
+| File | Purpose | Used By |
+| --- | --- | --- |
+| `post_ci_summary.py` | Build consolidated CI summary | `pr-00-gate.yml`, `maint-46-post-ci.yml` |
+| `coverage_guard.py` | Maintain rolling coverage baseline | `maint-coverage-guard.yml` |
+| `coverage_trend.py` | Compute coverage trend for CI | `reusable-10-ci-python.yml` |
+| `enforce_gate_branch_protection.py` | Ensure branch protection rules | `health-44-gate-branch-protection.yml` |
+| `disable_legacy_workflows.py` | Disable retired workflows | `maint-47-disable-legacy-workflows.yml` |
+| `resolve_mypy_pin.py` | Resolve mypy version pin | `reusable-10-ci-python.yml` |
+| `validate_quarantine_ttl.py` | Validate test quarantine TTL | Tests |
+| `agents_index.py` | List agent bootstrap files | Tests |
+| `simulate_codex_bootstrap.py` | Simulate Codex bootstrap logic | Tests |
+| `simulate_failure_tracker.js` | Test failure tracking | Tests |
+| `test_failure_signature.py` | Signature hashing for failures | `health_summarize.py` |
+| `strip_output.py` | Strip Jupyter notebook outputs | `pre-commit` hook |
+| `pre-commit` | Git pre-commit hook | Git hooks |
+| `sanitize_workflows.sh` | Sanitize workflow YAML | Utilities |
+
+**Removed (2025-11-30):**
+- `update_workflow_catalog.py` - Generator for stale catalog (catalog itself not tracked)
+
+**`trend_model.egg-info/` (not reviewed - build artifact):**
+- Auto-generated by pip during editable install
+- Already in `.gitignore` - not tracked
+- Safe to delete, regenerates on `pip install -e .`
+
+**`docs/` folder (reviewed 2025-11-30):**
+- **62 active documentation files** after archiving completed implementation plans
+- Archived 45 files: 39 plans, 5 ops-issues, 1 audit (see `docs/archive/`)
+
+| Category | Contents | Files |
+| --- | --- | --- |
+| User guides | `UserGuide.md`, `quickstart.md`, `install.md`, `usage.md` | 4 |
+| Configuration | `ConfigMap.md`, `config.md`, `PresetStrategies.md` | 3 |
+| CLI reference | `CLI.md`, `reference.md`, `api.md` | 3 |
+| CI/Workflow | `WORKFLOW_GUIDE.md`, `ci-workflow.md`, `ci_reuse.md`, `checks.md` | 4 |
+| Agent automation | `AGENTS_POLICY.md`, `agent-automation.md`, `codex_bootstrap_verification.md` | 3 |
+| Validation | `validation-scripts.md`, `efficient-validation.md`, `fast-validation-ecosystem.md` | 3 |
+| Dependencies | `DEPENDENCY_ENFORCEMENT.md`, `DEPENDENCY_MANAGEMENT.md`, `DEPENDENCY_SYNC.md`, `DEPENDENCY_WORKFLOW.md` | 4 |
+| Development | `code_ownership.md`, `release-process.md`, `pr-iteration-policy.md` | 3 |
+| Features | `backtesting_harness.md`, `walk_forward.md`, `Walkforward.md`, `plugin-interface.md`, `metric_cache.md` | 5 |
+| Operations | `docs/ops/` - `maintenance-playbook.md`, `ci-status-summary.md`, `codex-bootstrap-facts.md`, `cosmetic-repair.md`, `template-setup.md` | 5 |
+| Phase docs | `docs/phase-1/`, `docs/phase-2/` - Design documentation for development phases | 5 |
+| Index/Meta | `INDEX.md`, `README.md`, `repository_housekeeping.md` | 3 |
+
+**Notable cleanup decisions:**
+- Kept current feature documentation even if it mentions future work
+- Kept phase docs as design history (they're properly annotated)
+- Kept operations playbooks and runbooks (actively used)
+- Archived issue-specific plans when all referenced issues are CLOSED
+- Archived implementation summaries (`*_COMPLETE.md`, `*_SUMMARY.md`) - point-in-time snapshots
+
## Overlapping docs and their scopes
| Document | Audience | Scope/status |
| --- | --- | --- |
diff --git a/docs/UserGuide.md b/docs/UserGuide.md
index 1f4d2350e3..412ae95645 100644
--- a/docs/UserGuide.md
+++ b/docs/UserGuide.md
@@ -8,7 +8,7 @@ This document introduces the main features of the Trend Model Project and explai
```bash
./scripts/setup_env.sh
```
- This creates `.venv/` and installs packages from `requirements.txt`.
+ This creates `.venv/` and installs packages from `requirements.lock`.
2. Optional: run the test suite to verify everything works:
```bash
./scripts/run_tests.sh
@@ -210,7 +210,7 @@ For reproducible results across different runs and environments, see [Reproducib
## 12.1 Walk-forward (rolling OOS) analysis
-The Streamlit Results page now includes a Walk-forward analysis expander to aggregate metrics over rolling out-of-sample windows and, optionally, by regime. See [Walkforward.md](Walkforward.md) for a quick guide and a CLI example.
+The Streamlit Results page now includes a Walk-forward analysis expander to aggregate metrics over rolling out-of-sample windows and, optionally, by regime. See [walkforward-analysis.md](walkforward-analysis.md) for a quick guide and a CLI example.
## 13. Structured run logging
diff --git a/docs/WORKFLOW_GUIDE.md b/docs/WORKFLOW_GUIDE.md
index c70713f6aa..9237e5441a 100644
--- a/docs/WORKFLOW_GUIDE.md
+++ b/docs/WORKFLOW_GUIDE.md
@@ -17,13 +17,13 @@ operational detail for the kept set.
| Prefix | Purpose | Active Examples |
| ------ | ------- | ---------------- |
-| `pr-` | Pull-request CI wrappers | `pr-00-gate.yml` |
-| `maint-` | Post-CI maintenance and self-tests | `maint-45-cosmetic-repair.yml`, `maint-keepalive.yml` |
-| `health-` | Repository health & policy checks | `health-40-sweep.yml`, `health-40-repo-selfcheck.yml`, `health-41-repo-health.yml`, `health-42-actionlint.yml`, `health-43-ci-signature-guard.yml`, `health-44-gate-branch-protection.yml` |
-| `agents-` | Agent orchestration entry points | `agents-70-orchestrator.yml`, `agents-71-codex-belt-dispatcher.yml`, `agents-72-codex-belt-worker.yml`, `agents-73-codex-belt-conveyor.yml` |
-| `reusable-` | Reusable composites invoked by other workflows | `reusable-10-ci-python.yml`, `reusable-12-ci-docker.yml`, `reusable-18-autofix.yml`, `reusable-16-agents.yml` |
+| `pr-` | Pull-request CI wrappers | `pr-00-gate.yml`, `pr-11-ci-smoke.yml` |
+| `maint-` | Post-CI maintenance and self-tests | `maint-45-cosmetic-repair.yml`, `maint-46-post-ci.yml`, `maint-47-disable-legacy-workflows.yml`, `maint-50-tool-version-check.yml`, `maint-51-dependency-refresh.yml`, `maint-52-validate-workflows.yml`, `maint-60-release.yml`, `maint-coverage-guard.yml` |
+| `health-` | Repository health & policy checks | `health-40-sweep.yml`, `health-40-repo-selfcheck.yml`, `health-41-repo-health.yml`, `health-42-actionlint.yml`, `health-43-ci-signature-guard.yml`, `health-44-gate-branch-protection.yml`, `health-50-security-scan.yml` |
+| `agents-` | Agent orchestration entry points | `agents-63-issue-intake.yml`, `agents-64-verify-agent-assignment.yml`, `agents-70-orchestrator.yml`, `agents-71-codex-belt-dispatcher.yml`, `agents-72-codex-belt-worker.yml`, `agents-73-codex-belt-conveyor.yml`, `agents-guard.yml`, `agents-pr-meta.yml`, `agents-moderate-connector.yml`, `agents-keepalive-*.yml`, `agents-debug-issue-event.yml` |
+| `reusable-` | Reusable composites invoked by other workflows | `reusable-10-ci-python.yml`, `reusable-12-ci-docker.yml`, `reusable-16-agents.yml`, `reusable-18-autofix.yml`, `reusable-agents-issue-bridge.yml` |
| `selftest-` | Manual self-tests & experiments | `selftest-reusable-ci.yml` |
-| `autofix-` assets | Shared configuration for autofix tooling | `autofix-versions.env` |
+| `autofix.yml` | CI autofix loop | `autofix.yml` |
**Naming checklist**
1. Choose the correct prefix for the workflow's scope.
@@ -40,35 +40,51 @@ The active roster below mirrors the **Keep** list in the [Workflow System Overvi
### PR Checks
- **`pr-00-gate.yml`** — Required orchestrator that calls the reusable Python (3.11/3.12) and Docker smoke workflows, then fails fast if any leg does not succeed. A lightweight `detect_doc_only` job mirrors the former PR‑14 filters (Markdown, `docs/`, `assets/`) to skip heavy legs and post the friendly notice when a PR is documentation-only.
+- **`pr-11-ci-smoke.yml`** — Minimal invariant CI that runs on push/PR to phase-2-dev and main. Installs the project, validates imports, and runs `pytest tests/test_invariants.py` for fast regression detection.
_Inline Gate helper_
- **Gate summary job (`pr-00-gate.yml`)** — Post-CI job that downloads artifacts, computes coverage deltas, runs the label-gated autofix routine, and updates the PR summary comment with a stable marker.
### Maintenance & Repo Health
-- **`maint-keepalive.yml`** — Twice-daily cron plus manual dispatch heartbeat that posts a timestamped comment (with the run URL) to the Ops heartbeat issue using the `ACTIONS_BOT_PAT` secret. Fails fast when `OPS_HEARTBEAT_ISSUE` or the PAT are missing so misconfiguration surfaces immediately.
+- **`maint-45-cosmetic-repair.yml`** — Manual dispatch utility that runs `pytest -q`, applies guard-gated cosmetic fixes via `scripts/ci_cosmetic_repair.py`, and opens a labelled PR when changes exist.
+- **`maint-46-post-ci.yml`** — Post-CI summary recovery workflow triggered by `workflow_run` on Gate completion. Propagates Gate commit status and posts summaries when the Gate's own summary job doesn't complete.
+- **`maint-47-disable-legacy-workflows.yml`** — Manual dispatch utility to disable retired workflows that still appear in the Actions UI.
+- **`maint-50-tool-version-check.yml`** — Scheduled + manual dispatch workflow that checks for tool version updates.
+- **`maint-51-dependency-refresh.yml`** — Scheduled + manual dispatch workflow for dependency updates.
+- **`maint-52-validate-workflows.yml`** — PR/push workflow that validates workflow YAML syntax and structure.
+- **`maint-60-release.yml`** — Tag-triggered release workflow for publishing packages.
+- **`maint-coverage-guard.yml`** — Daily cron + dispatch workflow that monitors Gate coverage artifacts and maintains the rolling coverage baseline breach issue.
- **`health-40-sweep.yml`** — Weekly sweep that fans out to Actionlint and branch-protection verification. Pull requests trigger the Actionlint leg (paths-filter gated) while schedule/manual runs execute both checks to keep the enforcement snapshots fresh.
+- **`health-40-repo-selfcheck.yml`** — Read-only governance probe that surfaces label coverage and branch-protection visibility gaps in the run summary.
+- **`health-41-repo-health.yml`** — Weekly repository health sweep that writes a single run-summary report covering stale branches, unassigned issues, and default-branch protection drift, with optional `workflow_dispatch` reruns.
- **`health-42-actionlint.yml`** — Underlying Actionlint job invoked by the sweep (and still runnable via manual dispatch when you need a focused lint dry run).
- **`health-43-ci-signature-guard.yml`** — Guards the CI manifest with signed fixture checks.
- **`health-44-gate-branch-protection.yml`** — Enforces branch-protection policy via `tools/enforce_gate_branch_protection.py` when the PAT is configured (now triggered on PRs or by the consolidated sweep).
-
-_Additional opt-in utilities_
-- **`health-41-repo-health.yml`** — Weekly repository health sweep that writes a single run-summary report covering stale branches, unassigned issues, and default-branch protection drift, with optional `workflow_dispatch` reruns.
-- **`health-40-repo-selfcheck.yml`** — Read-only governance probe that surfaces label coverage and branch-protection visibility gaps in the run summary.
-- **`maint-45-cosmetic-repair.yml`** — Manual dispatch utility that runs `pytest -q`, applies guard-gated cosmetic fixes via `scripts/ci_cosmetic_repair.py`, and opens a labelled PR when changes exist.
+- **`health-50-security-scan.yml`** — Security scanning workflow triggered on push, PR, and schedule. Runs vulnerability checks and security audits.
### Agents & Issues
-- **`agents-70-orchestrator.yml`** — 20-minute cron plus manual dispatch entry point for readiness, Codex bootstrap, diagnostics, verification, and keepalive sweeps. Delegates to `reusable-16-agents.yml` and accepts extended options via `options_json`.
- **`agents-63-issue-intake.yml`** — Canonical front door that seeds Codex bootstrap PRs on `agent:codex`/`agents:codex` labels, exposes manual dispatch inputs, and services ChatGPT sync via `workflow_call`.
+- **`agents-64-verify-agent-assignment.yml`** — Workflow-call validator ensuring `agent:codex` issues remain assigned to approved automation accounts.
+- **`agents-70-orchestrator.yml`** — 20-minute cron plus manual dispatch entry point for readiness, Codex bootstrap, diagnostics, verification, and keepalive sweeps. Delegates to `reusable-16-agents.yml` and accepts extended options via `options_json`.
- **`agents-71-codex-belt-dispatcher.yml`** — Cron + manual dispatcher that selects the next `agent:codex` + `status:ready` issue, prepares the deterministic `codex/issue-*` branch, labels the source issue as in-progress, and repository-dispatches the worker.
- **`agents-72-codex-belt-worker.yml`** — Repository-dispatch consumer that re-validates labels, ensures the branch diverges from the base (empty commit when needed), and opens or refreshes the Codex automation PR with labels, assignees, and activation comment.
- **`agents-73-codex-belt-conveyor.yml`** — Gate follower that squash-merges successful belt PRs, deletes the branch, closes the originating issue, posts audit breadcrumbs, and re-dispatches the dispatcher so the queue keeps moving.
-- **`agents-64-verify-agent-assignment.yml`** — Workflow-call validator ensuring `agent:codex` issues remain assigned to approved automation accounts.
+- **`agents-guard.yml`** (aka Health 45 Agents Guard) — PR workflow that validates agent-related labels and permissions.
+- **`agents-pr-meta.yml`** — PR metadata manager that serializes Codex activation commands and PR body decoration through dedicated jobs sharing a concurrency group keyed by PR number.
+- **`agents-moderate-connector.yml`** — Comment moderation workflow that filters connector-authored comments based on allow/deny lists.
+- **`agents-keepalive-branch-sync.yml`** — Dispatch-triggered utility that syncs PR branches with their base branch (merges base into head).
+- **`agents-keepalive-dispatch-handler.yml`** — Repository dispatch handler for keepalive events.
+- **`agents-debug-issue-event.yml`** — Debug workflow that dumps GitHub context on issue events (labeled, unlabeled, opened, reopened). Useful for troubleshooting label triggers.
+
+### Autofix
+- **`autofix.yml`** — CI Autofix Loop triggered on `pull_request` and `pull_request_target`. Runs formatting fixes and commits changes back to the PR branch.
### Reusable Composites
- **`reusable-10-ci-python.yml`** — Python lint/type/test reusable invoked by Gate and downstream repositories.
- **`reusable-12-ci-docker.yml`** — Docker smoke reusable invoked by Gate and external consumers.
- **`reusable-16-agents.yml`** — Reusable agent automation stack.
- **`reusable-18-autofix.yml`** — Autofix harness used by the Gate summary job.
+- **`reusable-agents-issue-bridge.yml`** — Reusable workflow for bridging issues to agent automation, called by `agents-63-issue-intake.yml`.
### Self-tests
- **`selftest-reusable-ci.yml`** — Manual entry point that houses the verification matrix and comment/summary/dual-runtime publication logic.
diff --git a/docs/api.md b/docs/api.md
index c72fe4b971..6e42d52bdf 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -1,61 +1,110 @@
-Go API
-======
-[![API Document][api-badge]][apidoc]
-
-This document describes how to use [actionlint](..) as Go library.
-
-actionlint can be used from Go programs. See [the documentation][apidoc] to know the list of all APIs. It contains
-a workflow file parser built on top of `go-yaml/yaml`, expression `${{ }}` lexer/parser/checker, etc.
-
-Followings are unexhaustive list of interesting APIs.
-
-- `Command` struct represents entire `actionlint` command. `Command.Main` takes command line arguments and runs command
- until the end and returns exit status.
-- `Linter` manages linter lifecycle and applies checks to given files. If you want to run actionlint checks in your
- program, please use this struct.
-- `Project` and `Projects` detect a project (Git repository) in a given directory path and find configuration in it.
-- `Config` represents structure of `actionlint.yaml` config file. It can be decoded by [go-yaml/yaml][go-yaml] library.
-- `Workflow`, `Job`, `Step`, ... are nodes of workflow syntax tree. `Workflow` is a root node.
-- `Parse()` parses given contents into a workflow syntax tree. It tries to find syntax errors as much as possible and
- returns found errors as slice.
-- `Pass` is a visitor to traverse a workflow syntax tree. Multiple passes can be applied at single pass using `Visitor`.
-- `Rule` is an interface for rule checkers and `RuneBase` is a base struct to implement a rule checker.
- - `RuleExpression` is a rule checker to check expression syntax in `${{ }}`.
- - `RuleShellcheck` is a rule checker to apply `shellcheck` command to `run:` sections and collect errors from it.
- - `RuleJobNeeds` is a rule checker to check dependencies in `needs:` section. It can detect cyclic dependencies.
- - ...
-- `ExprLexer` lexes expression syntax in `${{ }}` and returns slice of `Token`.
-- `ExprParser` parses given slice of `Token` and returns syntax tree for expression in `${{ }}`. `ExprNode` is an
- interface for nodes in the expression syntax tree.
-- `ExprType` is an interface of types in expression syntax `${{ }}`. `ObjectType`, `ArrayType`, `StringType`,
- `NumberType`, ... are structs to represent actual types of expression.
-- `ExprSemanticsChecker` checks semantics of expression syntax `${{ }}`. It traverses given expression syntax tree and
- deduces its type, checking types and resolving variables (contexts).
-- `ValidateRefGlob()` and `ValidatePathGlob()` validate [glob filter pattern][filter-pattern-doc] and returns all errors
- found by the validator.
-- `ActionMetadata` is a struct for action metadata file (`action.yml`). It is used to check inputs specified at `with:`
- and typing `steps.{id}.outputs` object strictly.
-- `PopularActions` global variable is the data set of popular actions' metadata collected by [the script](../scripts/generate-popular-actions).
-- `AllWebhookTypes` global variable is the mapping from all webhook names to their types collected by [the script](../scripts/generate-webhook-events).
-- `WorkflowKeyAvailability()` returns available context names and special function names for the given workflow key like
- `jobs..outputs.`. This function uses the data collected by [the script](../scripts/generate-availability).
-
-## Library versioning
-
-The version of this repository is for command line tool `actionlint`. So it does not represent the version of the library.
-It means that the library does not follow semantic versioning and any patch version bump may introduce some breaking changes.
-
-## Go version compatibility
-
-Minimum supported Go version is written in [`go.mod`](../go.mod) file in this repository. That said, older Go versions are
-actually not tested on CI. Last two major Go versions are recommended because they're tested on CI. For example, when the latest
-Go version is v1.22, v1.21 and v1.22 are nice to use.
-
----
-
-[Checks](checks.md) | [Installation](install.md) | [Usage](usage.md) | [Configuration](config.md) | [References](reference.md)
-
-[api-badge]: https://pkg.go.dev/badge/github.com/rhysd/actionlint.svg
-[apidoc]: https://pkg.go.dev/github.com/rhysd/actionlint
-[go-yaml]: https://github.com/go-yaml/yaml
-[filter-pattern-doc]: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#filter-pattern-cheat-sheet
+# API Reference
+
+This document provides an overview of the Trend Model Project's programmatic interfaces.
+
+## Python API
+
+### Core Pipeline
+
+```python
+from trend_analysis.pipeline import run
+
+# Run analysis with config file
+results = run(config_path="config/demo.yml")
+
+# Run with config object
+from trend_analysis.config import load
+config = load("config/demo.yml")
+results = run(config=config)
+```
+
+### Configuration
+
+```python
+from trend_analysis.config import load, Config
+
+# Load from file
+config = load("config/defaults.yml")
+
+# Access configuration values
+print(config.data.csv_path)
+print(config.portfolio.top_n)
+```
+
+### Metrics
+
+```python
+from trend_analysis.metrics import (
+ calculate_returns,
+ calculate_sharpe_ratio,
+ calculate_max_drawdown,
+ calculate_cagr,
+)
+
+# Calculate metrics on return series
+sharpe = calculate_sharpe_ratio(returns, risk_free_rate=0.02)
+max_dd = calculate_max_drawdown(returns)
+cagr = calculate_cagr(returns, periods_per_year=12)
+```
+
+### Export
+
+```python
+from trend_analysis.export import export_to_excel, export_to_csv, export_to_json
+
+# Export results to various formats
+export_to_excel(results, "output.xlsx")
+export_to_csv(results, "output")
+export_to_json(results, "output.json")
+```
+
+## REST API
+
+The project includes a FastAPI server for programmatic access.
+
+### Starting the Server
+
+```bash
+uvicorn trend_analysis.api_server:app --host 0.0.0.0 --port 8000
+```
+
+### Endpoints
+
+| Endpoint | Method | Description |
+|----------|--------|-------------|
+| `/health` | GET | Health check |
+| `/analyze` | POST | Run analysis with config |
+| `/docs` | GET | OpenAPI documentation |
+
+### Example Request
+
+```bash
+curl -X POST http://localhost:8000/analyze \
+ -H "Content-Type: application/json" \
+ -d '{"config_path": "config/demo.yml"}'
+```
+
+## CLI Interface
+
+See [CLI.md](CLI.md) for command-line interface documentation.
+
+## Module Structure
+
+```
+src/trend_analysis/
+├── __init__.py # Package exports
+├── config/ # Configuration loading
+├── core/ # Core algorithms
+├── engine/ # Analysis engine
+├── export/ # Export functionality
+├── metrics.py # Metric calculations
+├── pipeline.py # Main pipeline
+├── data.py # Data loading/processing
+└── api_server/ # REST API
+```
+
+## See Also
+
+- [UserGuide.md](UserGuide.md) - User documentation
+- [CLI.md](CLI.md) - Command-line interface
+- [config.md](config.md) - Configuration reference
diff --git a/docs/archive/README.md b/docs/archive/README.md
new file mode 100644
index 0000000000..1ec530d0f3
--- /dev/null
+++ b/docs/archive/README.md
@@ -0,0 +1,33 @@
+# Documentation Archive
+
+This directory contains historical documentation that is no longer actively maintained
+but is preserved for reference purposes.
+
+## Contents
+
+| Folder | Description | Count |
+|--------|-------------|-------|
+| `plans/` | Completed implementation plans for closed issues | 39 |
+| `ops-issues/` | Operations issue scopes and smoke test artifacts | 5 |
+| `audits/` | Point-in-time audit reports | 1 |
+
+## Files
+
+- `ARCHIVE_WORKFLOWS.md` - Retired workflow documentation
+
+## Why Archive?
+
+Documents are archived when:
+- They describe **completed implementation work** for closed issues
+- They are **dated audit reports** superseded by newer versions
+- They reference **retired workflows or features**
+- They are **progress logs** for finished tasks
+
+## Active Documentation
+
+Current documentation lives in `docs/` root and subdirectories like:
+- `docs/ops/` - Operations playbooks and runbooks
+- `docs/phase-1/`, `docs/phase-2/` - Phase-specific design documentation
+- `docs/keepalive/` - Keepalive status and monitoring
+
+See `docs/INDEX.md` for the complete documentation map.
diff --git a/docs/repository_housekeeping_audit_2025-11-25.md b/docs/archive/audits/repository_housekeeping_audit_2025-11-25.md
similarity index 100%
rename from docs/repository_housekeeping_audit_2025-11-25.md
rename to docs/archive/audits/repository_housekeeping_audit_2025-11-25.md
diff --git a/docs/ops/issue-2494-scope.md b/docs/archive/ops-issues/issue-2494-scope.md
similarity index 100%
rename from docs/ops/issue-2494-scope.md
rename to docs/archive/ops-issues/issue-2494-scope.md
diff --git a/docs/ops/issue-2615-scope.md b/docs/archive/ops-issues/issue-2615-scope.md
similarity index 100%
rename from docs/ops/issue-2615-scope.md
rename to docs/archive/ops-issues/issue-2615-scope.md
diff --git a/docs/ops/issue-2820-scope.md b/docs/archive/ops-issues/issue-2820-scope.md
similarity index 100%
rename from docs/ops/issue-2820-scope.md
rename to docs/archive/ops-issues/issue-2820-scope.md
diff --git a/docs/ops/smoke-automerge-2.md b/docs/archive/ops-issues/smoke-automerge-2.md
similarity index 100%
rename from docs/ops/smoke-automerge-2.md
rename to docs/archive/ops-issues/smoke-automerge-2.md
diff --git a/docs/ops/smoke-automerge.md b/docs/archive/ops-issues/smoke-automerge.md
similarity index 100%
rename from docs/ops/smoke-automerge.md
rename to docs/archive/ops-issues/smoke-automerge.md
diff --git a/docs/DEPENDENCY_IMPLEMENTATION_SUMMARY.md b/docs/archive/plans/DEPENDENCY_IMPLEMENTATION_SUMMARY.md
similarity index 100%
rename from docs/DEPENDENCY_IMPLEMENTATION_SUMMARY.md
rename to docs/archive/plans/DEPENDENCY_IMPLEMENTATION_SUMMARY.md
diff --git a/docs/DEPENDENCY_MANAGEMENT_SUMMARY.md b/docs/archive/plans/DEPENDENCY_MANAGEMENT_SUMMARY.md
similarity index 100%
rename from docs/DEPENDENCY_MANAGEMENT_SUMMARY.md
rename to docs/archive/plans/DEPENDENCY_MANAGEMENT_SUMMARY.md
diff --git a/docs/DEPENDENCY_SYNC.md b/docs/archive/plans/DEPENDENCY_SYNC.md
similarity index 100%
rename from docs/DEPENDENCY_SYNC.md
rename to docs/archive/plans/DEPENDENCY_SYNC.md
diff --git a/docs/DEPENDENCY_SYSTEM_COMPLETE.md b/docs/archive/plans/DEPENDENCY_SYSTEM_COMPLETE.md
similarity index 100%
rename from docs/DEPENDENCY_SYSTEM_COMPLETE.md
rename to docs/archive/plans/DEPENDENCY_SYSTEM_COMPLETE.md
diff --git a/docs/DEPENDENCY_WORKFLOW.md b/docs/archive/plans/DEPENDENCY_WORKFLOW.md
similarity index 100%
rename from docs/DEPENDENCY_WORKFLOW.md
rename to docs/archive/plans/DEPENDENCY_WORKFLOW.md
diff --git a/docs/archive/plans/actionlint-api.md b/docs/archive/plans/actionlint-api.md
new file mode 100644
index 0000000000..c72fe4b971
--- /dev/null
+++ b/docs/archive/plans/actionlint-api.md
@@ -0,0 +1,61 @@
+Go API
+======
+[![API Document][api-badge]][apidoc]
+
+This document describes how to use [actionlint](..) as Go library.
+
+actionlint can be used from Go programs. See [the documentation][apidoc] to know the list of all APIs. It contains
+a workflow file parser built on top of `go-yaml/yaml`, expression `${{ }}` lexer/parser/checker, etc.
+
+Followings are unexhaustive list of interesting APIs.
+
+- `Command` struct represents entire `actionlint` command. `Command.Main` takes command line arguments and runs command
+ until the end and returns exit status.
+- `Linter` manages linter lifecycle and applies checks to given files. If you want to run actionlint checks in your
+ program, please use this struct.
+- `Project` and `Projects` detect a project (Git repository) in a given directory path and find configuration in it.
+- `Config` represents structure of `actionlint.yaml` config file. It can be decoded by [go-yaml/yaml][go-yaml] library.
+- `Workflow`, `Job`, `Step`, ... are nodes of workflow syntax tree. `Workflow` is a root node.
+- `Parse()` parses given contents into a workflow syntax tree. It tries to find syntax errors as much as possible and
+ returns found errors as slice.
+- `Pass` is a visitor to traverse a workflow syntax tree. Multiple passes can be applied at single pass using `Visitor`.
+- `Rule` is an interface for rule checkers and `RuneBase` is a base struct to implement a rule checker.
+ - `RuleExpression` is a rule checker to check expression syntax in `${{ }}`.
+ - `RuleShellcheck` is a rule checker to apply `shellcheck` command to `run:` sections and collect errors from it.
+ - `RuleJobNeeds` is a rule checker to check dependencies in `needs:` section. It can detect cyclic dependencies.
+ - ...
+- `ExprLexer` lexes expression syntax in `${{ }}` and returns slice of `Token`.
+- `ExprParser` parses given slice of `Token` and returns syntax tree for expression in `${{ }}`. `ExprNode` is an
+ interface for nodes in the expression syntax tree.
+- `ExprType` is an interface of types in expression syntax `${{ }}`. `ObjectType`, `ArrayType`, `StringType`,
+ `NumberType`, ... are structs to represent actual types of expression.
+- `ExprSemanticsChecker` checks semantics of expression syntax `${{ }}`. It traverses given expression syntax tree and
+ deduces its type, checking types and resolving variables (contexts).
+- `ValidateRefGlob()` and `ValidatePathGlob()` validate [glob filter pattern][filter-pattern-doc] and returns all errors
+ found by the validator.
+- `ActionMetadata` is a struct for action metadata file (`action.yml`). It is used to check inputs specified at `with:`
+ and typing `steps.{id}.outputs` object strictly.
+- `PopularActions` global variable is the data set of popular actions' metadata collected by [the script](../scripts/generate-popular-actions).
+- `AllWebhookTypes` global variable is the mapping from all webhook names to their types collected by [the script](../scripts/generate-webhook-events).
+- `WorkflowKeyAvailability()` returns available context names and special function names for the given workflow key like
+ `jobs..outputs.`. This function uses the data collected by [the script](../scripts/generate-availability).
+
+## Library versioning
+
+The version of this repository is for command line tool `actionlint`. So it does not represent the version of the library.
+It means that the library does not follow semantic versioning and any patch version bump may introduce some breaking changes.
+
+## Go version compatibility
+
+Minimum supported Go version is written in [`go.mod`](../go.mod) file in this repository. That said, older Go versions are
+actually not tested on CI. Last two major Go versions are recommended because they're tested on CI. For example, when the latest
+Go version is v1.22, v1.21 and v1.22 are nice to use.
+
+---
+
+[Checks](checks.md) | [Installation](install.md) | [Usage](usage.md) | [Configuration](config.md) | [References](reference.md)
+
+[api-badge]: https://pkg.go.dev/badge/github.com/rhysd/actionlint.svg
+[apidoc]: https://pkg.go.dev/github.com/rhysd/actionlint
+[go-yaml]: https://github.com/go-yaml/yaml
+[filter-pattern-doc]: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#filter-pattern-cheat-sheet
diff --git a/docs/archive/plans/actionlint-checks-reference.md b/docs/archive/plans/actionlint-checks-reference.md
new file mode 100644
index 0000000000..82fa9f379a
--- /dev/null
+++ b/docs/archive/plans/actionlint-checks-reference.md
@@ -0,0 +1,2896 @@
+All checks done by actionlint
+=============================
+
+This document describes all checks done by [actionlint](..) with example inputs, outputs, and playground links.
+
+List of checks:
+
+- [Unexpected keys](#check-unexpected-keys)
+- [Missing required keys or key duplicates](#check-missing-required-duplicate-keys)
+- [Unexpected empty mappings](#check-empty-mapping)
+- [Unexpected mapping values](#check-mapping-values)
+- [Syntax check for expression `${{ }}`](#check-syntax-expression)
+- [Type checks for expression syntax in `${{ }}`](#check-type-check-expression)
+- [Contexts and built-in functions](#check-contexts-and-builtin-func)
+- [Contextual typing for `steps.` objects](#check-contextual-step-object)
+- [Contextual typing for `matrix` object](#check-contextual-matrix-object)
+- [Contextual typing for `needs` object](#check-contextual-needs-object)
+- [Strict type checks for comparison operators](#check-comparison-types)
+- [shellcheck integration for `run:`](#check-shellcheck-integ)
+- [pyflakes integration for `run:`](#check-pyflakes-integ)
+- [Script injection by potentially untrusted inputs](#untrusted-inputs)
+- [Job dependencies validation](#check-job-deps)
+- [Matrix values](#check-matrix-values)
+- [Webhook events validation](#check-webhook-events)
+- [Workflow dispatch event validation](#check-workflow-dispatch-events)
+- [Glob filter pattern syntax validation](#check-glob-pattern)
+- [CRON syntax check at `schedule:`](#check-cron-syntax)
+- [Runner labels](#check-runner-labels)
+- [Action format in `uses:`](#check-action-format)
+- [Local action inputs validation at `with:`](#check-local-action-inputs)
+- [Popular action inputs validation at `with:`](#check-popular-action-inputs)
+- [Outdated popular actions detection at `with:`](#detect-outdated-popular-actions)
+- [Shell name validation at `shell:`](#check-shell-names)
+- [Job ID and step ID uniqueness](#check-job-step-ids)
+- [Hardcoded credentials](#check-hardcoded-credentials)
+- [Environment variable names](#check-env-var-names)
+- [Permissions](#permissions)
+- [Reusable workflows](#check-reusable-workflows)
+- [ID naming convention](#id-naming-convention)
+- [Contexts and special functions availability](#ctx-spfunc-availability)
+- [Deprecated workflow commands](#check-deprecated-workflow-commands)
+- [Conditions always evaluated to true at `if:`](#if-cond-always-true)
+- [Action metadata syntax validation](#action-metadata-syntax)
+
+Note that actionlint focuses on catching mistakes in workflow files. If you want some general code style checks, please consider
+using a general YAML checker like [yamllint][].
+
+
+## Unexpected keys
+
+Example input:
+
+```yaml
+on: push
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ # ERROR: Typo of `defaults:`
+ default:
+ run:
+ working-directory: /path/to/dir
+ steps:
+ - run: echo hello
+ # ERROR: `shell:` must be in lower case
+ Shell: bash
+```
+
+Output:
+
+```
+test.yaml:6:5: unexpected key "default" for "job" section. expected one of "concurrency", "container", "continue-on-error", "defaults", "env", "environment", "if", "name", "needs", "outputs", "permissions", "runs-on", "secrets", "services", "steps", "strategy", "timeout-minutes", "uses", "with" [syntax-check]
+ |
+6 | default:
+ | ^~~~~~~~
+test.yaml:12:9: unexpected key "Shell" for "step" section. expected one of "continue-on-error", "env", "id", "if", "name", "run", "shell", "timeout-minutes", "uses", "with", "working-directory" [syntax-check]
+ |
+12 | Shell: bash
+ | ^~~~~~
+```
+
+[Playground](https://rhysd.github.io/actionlint#eJw9jEEOwyAMBO95xX4AcecbfQEkTkmLMMK2ov6+ATU92d6ZNdeAZpKXFycJC6AkOibQrYrji1uyquZKHGyijfZo5edN816Bk/v7qE+3HZ1W5f4J8C1q9sr+yqYnSk3uipt90JoZmUrh/6vHOANSlPwFtPsxjA==)
+
+[Workflow syntax][syntax-doc] defines what keys can be defined in which mapping object. When unknown key is defined, it makes
+the workflow run fail.
+
+actionlint can detect unexpected keys while parsing workflow syntax and report them as an error.
+
+Key names are basically case sensitive (though some specific key names are case insensitive). This check is useful to catch
+case-sensitivity mistakes.
+
+
+## Missing required keys and key duplicates
+
+Example input:
+
+```yaml
+on: push
+jobs:
+ test:
+ strategy:
+ # ERROR: Matrix name is duplicated. These keys are case insensitive
+ matrix:
+ version_name: [v1, v2]
+ VERSION_NAME: [V1, V2]
+ # ERROR: runs-on is missing
+ steps:
+ - run: echo 'hello'
+```
+
+Output:
+
+```
+test.yaml:3:3: "runs-on" section is missing in job "test" [syntax-check]
+ |
+3 | test:
+ | ^~~~~
+test.yaml:8:9: key "version_name" is duplicated in "matrix" section. previously defined at line:7,col:9. note that key names are case insensitive [syntax-check]
+ |
+8 | VERSION_NAME: [V1, V2]
+ | ^~~~~~~~~~~~~
+```
+
+[Playground](https://rhysd.github.io/actionlint#eJzLz7NSKCgtzuDKyk8qtuJSUChJLS4B0QoKxSVFiSWp6ZUQnoJCbmJJUWYFjKegUJZaVJyZnxefl5ibaqUQXWaoo1BmFAuXDnMNCvb094v3c/R1BUqHAaXDoNLFJakFxTCDdBWKSoGOSE3OyFdQz0jNyclXBwA2byiy)
+
+Some mappings must include specific keys. For example, job mappings must include `runs-on:` and `steps:`.
+
+And duplicate keys are not allowed. In workflow syntax, comparing some keys is **case insensitive**. For example, the job ID
+`test` in lower case and the job ID `TEST` in upper case are not able to exist in the same workflow.
+
+actionlint checks these missing required keys and duplicate keys while parsing, and reports an error.
+
+
+## Unexpected empty mappings
+
+Example input:
+
+```yaml
+on: push
+jobs:
+```
+
+Output:
+
+```
+test.yaml:2:6: "jobs" section should not be empty. please remove this section if it's unnecessary [syntax-check]
+ |
+2 | jobs:
+ | ^
+```
+
+[Playground](https://rhysd.github.io/actionlint#eJzLz7NSKCgtzuDKyk8qtgIAJQsE6g==)
+
+Some mappings and sequences should not be empty. For example, `steps:` must include at least one step.
+
+actionlint checks such mappings and sequences are not empty while parsing, and reports the empty mappings and sequences as an
+error.
+
+
+## Unexpected mapping values
+
+Example input:
+
+```yaml
+on: push
+jobs:
+ test:
+ strategy:
+ # ERROR: Boolean value "true" or "false" is expected
+ fail-fast: off
+ # ERROR: Integer value is expected
+ max-parallel: 1.5
+ runs-on: ubuntu-latest
+ steps:
+ - run: sleep 200
+ # ERROR: Float value is expected
+ timeout-minutes: two minutes
+```
+
+Output:
+
+```
+test.yaml:6:18: expecting a single ${{...}} expression or boolean literal "true" or "false", but found plain text node [syntax-check]
+ |
+6 | fail-fast: off
+ | ^~~
+test.yaml:8:21: expected scalar node for integer value but found scalar node with "!!float" tag [syntax-check]
+ |
+8 | max-parallel: 1.5
+ | ^~~
+test.yaml:13:26: expecting a single ${{...}} expression or float number literal, but found plain text node [syntax-check]
+ |
+13 | timeout-minutes: two minutes
+ | ^~~
+```
+
+[Playground](https://rhysd.github.io/actionlint#eJw1jssNAjEMRO9bxTQQtCBxSTdeyYGg/BTbArongXCy5o395Fo8msl9e9RD/AYoi84JiHZSvr1/CQgUkws0atQQFsz0co06pcTJ43y6fnm3Iq4OtR1W1FyiqV1WbvJXurnpIYm54bLvC48vYuZq6nIsNk499FmxwgdsuTVm)
+
+Some mapping values are restricted to some constant strings. Several mapping values expect boolean value like `true` or
+`false`. And some mapping values expect integer or floating number values.
+
+actionlint checks such constant strings are used properly while parsing and reports an error when an unexpected value is
+specified.
+
+
+## Syntax check for expression `${{ }}`
+
+Example input:
+
+```yaml
+on: push
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ # " is not available for string literal delimiter
+ - run: echo '${{ "hello" }}'
+ # + operator does not exist
+ - run: echo '${{ 1 + 1 }}'
+ # Missing ')' paren
+ - run: echo "${{ toJson(hashFiles('**/lock', '**/cache/') }}"
+ # unexpected end of input
+ - run: echo '${{ github.event. }}'
+```
+
+Output:
+
+```
+test.yaml:7:24: got unexpected character '"' while lexing expression, expecting 'a'..'z', 'A'..'Z', '_', '0'..'9', ''', '}', '(', ')', '[', ']', '.', '!', '<', '>', '=', '&', '|', '*', ',', ' '. do you mean string literals? only single quotes are available for string delimiter [expression]
+ |
+7 | - run: echo '${{ "hello" }}'
+ | ^~~~~~~
+test.yaml:9:26: got unexpected character '+' while lexing expression, expecting 'a'..'z', 'A'..'Z', '_', '0'..'9', ''', '}', '(', ')', '[', ']', '.', '!', '<', '>', '=', '&', '|', '*', ',', ' ' [expression]
+ |
+9 | - run: echo '${{ 1 + 1 }}'
+ | ^
+test.yaml:11:65: unexpected end of input while parsing arguments of function call. expecting ",", ")" [expression]
+ |
+11 | - run: echo "${{ toJson(hashFiles('**/lock', '**/cache/') }}"
+ | ^~~
+test.yaml:13:38: unexpected end of input while parsing object property dereference like 'a.b' or array element dereference like 'a.*'. expecting "IDENT", "*" [expression]
+ |
+13 | - run: echo '${{ github.event. }}'
+ | ^~~
+```
+
+[Playground](https://rhysd.github.io/actionlint#eJx1jTEOwjAMRfeewoqQUgptxZoDMHCLJLJwIYor7LBUvTsNrHT48pfekz9nB3MRah4cxDUAiqL1ArxKlp43XkLJWvrkK/siUZzlZwH01XSAkRjsYVnAEKbEBtbV7ikXOG35L5gqKN+Ec0te6DollNZ23Zg4Pu0Zao0+Eo72uP0weyP3SamEAd+YdahjH8ffRDM=)
+
+actionlint lexes and parses expression in `${{ }}` following [the expression syntax document][expr-doc]. It can detect
+many syntax errors like invalid characters, missing parens, unexpected end of input, ...
+
+
+## Type checks for expression syntax in `${{ }}`
+
+actionlint checks types of expressions in `${{ }}` placeholders of templates. The following types are supported by the type
+checker.
+
+| Type | Description | Notation |
+|---------------|--------------------------------------------------------------------------------------------|--------------------------|
+| Any | Any value like `any` type in TypeScript. Fallback type when a value can no longer be typed | `any` |
+| Number | Number value (integer or float) | `number` |
+| Bool | Boolean value | `bool` |
+| String | String value | `string` |
+| Null | Type of `null` value | `null` |
+| Array | Array of specific type elements | `array` |
+| Loose object | Object which can contain any properties | `object` |
+| Strict object | Object whose properties are strictly typed | `{prop1: T1, prop2: T2}` |
+| Map object | Object who has specific type values like `env` context | `{string => T}` |
+
+Type check by actionlint is more strict than GitHub Actions runtime.
+
+- Only `any` and `number` are allowed to be converted to string implicitly
+- Implicit conversion to `number` is not allowed
+- Object, array, and null are not allowed to be evaluated at `${{ }}`
+
+Example input:
+
+```yaml
+on: push
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ # ERROR: `env` is object. Index access object is invalid
+ - run: echo '${{ env[0] }}'
+ # ERROR: Properties in objects are strongly typed. Missing property can be caught
+ - run: echo '${{ job.container.os }}'
+ # ERROR: `github.repository` is string. Trying to access .owner property is invalid
+ - run: echo '${{ github.repository.owner }}'
+ # ERROR: Objects, arrays and null should not be evaluated at ${{ }} since the outputs are useless
+ - run: echo '${{ env }}'
+```
+
+Output:
+
+```
+test.yaml:7:28: property access of object must be type of string but got "number" [expression]
+ |
+7 | - run: echo '${{ env[0] }}'
+ | ^~
+test.yaml:9:24: property "os" is not defined in object type {id: string; network: string} [expression]
+ |
+9 | - run: echo '${{ job.container.os }}'
+ | ^~~~~~~~~~~~~~~~
+test.yaml:11:24: receiver of object dereference "owner" must be type of object but got "string" [expression]
+ |
+11 | - run: echo '${{ github.repository.owner }}'
+ | ^~~~~~~~~~~~~~~~~~~~~~~
+test.yaml:13:20: object, array, and null values should not be evaluated in template with ${{ }} but evaluating the value of type {string => string} [expression]
+ |
+13 | - run: echo '${{ env }}'
+ | ^~~
+```
+
+[Playground](https://rhysd.github.io/actionlint#eJx9jrEKAjEQRPv7iimEqxKs8yticTkWE5FsyO4qcty/m2jtVVO8N8xwCagmabpzlDABSqIjgWZFHHdu0YqaeyyDfZEoVflZgBtmAK2JMZ+2DVSel/MV+z7/M/qYX7nokgs1z3Lk3rImi75RZcnK7e351VtHlX5g4A+nCkLw)
+
+Type checks for expression syntax in `${{ }}` are done by semantics checker. Note that actual type checks by GitHub Actions
+runtime is loose.
+
+Any object value can be assigned into string value as string `'Object'`. `echo '${{ env }}'` will be replaced with
+`echo 'Object'`. And an array can also be converted into `'Array'` string. Such loose conversions are bugs in almost all cases.
+actionlint checks types more strictly. actionlint checks values evaluated at `${{ }}` are not object (replaced with string
+`'Object'`), array (replaced with string `'Array'`), nor null (replaced with string `''`). If you want to check a content of
+object or array, use `toJSON()` function.
+
+```
+echo '${{ toJSON(github.event) }}'
+```
+
+There are two object types internally. One is an object which is strict for properties, which causes a type error when trying to
+access unknown properties. And another is an object which is not strict for properties, which allows to access unknown properties.
+In the case, accessing unknown property is typed as `any`.
+
+When the type check cannot be done statically, the type is deduced to `any` (e.g. return type of `toJSON()`).
+
+As special case of `${{ }}`, it can be used for expanding object and array values.
+
+Example input:
+
+```yaml
+on: push
+jobs:
+ test:
+ strategy:
+ matrix:
+ env_string:
+ - 'FOO=BAR'
+ - 'FOO=PIYO'
+ env_object:
+ - FOO: BAR
+ - FOO: PIYO
+ runs-on: ubuntu-latest
+ steps:
+ # OK: Expanding object at 'env:' section
+ - run: echo "$FOO"
+ env: ${{ matrix.env_object }}
+ # ERROR: String value cannot be expanded as object
+ - run: echo "$FOO"
+ env: ${{ matrix.env_string }}
+```
+
+Output:
+
+```
+test.yaml:19:14: type of expression at "env" must be object but found type string [expression]
+ |
+19 | env: ${{ matrix.env_string }}
+ | ^~~
+```
+
+[Playground](https://rhysd.github.io/actionlint#eJydkM0KgzAQhO8+xSCCp/QBAj20h0JPFm89lUSCP7SJmE1pEd+9SVWseOtp2WHm20mM5midraLGSMsjgJSlMAFLnSBVvscNeAjq6te8AUo/b95T63LRAIb0lGX74yFPt+rlfM3SFcDIRhW0BngnhwdsxZD/qp3Tlhnf3UmnybG7CL2n2qq1M5AFJ4cqKoM48Yz49zpH0vfTu3ZLGwzDf/HxN3z8A4EEWVQ=)
+
+In above example, environment variables mapping is expanded at `env:` section. actionlint checks type of the expanded value.
+
+
+## Contexts and built-in functions
+
+Example input:
+
+```yaml
+on: push
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ # Access undefined context
+ - run: echo '${{ unknown_context }}'
+ # Access undefined property of context
+ - run: echo '${{ github.events }}'
+ # Calling undefined function (start's'With is correct)
+ - run: echo "${{ startWith('hello, world', 'lo,') }}"
+ # Wrong number of arguments
+ - run: echo "${{ startsWith('hello, world') }}"
+ # Wrong type of parameter
+ - run: echo "${{ startsWith('hello, world', github.event) }}"
+ # Function overloads can be handled properly. contains() has string version and array version
+ - run: echo "${{ contains('hello, world', 'lo,') }}"
+ - run: echo "${{ contains(github.event.labels.*.name, 'enhancement') }}"
+ # format() has a special check for formatting string
+ - run: echo "${{ format('{0}{1}', 1, 2, 3) }}"
+```
+
+Output:
+
+```
+test.yaml:7:24: undefined variable "unknown_context". available variables are "env", "github", "job", "matrix", "needs", "runner", "secrets", "steps", "strategy", "vars" [expression]
+ |
+7 | - run: echo '${{ unknown_context }}'
+ | ^~~~~~~~~~~~~~~
+test.yaml:9:24: property "events" is not defined in object type {workspace: string; env: string; event_name: string; event_path: string; ...} [expression]
+ |
+9 | - run: echo '${{ github.events }}'
+ | ^~~~~~~~~~~~~
+test.yaml:11:24: undefined function "startWith". available functions are "always", "cancelled", "contains", "endswith", "failure", "format", "fromjson", "hashfiles", "join", "startswith", "success", "tojson" [expression]
+ |
+11 | - run: echo "${{ startWith('hello, world', 'lo,') }}"
+ | ^~~~~~~~~~~~~~~~~
+test.yaml:13:24: number of arguments is wrong. function "startsWith(string, string) -> bool" takes 2 parameters but 1 arguments are given [expression]
+ |
+13 | - run: echo "${{ startsWith('hello, world') }}"
+ | ^~~~~~~~~~~~~~~~~~
+test.yaml:15:51: 2nd argument of function call is not assignable. "object" cannot be assigned to "string". called function type is "startsWith(string, string) -> bool" [expression]
+ |
+15 | - run: echo "${{ startsWith('hello, world', github.event) }}"
+ | ^~~~~~~~~~~~~
+test.yaml:20:24: format string "{0}{1}" does not contain placeholder {2}. remove argument which is unused in the format string [expression]
+ |
+20 | - run: echo "${{ format('{0}{1}', 1, 2, 3) }}"
+ | ^~~~~~~~~~~~~~~~
+```
+
+[Playground](https://rhysd.github.io/actionlint#eJydkNGKwjAQRd/9ikGEuJIWdd/6Iz5KWmeNazojnYkKpf9uorAorH3wKYR7zs0lTBWcovjJL9dSTQAURfMJ0EWSglMe60gai+Bydo9E8SQPCqDIZAXYeAYz63uIdCS+0LZhUrwqDIN5h+4P6mNd4hlJ5Q04zaCo63ST6LnxGAJbuHAXdsaCSRfzldzpqCv/yJ9Z9mX1aEf+AXcg+WD0n/r8WBlcjUHKRUmuxVSD5B012KZsvO6Hu9bp3PTLoV8NacHKwtrC9126AZ31neg=)
+
+[Contexts][contexts-doc] and [built-in functions][funcs-doc] are strongly typed. Typos in property access of contexts and
+function names can be checked. And invalid function calls like wrong number of arguments or type mismatch at parameter also
+can be checked thanks to type checker.
+
+The semantics checker can properly handle that
+
+- some functions are overloaded (e.g. `contains(str, substr)` and `contains(array, item)`)
+- some parameters are optional (e.g. `join(strings, sep)` and `join(strings)`)
+- some parameters are repeatable (e.g. `hashFiles(file1, file2, ...)`)
+
+In addition, `format()` function has a special check for placeholders in the first parameter which represents the formatting
+string.
+
+Note that context names and function names are case insensitive. For example, `toJSON` and `toJson` are the same function.
+
+
+## Contextual typing for `steps.` objects
+
+Example input:
+
+```yaml
+on: push
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ outputs:
+ # Step outputs can be used in job outputs since this section is evaluated after all steps were run
+ foo: '${{ steps.get_value.outputs.name }}'
+ steps:
+ # ERROR: Access undefined step outputs
+ - run: echo '${{ steps.get_value.outputs.name }}'
+ # Outputs are set here
+ - run: echo "foo=value" >> "$GITHUB_OUTPUT"
+ id: get_value
+ # OK
+ - run: echo '${{ steps.get_value.outputs.name }}'
+ # OK
+ - run: echo '${{ steps.get_value.conclusion }}'
+ other:
+ runs-on: ubuntu-latest
+ steps:
+ # ERROR: Access undefined step outputs. Step objects are job-local
+ - run: echo '${{ steps.get_value.outputs.name }}'
+```
+
+Output:
+
+```
+test.yaml:10:24: property "get_value" is not defined in object type {} [expression]
+ |
+10 | - run: echo '${{ steps.get_value.outputs.name }}'
+ | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~
+test.yaml:22:24: property "get_value" is not defined in object type {} [expression]
+ |
+22 | - run: echo '${{ steps.get_value.outputs.name }}'
+ | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~
+```
+
+[Playground](https://rhysd.github.io/actionlint#eJytkEsOglAMRees4g5MGD0W0MS1GMAqGHwltHVC2Ls+Pg6MiTE66uCec9tUIqF3bbKLVEoZYKyWJjB41CCP3CuP5qErUzZH4ta76cIBJxFCvhtHqHGvxZntcCs752IFi1heGdOUz8IMbW5IewhcN/JFxYtHpGxhIZHAfTqJ5oJNANoj4dn7z/XvvFpi3bm2EldLrOHh42d/+80ddrSUCw==)
+
+Outputs of step can be accessed via `steps.` objects. The `steps` context is dynamic:
+
+- Accessing the outputs before running the step causes `null`
+- Outputs of steps only in the job can be accessed. It cannot access steps across jobs
+
+It is a common mistake to access the wrong step outputs since people often forget to fix placeholders on copying&pasting
+steps. actionlint can catch invalid accesses to step outputs and reports them as errors.
+
+When the outputs are set by popular actions, the outputs object is more strictly typed.
+
+Example input:
+
+```yaml
+on: push
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ # ERROR: The step is not run yet at this point
+ - run: echo ${{ steps.cache.outputs.cache-hit }}
+ # actions/cache sets cache-hit output
+ - uses: actions/cache@v3
+ id: cache
+ with:
+ key: ${{ hashFiles('**/*.lock') }}
+ path: ./packages
+ # OK
+ - run: echo ${{ steps.cache.outputs.cache-hit }}
+ # ERROR: Typo at output name
+ - run: echo ${{ steps.cache.outputs.cache_hit }}
+```
+
+Output:
+
+```
+test.yaml:8:23: property "cache" is not defined in object type {} [expression]
+ |
+8 | - run: echo ${{ steps.cache.outputs.cache-hit }}
+ | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+test.yaml:18:23: property "cache_hit" is not defined in object type {cache-hit: string} [expression]
+ |
+18 | - run: echo ${{ steps.cache.outputs.cache_hit }}
+ | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+```
+
+[Playground](https://rhysd.github.io/actionlint#eJyNTksKwjAQ3fcUbyFUC0nBZVauvIakMZjY0gRnokjp3W3TUl26Gt53XugVYiJXFPfQkCoAtsTzBR6pJxEmQ2pSz0l0etayRGwjLS5AIJElBW3Yh55qo42zp+dxlQF/Vcjkxrw8O7UhoLVvhd0wwGlyZ99Z2pdVVVeyC6YtDxjHH3PUUxiyjtq0+mZpmzENVrDGhVyVN8r8V4bEMfGKhPP8bfw7dlliH1xHWso=)
+
+In the above example, [actions/cache][actions-cache] action sets `cache-hit` output so that the following steps can know
+whether the cache was hit or not. At line 8, the cache action is not run yet. So `cache` property does not exist in the
+`steps` context yet. On running the step whose ID is `cache`, `steps.cache` object is typed as
+`{outputs: {cache-hit: any}, conclusion: string, outcome: string}`. At line 18, the expression has a typo in the output
+name. actionlint can check it because properties of `steps.cache.outputs` are typed.
+
+This strict typing for outputs is also applied to local actions. Let's say we have the following local action.
+
+```yaml
+name: 'My action with output'
+author: 'rhysd '
+description: 'my action with outputs'
+
+outputs:
+ some_value:
+ description: some value returned from this action
+
+runs:
+ using: 'node20'
+ main: 'index.js'
+```
+
+Example input:
+
+```yaml
+on: push
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ # ERROR: The step is not yet run
+ - run: echo ${{ steps.my_action.outputs.some_value }}
+ # The action runs here and sets its outputs
+ - uses: ./.github/actions/my-action-with-output
+ id: my_action
+ # OK
+ - run: echo ${{ steps.my_action.outputs.some_value }}
+ # ERROR: No output named 'some-value' (typo)
+ - run: echo ${{ steps.my_action.outputs.some-value }}
+```
+
+Output:
+
+```
+test.yaml:8:23: property "my_action" is not defined in object type {} [expression]
+ |
+8 | - run: echo ${{ steps.my_action.outputs.some_value }}
+ | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+test.yaml:15:23: property "some-value" is not defined in object type {some_value: string} [expression]
+ |
+15 | - run: echo ${{ steps.my_action.outputs.some-value }}
+ | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+```
+
+The 'My action with output' action defines one output `some_value`. The property is typed at `steps.my_action.outputs` object
+so that actionlint can check incorrect property accesses like a typo in the output name.
+
+
+## Contextual typing for `matrix` object
+
+Example input:
+
+```yaml
+on: push
+jobs:
+ test:
+ strategy:
+ matrix:
+ os: [ubuntu-latest, windows-latest]
+ node: [14, 15]
+ package:
+ - name: 'foo'
+ optional: true
+ - name: 'bar'
+ optional: false
+ include:
+ - node: 15
+ npm: 7.5.4
+ runs-on: ${{ matrix.os }}
+ steps:
+ # Access undefined matrix value
+ - run: echo '${{ matrix.platform }}'
+ # Matrix value is strongly typed. Below line causes an error since matrix.package is {name: string, optional: bool}
+ - run: echo '${{ matrix.package.dev }}'
+ # OK
+ - run: |
+ echo 'os: ${{ matrix.os }}'
+ echo 'node version: ${{ matrix.node }}'
+ echo 'package: ${{ matrix.package.name }} (optional=${{ matrix.package.optional }})'
+ # Additional matrix values in 'include:' are supported
+ - run: echo 'npm version is specified'
+ if: ${{ contains(matrix.npm, '7.5') }}
+ test2:
+ runs-on: ubuntu-latest
+ steps:
+ # Matrix values in other job is not accessible
+ - run: echo '${{ matrix.os }}'
+```
+
+Output:
+
+```
+test.yaml:19:24: property "platform" is not defined in object type {os: string; node: number; package: {name: string; optional: bool}; npm: string} [expression]
+ |
+19 | - run: echo '${{ matrix.platform }}'
+ | ^~~~~~~~~~~~~~~
+test.yaml:21:24: property "dev" is not defined in object type {name: string; optional: bool} [expression]
+ |
+21 | - run: echo '${{ matrix.package.dev }}'
+ | ^~~~~~~~~~~~~~~~~~
+test.yaml:34:24: property "os" is not defined in object type {} [expression]
+ |
+34 | - run: echo '${{ matrix.os }}'
+ | ^~~~~~~~~
+```
+
+[Playground](https://rhysd.github.io/actionlint#eJyNUstuwyAQvOcr5lCJRIotpUpUCalfUvVAbJzQ2oBYSFql+fdCHOo8XLUntMsMOzuD0Rw20HbyZtbEJ4CX5NMJkHfCy81nXwGd8E595AowxPES1kH7ULQi8ebYK12bPZ3r1x+sNrWM6MVyjsVqaFtRvYuNHN4ECmjRRSxrjGEX/TjPemW0aDm8C3KMshbuN0ojWho4SldtqG/nnjQuVlcvaNtxPJWrcnlqu6CpMNGzh8PhbEhpCMfj2TFpKT9aJDCHrLYG7AJuozeNcV0ksb+gvT1lLXf36K8LnT0zBXKri92h0prYSUfqZo/TxRgjp4QRacn5SMI0W/08Asp3ETgb3Tm6nCVBEcjKSjVK1oMW1fTjK6O9UJqmWbTt5mAxIDbrU0j/7pFfh3X1Sf+fVG/gN6xO5N4=)
+
+Types of `matrix` context are contextually checked by the semantics checker. Type of matrix values in `matrix:` section
+is deduced from element values of its array. When the matrix value is an array of objects, objects' properties are checked
+strictly like `package.name` in above example.
+
+When a type of the array elements is not persistent, the type of the matrix value falls back to `any`.
+
+```yaml
+strategy:
+ matrix:
+ foo:
+ - 'string value'
+ - 42
+ - {aaa: true, bbb: null}
+ bar:
+ - [42]
+ - [true]
+ - [{aaa: true, bbb: null}]
+ - []
+steps:
+ # matrix.foo is any type value
+ - run: echo ${{ matrix.foo }}
+ # matrix.bar is array type value
+ - run: echo ${{ matrix.bar[0] }}
+ # ERROR: Array cannot be evaluated as string
+ - run: echo ${{ matrix.bar }}
+```
+
+
+## Contextual typing for `needs` object
+
+Example input:
+
+```yaml
+on: push
+jobs:
+ install:
+ outputs:
+ installed: '...'
+ runs-on: ubuntu-latest
+ steps:
+ - run: echo 'install something'
+ prepare:
+ outputs:
+ prepared: '...'
+ runs-on: ubuntu-latest
+ steps:
+ - run: echo 'parepare something'
+ # ERROR: Outputs in other job is not accessible
+ - run: echo '${{ needs.prepare.outputs.prepared }}'
+ build:
+ needs: [install, prepare]
+ outputs:
+ built: '...'
+ runs-on: ubuntu-latest
+ steps:
+ # OK: Accessing job results
+ - run: echo 'build something with ${{ needs.install.outputs.installed }} and ${{ needs.prepare.outputs.prepared }}'
+ # ERROR: Accessing undefined output causes an error
+ - run: echo '${{ needs.install.outputs.foo }}'
+ # ERROR: Accessing undefined job ID
+ - run: echo '${{ needs.some_job }}'
+ other:
+ runs-on: ubuntu-latest
+ steps:
+ # ERROR: Cannot access outputs across jobs
+ - run: echo '${{ needs.build.outputs.built }}'
+```
+
+Output:
+
+```
+test.yaml:16:24: property "prepare" is not defined in object type {} [expression]
+ |
+16 | - run: echo '${{ needs.prepare.outputs.prepared }}'
+ | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+test.yaml:26:24: property "foo" is not defined in object type {installed: string} [expression]
+ |
+26 | - run: echo '${{ needs.install.outputs.foo }}'
+ | ^~~~~~~~~~~~~~~~~~~~~~~~~
+test.yaml:28:24: property "some_job" is not defined in object type {install: {outputs: {installed: string}; result: string}; prepare: {outputs: {prepared: string}; result: string}} [expression]
+ |
+28 | - run: echo '${{ needs.some_job }}'
+ | ^~~~~~~~~~~~~~
+test.yaml:33:24: property "build" is not defined in object type {} [expression]
+ |
+33 | - run: echo '${{ needs.build.outputs.built }}'
+ | ^~~~~~~~~~~~~~~~~~~~~~~~~
+```
+
+[Playground](https://rhysd.github.io/actionlint#eJylUs1uwjAMvvMUPiD1QvMAeRU0oZZ4S6cujmpHHBDvTh1CKjY0EBwiJfHn78cJBQsxsV99U892BTAElm4cdQtASWISvhxqDZ2FxhjT5OspBW5ppkl9CpLasRNkySUWjLW5VaQF3HuCpjAB0w+KH8KXcsUJYzfhXelSe19ZWXTdSv+BrY9HCIiOTVE2xdD17OB00s4+DaO7KGW8hW0Jt7ma/rgXSPvk7TRZfYkCh0E8LN6Lk+q9PuBsHrrg4OmY/wzot8gn0eMmtbyb/1xBknic7OtzWIjzRKqXPGXVOAPMHOsV)
+
+Job dependencies can be defined at [`needs:`][needs-doc]. A job runs after all jobs defined in `needs:` are done.
+Outputs from the jobs can be accessed only from jobs following them via [`needs` context][needs-context-doc].
+
+actionlint defines a type of `needs` variable contextually by looking at each job's `outputs:` section and `needs:` section.
+
+
+## Strict type checks for comparison operators
+
+```yaml
+on:
+ workflow_call:
+ inputs:
+ timeout:
+ type: boolean
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - run: echo 'called!'
+ # ERROR: Comparing string to object is always evaluated to false
+ if: ${{ github.event == 'workflow_call' }}
+ - run: echo 'timeout is too long'
+ # ERROR: Comparing boolean value with `>` doesn't make sense
+ if: ${{ inputs.timeout > 60 }}
+```
+
+Output:
+
+```
+test.yaml:13:17: "object" value cannot be compared to "string" value with "==" operator [expression]
+ |
+13 | if: ${{ github.event == 'workflow_call' }}
+ | ^~~~~~~~~~~~
+test.yaml:16:17: "bool" value cannot be compared to "number" value with ">" operator [expression]
+ |
+16 | if: ${{ inputs.timeout > 60 }}
+ | ^~~~~~~~~~~~~~
+```
+
+Expressions in `${{ }}` placeholders support `==`, `!=`, `>`, `>=`, `<`, `<=` comparison operators. Arbitrary types of operands
+can be compared. When different type values are compared, they are implicitly converted to numbers before the comparison. Please
+see [the official document][operators-doc] to know the details of operators behavior.
+
+However, comparisons between some types are actually meaningless:
+
+- Objects and arrays are converted to `NaN`. Comparing an object or an array with other type is always evaluated to false.
+- Comparing booleans, null, objects, and arrays with `>`, `>=`, `<`, `<=` makes no sense.
+
+actionlint checks operands of comparison operators and reports errors in these cases.
+
+There are some additional surprising behaviors, but actioonlint allows them not to cause false positives as much as possible.
+
+- `0 == null`, `'0' == null`, `false == null` are true since they are implicitly converted to `0 == 0`
+- `'0' == false` and `0 == false` are true due to the same reason as above
+- Objects and arrays are only considered equal when they are the same instance
+
+
+## [shellcheck][] integration for `run:`
+
+Example input:
+
+```yaml
+on: push
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - run: echo $FOO
+ test-win:
+ runs-on: windows-latest
+ steps:
+ # Shell on Windows is PowerShell by default.
+ # shellcheck is not run in this case.
+ - run: echo $FOO
+ # This script is run with bash due to 'shell:' configuration
+ - run: echo $FOO
+ shell: bash
+```
+
+Output:
+
+```
+test.yaml:6:9: shellcheck reported issue in this script: SC2086:info:1:6: Double quote to prevent globbing and word splitting [shellcheck]
+ |
+6 | - run: echo $FOO
+ | ^~~~
+test.yaml:14:9: shellcheck reported issue in this script: SC2086:info:1:6: Double quote to prevent globbing and word splitting [shellcheck]
+ |
+14 | - run: echo $FOO
+ | ^~~~
+```
+
+[shellcheck][] is a famous linter for ShellScript. actionlint runs shellcheck for scripts at `run:` step in a workflow.
+For installing shellcheck, see [the official installation document][shellcheck-install].
+
+actionlint detects which shell is used to run the scripts following [the documentation][shell-doc]. On Linux or macOS the
+default shell is `bash`, and on Windows it is `pwsh`. Shell can be configured by `shell:` configuration at a workflow
+level or job level. Each step can configure shell to run scripts by `shell:`.
+
+In the above example output, `SC2086:info:1:6:` means that shellcheck reported SC2086 rule violation and the location is at
+line 1, column 6. Note that the location is relative to the script of the `run:` section.
+
+actionlint remembers the default shell and checks what OS the job runs on. Only when the shell is `bash` or `sh`, actionlint
+applies shellcheck to scripts.
+
+By default, actionlint checks if `shellcheck` command exists in your system and uses it when it is found. The `-shellcheck`
+option on running `actionlint` command specifies the executable path of shellcheck. Setting empty string by `shellcheck=`
+disables shellcheck integration explicitly.
+
+Since both `${{ }}` expression syntax and ShellScript's variable access `$FOO` use `$`, the remaining `${{ }}` confuses
+shellcheck. To avoid it, actionlint replaces `${{ }}` with underscores. For example `echo '${{ matrix.os }}'` is replaced
+with `echo '________________'`.
+
+Some shellcheck rules conflict with the `${{ }}` expression syntax. To avoid errors due to the syntax, [SC1091][], [SC2050][],
+[SC2194][], [SC2154][], [SC2157][] are disabled.
+
+When what shell is used cannot be determined statically, actionlint assumes `shell: bash` optimistically. For example,
+
+```yaml
+strategy:
+ matrix:
+ os: [ubuntu-latest, macos-latest, windows-latest]
+runs-on: ${{ matrix.os }}
+steps:
+ - name: Show file content
+ run: Get-Content -Path xxx\yyy.txt
+ if: ${{ matrix.os == 'windows-latest' }}
+```
+
+The 'Show file content' script is only run by `pwsh` due to `matrix.os == 'windows-latest'` guard. However actionlint does not
+know that. It checks the script with shellcheck and it'd probably cause a false-positive (due to file separator). This kind of
+false positives can be avoided by showing the shell name explicitly. It is also better in terms of maintenance of the workflow.
+
+```yaml
+- name: Show file content
+ run: Get-Content -Path xxx\yyy.txt
+ if: ${{ matrix.os == 'windows-latest' }}
+ shell: pwsh
+```
+
+When you want to control shellcheck behavior, [`SHELLCHECK_OPTS` environment variable][shellcheck-env-var] is useful.
+
+From command line:
+
+```sh
+# Enable some optional rules
+SHELLCHECK_OPTS='--enable=avoid-nullary-conditions' actionlint
+
+# Disable some rules
+SHELLCHECK_OPTS='--exclude=SC2129' actionlint
+```
+
+On GitHub Actions:
+
+```yaml
+- run: actionlint
+ env:
+ SHELLCHECK_OPTS: --exclude=SC2129
+```
+
+
+## [pyflakes][] integration for `run:`
+
+Example input:
+
+```yaml
+on: push
+jobs:
+ linux:
+ runs-on: ubuntu-latest
+ steps:
+ # Yay! No error
+ - run: print('${{ runner.os }}')
+ shell: python
+ # ERROR: Undefined variable
+ - run: print(hello)
+ shell: python
+ linux2:
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ # Run script with Python by default
+ shell: python
+ steps:
+ - run: |
+ import sys
+ for sys in ['system1', 'system2']:
+ print(sys)
+ - run: |
+ from time import sleep
+ print(100)
+```
+
+Output:
+
+```
+test.yaml:10:9: pyflakes reported issue in this script: 1:7: undefined name 'hello' [pyflakes]
+ |
+10 | - run: print(hello)
+ | ^~~~
+test.yaml:19:9: pyflakes reported issue in this script: 2:5: import 'sys' from line 1 shadowed by loop variable [pyflakes]
+ |
+19 | - run: |
+ | ^~~~
+test.yaml:23:9: pyflakes reported issue in this script: 1:1: 'time.sleep' imported but unused [pyflakes]
+ |
+23 | - run: |
+ | ^~~~
+```
+
+Python script can be written in `run:` when `shell: python` is configured.
+
+[pyflakes][] is a famous linter for Python. It is suitable for linting small code like scripts at `run:` since it focuses
+on finding mistakes (not a code style issue) and tries to make false positives as minimal as possible. Install pyflakes
+by `pip install pyflakes`.
+
+actionlint runs pyflakes for scripts at `run:` steps in a workflow and reports errors found by pyflakes. actionlint detects
+Python scripts in a workflow by checking `shell: python` at each step and `defaults:` configurations at workflows and jobs.
+
+By default, actionlint checks if `pyflakes` command exists in your system and uses it when found. The `-pyflakes` option
+of `actionlint` command allows to specify the executable path of pyflakes. Setting empty string by `pyflakes=` disables
+pyflakes integration explicitly.
+
+Since both `${{ }}` expression syntax is invalid as Python, remaining `${{ }}` might confuse pyflakes. To avoid it,
+actionlint replaces `${{ }}` with underscores. For example `print('${{ matrix.os }}')` is replaced with
+`print('________________')`.
+
+
+## Script injection by potentially untrusted inputs
+
+Example input:
+
+```yaml
+name: Test
+on: pull_request
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Print pull request title
+ # ERROR: Using the potentially untrusted input can cause script injection
+ run: echo '${{ github.event.pull_request.title }}'
+ - uses: actions/stale@v9
+ with:
+ repo-token: ${{ secrets.TOKEN }}
+ # This is OK because action input is not evaluated by shell
+ stale-pr-message: ${{ github.event.pull_request.title }} was closed
+ - uses: actions/github-script@v7
+ with:
+ # ERROR: Using the potentially untrusted input can cause script injection
+ script: console.log('${{ github.event.head_commit.author.name }}')
+ - name: Get comments
+ # ERROR: Accessing untrusted inputs via `.*` object filter; bodies of comment, review, and review_comment
+ run: echo '${{ toJSON(github.event.*.body) }}'
+```
+
+Output:
+
+```
+test.yaml:10:24: "github.event.pull_request.title" is potentially untrusted. avoid using it directly in inline scripts. instead, pass it through an environment variable. see https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions for more details [expression]
+ |
+10 | run: echo '${{ github.event.pull_request.title }}'
+ | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+test.yaml:19:36: "github.event.head_commit.author.name" is potentially untrusted. avoid using it directly in inline scripts. instead, pass it through an environment variable. see https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions for more details [expression]
+ |
+19 | script: console.log('${{ github.event.head_commit.author.name }}')
+ | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+test.yaml:22:31: object filter extracts potentially untrusted properties "github.event.comment.body", "github.event.discussion.body", "github.event.issue.body", "github.event.pull_request.body", "github.event.review.body", "github.event.review_comment.body". avoid using the value directly in inline scripts. instead, pass the value through an environment variable. see https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions for more details [expression]
+ |
+22 | run: echo '${{ toJSON(github.event.*.body) }}'
+ | ^~~~~~~~~~~~~~~~~~~~
+```
+
+[Playground](https://rhysd.github.io/actionlint#eJyFkUFLAzEQhe/9FXMQ2gqJRzGnXkRQaAV7L9ns0F3NZtbMpEVK/7vJbilVKZ5CMm++93gJtkMDa2SZUDDQJ+83ET9TeZi8U8VmAiD5Vk6AmAKrIkxVCpKUt2U2jFiw51EFoCAM4NfYBhmocKKCtOLxJBuABtA1BNObwwG2rTSp0rjDIPoyjB7W4Hicnh0SIxuwTloKfMdiPS52D2fyPqPM+ZadsCcl9IHZsFgxuojCer16eVxm8IV0YKk+qg6Z7RbHhf+zwd4yOE+M9ZWUI0Oxi20vi9391bSjwoDLW+RRe9rO/jbUoK03jrquFW2TNBR16b3UNP/1E08oUJR5ja+1L/T8tlrOfljc6orqr3lBfgPRHa9N)
+
+Since `${{ }}` placeholders are evaluated and replaced directly by GitHub Actions runtime, you need to use them carefully in
+inline scripts at `run:`. For example, if we have step as follows,
+
+```yaml
+- run: echo 'issue ${{github.event.issue.title}}'
+```
+
+an attacker can create a new issue with the title `'; malicious_command ...`, and the inline script will run
+`echo 'issue'; malicious_command ...` in your workflow. The remediation of such script injection is passing potentially untrusted
+inputs via environment variables. See [the official document][security-doc] for more details.
+
+```yaml
+- run: echo "issue ${TITLE}"
+ env:
+ TITLE: ${{github.event.issue.title}}
+```
+
+actionlint recognizes the following inputs as potentially untrusted and checks your inline scripts at `run:`. When they are used
+directly in a script, actionlint will report it as an error.
+
+- `github.event.issue.title`
+- `github.event.issue.body`
+- `github.event.pull_request.title`
+- `github.event.pull_request.body`
+- `github.event.comment.body`
+- `github.event.review.body`
+- `github.event.review_comment.body`
+- `github.event.pages.*.page_name`
+- `github.event.commits.*.message`
+- `github.event.head_commit.message`
+- `github.event.head_commit.author.email`
+- `github.event.head_commit.author.name`
+- `github.event.commits.*.author.email`
+- `github.event.commits.*.author.name`
+- `github.event.pull_request.head.ref`
+- `github.event.pull_request.head.label`
+- `github.event.pull_request.head.repo.default_branch`
+- `github.head_ref`
+
+Not only direct access to the untrusted properties, actionlint also detects those properties indirectly accessed via
+[object filter syntax][object-filter-syntax]. For example, `github.event.*.body` collects all `body` properties in child objects
+of `github.event` as array. Those properties include untrusted inputs like `github.event.comment.body`,
+`github.event.pull_request.body`, ...
+
+```sh
+# Echo list of github.event.comment.body, github.event.pull_request.body, ...
+echo '${{ toJSON(github.event.*.body) }}'
+```
+
+Instead, you should store the JSON string in an environment variable:
+
+```sh
+- run: echo "${BODIES}"
+ env:
+ BODIES: '${{ toJSON(github.event.*.body) }}'
+```
+
+At last, the popular action [actions/github-script][github-script] has the same issue in its `script` input. actionlint also
+checks the input.
+
+
+## Job dependencies validation
+
+Example input:
+
+```yaml
+on: push
+jobs:
+ prepare:
+ needs: [build]
+ runs-on: ubuntu-latest
+ steps:
+ - run: echo 'prepare'
+ install:
+ needs: [prepare]
+ runs-on: ubuntu-latest
+ steps:
+ - run: echo 'install'
+ build:
+ needs: [install]
+ runs-on: ubuntu-latest
+ steps:
+ - run: echo 'build'
+```
+
+Output:
+
+```
+test.yaml:8:3: cyclic dependencies in "needs" configurations of jobs are detected. detected cycle is "install" -> "prepare", "prepare" -> "build", "build" -> "install" [job-needs]
+ |
+8 | install:
+ | ^~~~~~~~
+```
+
+[Playground](https://rhysd.github.io/actionlint#eJyljjEOxCAMBPu8YjsqPsBXTikgsZREyCBs/z+Bo0mdzvJ4Z104oJocy1WShAWojWps1EeAiXYJ+CU7876OVTMWX56UJWM1n6OS6ECiVOUfBHy/DKDtKHBT6h52smjM+e2f/EPD1PaG8ezbP+kH/5C6G78nW+Q=)
+
+Job dependencies can be defined at [`needs:`][needs-doc]. If cyclic dependencies exist, jobs never start to run. actionlint
+detects cyclic dependencies in `needs:` sections of jobs and reports it as an error.
+
+actionlint also detects undefined jobs and duplicate jobs in `needs:` section.
+
+Example input:
+
+```yaml
+on: push
+jobs:
+ foo:
+ needs: [bar, BAR]
+ runs-on: ubuntu-latest
+ steps:
+ - run: echo 'hi'
+ bar:
+ needs: [unknown]
+ runs-on: ubuntu-latest
+ steps:
+ - run: echo 'hi'
+```
+
+Output:
+
+```
+test.yaml:4:18: job ID "BAR" duplicates in "needs" section. note that job ID is case insensitive [job-needs]
+ |
+4 | needs: [bar, BAR]
+ | ^~~~
+test.yaml:8:3: job "bar" needs job "unknown" which does not exist in this workflow [job-needs]
+ |
+8 | bar:
+ | ^~~~
+```
+
+[Playground](https://rhysd.github.io/actionlint#eJyljD0OgiEQRHtOMR2NXIDO7wi2xgJ0v+BPdgnLxusLWFlbTfJm5glHVNPiHpI1OmAXmQEw0U0jzjm1A7bj6bJoM9Yg42TZuFt4pU7aV6Wdqn6/QJjLCLoWgS93P/AQ/ZqNnyxv/k/8AXoNOHs=)
+
+
+## Matrix values
+
+Example input:
+
+```yaml
+on: push
+jobs:
+ test:
+ strategy:
+ matrix:
+ node: [10, 12, 14, 14]
+ os: [ubuntu-latest, macos-latest]
+ exclude:
+ - node: 13
+ os: ubuntu-latest
+ - node: 10
+ platform: ubuntu-latest
+ runs-on: ${{ matrix.os }}
+ steps:
+ - run: echo ...
+```
+
+Output:
+
+```
+test.yaml:6:28: duplicate value "14" is found in matrix "node". the same value is at line:6,col:24 [matrix]
+ |
+6 | node: [10, 12, 14, 14]
+ | ^~~
+test.yaml:9:19: value "13" in "exclude" does not match in matrix "node" combinations. possible values are "10", "12", "14", "14" [matrix]
+ |
+9 | - node: 13
+ | ^~
+test.yaml:12:13: "platform" in "exclude" section does not exist in matrix. available matrix configurations are "node", "os" [matrix]
+ |
+12 | platform: ubuntu-latest
+ | ^~~~~~~~~
+```
+
+[Playground](https://rhysd.github.io/actionlint#eJxtkMEOgjAQRO9+xRw80gbUU3/FeACsooEu6bYJhvDvtqGgJBw2zezOvOyWjELvuTm8qWJ1AJxmF1+AnS2dfn5mBXSls69hUYChu1a4FnmG4hTqEuu2jonD0FfeOC/aMmKzgKiJk/o59VC3PrDWBiASvTj/NWfmBrkXyTeRPhgfZLu9oPWGBYUfOI5jOk8SY5rS/brnZSkRzQq6bghSyi9UwlNB)
+
+[`matrix:`][matrix-doc] defines combinations of multiple values. Nested `include:` and `exclude:` can add/remove specific
+combination of matrix values. actionlint checks
+
+- values in `exclude:` appear in `matrix:` or `include:`
+- duplicate variations of matrix values
+
+
+## Webhook events validation
+
+Example input:
+
+```yaml
+on:
+ push:
+ # ERROR: Incorrect filter. 'branches' is correct
+ branch: foo
+ # ERROR: Both 'paths' and 'paths-ignore' filters cannot be used for the same event
+ paths: path/to/foo
+ paths-ignore: path/to/foo
+ issues:
+ # ERROR: Incorrect type. 'opened' is correct
+ types: created
+ release:
+ # ERROR: 'tags' filter is not available for 'release' event
+ tags: v*.*.*
+ # ERROR: Unknown event name
+ pullreq:
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - run: echo ...
+```
+
+Output:
+
+```
+test.yaml:4:5: unexpected key "branch" for "push" section. expected one of "branches", "branches-ignore", "paths", "paths-ignore", "tags", "tags-ignore", "types", "workflows" [syntax-check]
+ |
+4 | branch: foo
+ | ^~~~~~~
+test.yaml:7:5: both "paths" and "paths-ignore" filters cannot be used for the same event "push". note: use '!' to negate patterns [events]
+ |
+7 | paths-ignore: path/to/foo
+ | ^~~~~~~~~~~~~
+test.yaml:10:12: invalid activity type "created" for "issues" Webhook event. available types are "assigned", "closed", "deleted", "demilestoned", "edited", "labeled", "locked", "milestoned", "opened", "pinned", "reopened", "transferred", "unassigned", "unlabeled", "unlocked", "unpinned" [events]
+ |
+10 | types: created
+ | ^~~~~~~
+test.yaml:13:5: "tags" filter is not available for release event. it is only for push event [events]
+ |
+13 | tags: v*.*.*
+ | ^~~~~
+test.yaml:15:3: unknown Webhook event "pullreq". see https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#webhook-events for list of all Webhook event names [events]
+ |
+15 | pullreq:
+ | ^~~~~~~~
+```
+
+[Playground](https://rhysd.github.io/actionlint#eJxdjkEOAyEIRfeegnUTnb23UUvHaYxYwCa9fdWZTRsWH3g/H6h6A9C65KkAkUNN2cODaM0taBa/ZFPaftb22Csx/tNDpKOccfppo4XEGBTvY8VYMAheNOwDvm9u1PqiFMaXN+ZJcQUoip5W7lUsVQ899qrdljDZQqLYrnMAdjo9YMoEzrkvdVRCRg==)
+
+At `on:`, Webhook events can be specified to trigger the workflow. [Webhook event documentation][webhook-doc] defines
+which Webhook events are available and what types can be specified at `types:` for each event.
+
+actionlint validates the Webhook configurations:
+
+- Webhook event name
+- types for Webhook event
+- filter names
+- filter usages
+ - `paths` and `paths-ignore`, `branches` and `branches-ignore`, `tags` and `tags-ignore` are exclusive. They can not
+ be used for the same event.
+ - Some filters are only available for specific events as explained in [the official document][specific-paths-doc]
+ (see the following table).
+
+| Filter name | Events where the filter is available |
+|-------------------|-----------------------------------------------|
+| `paths` | `push`, `pull_request`, `pull_request_target` |
+| `paths-ignore` | `push`, `pull_request`, `pull_request_target` |
+| `branches` | `push`, `pull_request`, `pull_request_target` |
+| `branches-ignore` | `push`, `pull_request`, `pull_request_target` |
+| `tags` | `push` |
+| `tags-ignore` | `push` |
+
+The table of available Webhooks and their types are defined in [`all_webhooks.go`](../all_webhooks.go). It is generated
+by [a script][generate-webhook-events] and kept to the latest by CI workflow triggered weekly.
+
+
+## Workflow dispatch event validation
+
+Example input:
+
+```yaml
+on:
+ workflow_dispatch:
+ inputs:
+ # Unknown input type
+ id:
+ type: text
+ # ERROR: No options for 'choice' input type
+ kind:
+ type: choice
+ name:
+ type: choice
+ options:
+ - Tama
+ - Mike
+ # ERROR: Default value is not in options
+ default: Chobi
+ message:
+ type: string
+ verbose:
+ type: boolean
+ # ERROR: Boolean value must be 'true' or 'false'
+ default: yes
+ age:
+ type: number
+ # ERROR: Number value must be parsed as a float number
+ default: teen
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ # ERROR: Undefined input
+ - run: echo "${{ inputs.massage }}"
+ # ERROR: Bool value is not available for object key
+ - run: echo "${{ env[inputs.verbose] }}"
+ # ERROR: Number value is not available for object key
+ - run: echo "${{ env[inputs.age] }}"
+ # ERROR: `github.event.inputs` is also not defined
+ - run: echo "${{ github.event.inputs.massage }}"
+```
+
+Output:
+
+```
+test.yaml:6:15: input type of workflow_dispatch event must be one of "string", "number", "boolean", "choice", "environment" but got "text" [syntax-check]
+ |
+6 | type: text
+ | ^~~~
+test.yaml:8:7: input type of "kind" is "choice" but "options" is not set [events]
+ |
+8 | kind:
+ | ^~~~~
+test.yaml:16:18: default value "Chobi" of "name" input is not included in its options "\"Tama\", \"Mike\"" [events]
+ |
+16 | default: Chobi
+ | ^~~~~
+test.yaml:22:18: type of "verbose" input is "boolean". its default value "yes" must be "true" or "false" [events]
+ |
+22 | default: yes
+ | ^~~
+test.yaml:26:18: type of "age" input is "number" but its default value "teen" cannot be parsed as a float number: strconv.ParseFloat: parsing "teen": invalid syntax [events]
+ |
+26 | default: teen
+ | ^~~~
+test.yaml:33:24: property "massage" is not defined in object type {age: number; id: any; kind: string; message: string; name: string; verbose: bool} [expression]
+ |
+33 | - run: echo "${{ inputs.massage }}"
+ | ^~~~~~~~~~~~~~
+test.yaml:35:28: property access of object must be type of string but got "bool" [expression]
+ |
+35 | - run: echo "${{ env[inputs.verbose] }}"
+ | ^~~~~~~~~~~~~~~
+test.yaml:37:28: property access of object must be type of string but got "number" [expression]
+ |
+37 | - run: echo "${{ env[inputs.age] }}"
+ | ^~~~~~~~~~~
+test.yaml:39:24: property "massage" is not defined in object type {age: string; id: string; kind: string; message: string; name: string; verbose: string} [expression]
+ |
+39 | - run: echo "${{ github.event.inputs.massage }}"
+ | ^~~~~~~~~~~~~~~~~~~~~~~~~~~
+```
+
+[Playground](https://rhysd.github.io/actionlint#eJyNkcFugzAMQO98hVX1Ch+Q68697TZNUwIuZICNYoeuqvrvA5qtmrJVuznPz07sMJkC4MShPw58emu8TFbrboUAnqaocouXU/MVAeh5QgOKH5pQ7ylL1x37GhMkO+JDAYAn9UxytwBKeLaj/QEOvr+XNHi0cVADTx07n/CIIrbNbhMNntoEZwyOJXMc84CW8v5nlAR/6UxxdBjyIkWkonhnt82kKHqrDJGkZDIQXSSN5WDX3JYSxel7A+VqGsBlT7DbXy7pQ6rRbgPC9br7y0SaX5KdRn39p740fqi2XrvoKpyRtMof9AmbKqYK)
+
+[`workflow_dispatch`][workflow-dispatch-event] is an event to trigger a workflow manually. The event can have parameters called
+'inputs'. Each input has its name, description, default value, and [input type][workflow-dispatch-input-type-announce].
+
+actionlint checks several mistakes around `workflow_dispatch` configuration.
+
+- Input type must be one of 'choice', 'string', 'number', 'boolean', 'environment'
+- `options:` must be set for 'choice' input type
+- The default value of 'choice' input must be included in options
+- The default value of 'boolean' input must be `true` or `false`
+- The default value of 'number' input must be parsed as a float number
+
+In addition, `github.event.inputs` and `inputs` objects are typed based on the input definitions. Properties not defined in
+`inputs:` will cause a type error thanks to a type checker.
+
+For example,
+
+```yaml
+inputs:
+ string_input:
+ type: string
+ choice_input:
+ type: choice
+ options: ['hello']
+ bool_input:
+ type: boolean
+ num_input:
+ type: number
+ env_input:
+ type: environment
+ no_type_input:
+```
+
+`inputs` is typed as follows from these definitions:
+
+```
+{
+ "string_input": string;
+ "choice_input": string;
+ "bool_input": bool;
+ "num_input": number;
+ "env_input": string;
+ "no_type_input": any;
+}
+```
+
+`github.event.inputs` is typed as follows since all properties of it are strings unlike `inputs`:
+
+```
+{
+ "string_input": string;
+ "choice_input": string;
+ "bool_input": string;
+ "num_input": string;
+ "env_input": string;
+ "no_type_input": string;
+}
+```
+
+
+## Glob filter pattern syntax validation
+
+Example input:
+
+```yaml
+on:
+ push:
+ branches:
+ # ^ is not available for branch name. This kind of mistake is usually caused by misunderstanding
+ # that regular expression is available here
+ - '^foo-'
+ tags:
+ # Invalid syntax. + cannot follow special character *
+ - 'v*+'
+ # Invalid character range 9-1
+ - 'v[9-1]'
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - run: echo ...
+```
+
+Output:
+
+```
+test.yaml:6:10: character '^' is invalid for branch and tag names. ref name cannot contain spaces, ~, ^, :, [, ?, *. see `man git-check-ref-format` for more details. note that regular expression is unavailable. note: filter pattern syntax is explained at https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#filter-pattern-cheat-sheet [glob]
+ |
+6 | - '^foo-'
+ | ^~~~~~
+test.yaml:9:12: invalid glob pattern. unexpected character '+' while checking special character + (one or more). the preceding character must not be special character. note: filter pattern syntax is explained at https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#filter-pattern-cheat-sheet [glob]
+ |
+9 | - 'v*+'
+ | ^~
+test.yaml:11:14: invalid glob pattern. unexpected character '1' while checking character range in []. start of range '9' (57) is larger than end of range '1' (49). note: filter pattern syntax is explained at https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#filter-pattern-cheat-sheet [glob]
+ |
+11 | - 'v[9-1]'
+ | ^~~
+```
+
+[Playground](https://rhysd.github.io/actionlint#eJxNjEEKAjEQBO95Rd8CygQ8mq+IQrJEg8jMsjPj+3Wzl5yaoooWzgFYXfu+QN0KL73pQQAhPp4iFAdbec3mezrHiW5XutxjCG+po7KmdtSbs5Jwhldnc/qU3Q2l1tbp819mtKULUko/10snvA==)
+
+For filtering branches, tags and paths in Webhook events, [glob syntax][filter-pattern-doc] is available.
+actionlint validates glob patterns `branches:`, `branches-ignore:`, `tags:`, `tags-ignore:`, `paths:`, `paths-ignore:` in a
+workflow. It checks:
+
+- syntax errors like missing closing brackets for character range `[..]`
+- invalid usage like `?` following `*`, invalid character range `[9-1]`, ...
+- invalid character usage for Git ref names (branch name, tag name)
+ - ref name cannot start/end with `/`
+ - ref name cannot contain `[`, `:`, `\`, ...
+
+Most common mistake I have ever seen here is a misunderstanding that regular expression is available for filtering.
+This rule can catch the mistake so that users can notice their mistakes.
+
+
+## CRON syntax check at `schedule:`
+
+Example input:
+
+```yaml
+on:
+ schedule:
+ # ERROR: Cron syntax is not correct
+ - cron: '0 */3 * *'
+ # ERROR: Interval of scheduled job is too small (job runs too frequently)
+ - cron: '* */3 * * *'
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - run: echo ...
+```
+
+Output:
+
+```
+test.yaml:4:13: invalid CRON format "0 */3 * *" in schedule event: expected exactly 5 fields, found 4: [0 */3 * *] [events]
+ |
+4 | - cron: '0 */3 * *'
+ | ^~
+test.yaml:6:13: scheduled job runs too frequently. it runs once per 60 seconds. the shortest interval is once every 5 minutes [events]
+ |
+6 | - cron: '* */3 * * *'
+ | ^~
+```
+
+[Playground](https://rhysd.github.io/actionlint#eJxVjEEKgDAMBO99xd6EQKvgrb/RGhAprTTN/zWKB2/LzuzWEh0gaedNM1sGPFKrJWKYQOMMAg3/nr7eiDvqKjbsLP09aFrEm6mrlq4+L8YeJJ1PeS07vM0ITntFCOEChKgjxA==)
+
+To trigger a workflow in specific interval, [scheduled event][schedule-event-doc] can be defined in [POSIX CRON syntax][cron-syntax].
+
+actionlint checks the CRON syntax and frequency of running a job. [The official document][schedule-event-doc] says:
+
+> The shortest interval you can run scheduled workflows is once every 5 minutes.
+
+When the job is run more frequently than once every 5 minutes, actionlint reports it as an error.
+
+
+## Runner labels
+
+Example input:
+
+```yaml
+on: push
+jobs:
+ test:
+ strategy:
+ matrix:
+ runner:
+ # OK
+ - macos-latest
+ # ERROR: Unknown runner
+ - linux-latest
+ # OK: Preset labels for self-hosted runner
+ - [self-hosted, linux, x64]
+ # OK: Single preset label for self-hosted runner
+ - arm64
+ # ERROR: Unknown label "gpu". Custom label must be defined in actionlint.yaml config file
+ - gpu
+ runs-on: ${{ matrix.runner }}
+ steps:
+ - run: echo ...
+
+ test2:
+ # ERROR: Too old macOS worker
+ runs-on: macos-10.13
+ steps:
+ - run: echo ...
+```
+
+Output:
+
+```
+test.yaml:10:13: label "linux-latest" is unknown. available labels are "windows-latest", "windows-2022", "windows-2019", "ubuntu-latest", ... [runner-label]
+ |
+10 | - linux-latest
+ | ^~~~~~~~~~~~
+test.yaml:16:13: label "gpu" is unknown. available labels are "windows-latest", "windows-2022", "windows-2019", "ubuntu-latest", ... [runner-label]
+ |
+16 | - gpu
+ | ^~~
+test.yaml:23:14: label "macos-10.13" is unknown. available labels are "windows-latest", "windows-2022", "windows-2019", "ubuntu-latest", ... [runner-label]
+ |
+23 | runs-on: macos-10.13
+ | ^~~~~~~~~~~
+```
+
+[Playground](https://rhysd.github.io/actionlint#eJyFj8EKgzAQRO/5ijn0aEJtxUN+pfSQ2lQtNpFsAhbx36skUgKFnpbZnR3mWSMxBurY095IMsBr8tsEyDvldfuOCngp7/ppV4ALxmj31QBfPY0lPqgtJTsMvQnTr8OF9PDgnSWv70W0FZjq6pq5lHvVVbZpx8BSC+J2pTjMc6ooYjMsS+LQI+01+fYgoZvOQgjBEvFJ5mGRozyK8vw34wNI+VUQ)
+
+GitHub Actions provides two kinds of job runners, [GitHub-hosted runner][gh-hosted-runner] and [self-hosted runner][self-hosted-runner].
+Each runner has one or more labels. GitHub Actions runtime finds a proper runner based on label(s) specified at `runs-on:`
+to run the job. So specifying proper labels at `runs-on:` is important.
+
+actionlint checks proper label is used at `runs-on:` configuration. Even if an expression is used in the section like
+`runs-on: ${{ matrix.foo }}`, actionlint parses the expression and resolves the possible values, then validates the values.
+
+When you define some custom labels for your self-hosted runner, actionlint does not know the labels. Please set the label
+names in [`actionlint.yaml` configuration file](config.md) to let actionlint know them.
+
+In addition to checking label values, actionlint checks combinations of labels. `runs-on:` section can be an array that contains
+multiple labels. In this case, a runner which has all the labels will be selected. However, those labels combinations can have
+conflicts.
+
+Example input:
+
+```yaml
+on: push
+jobs:
+ test:
+ runs-on: [ubuntu-latest, windows-latest]
+ steps:
+ - run: echo ...
+```
+
+Output:
+
+```
+test.yaml:4:30: label "windows-latest" conflicts with label "ubuntu-latest" defined at line:4,col:15. note: to run your job on each worker, use matrix [runner-label]
+ |
+4 | runs-on: [ubuntu-latest, windows-latest]
+ | ^~~~~~~~~~~~~~~
+```
+
+[Playground](https://rhysd.github.io/actionlint#eJwti0EOgCAMBO+8Yh8gPICvGA+iJEhMS2wbvq+op81mZpgimklxlZNEB2gWHQtcRuL54bMlIzV/rgNO6Aft3OX/yyuL5iZfB/jRRuStMEIIN17iHww=)
+
+In most cases, this is a misunderstanding that a matrix combination can be specified at `runs-on:` directly. It should use
+`matrix:` and expand it with `${{ }}` at `runs-on:` to run the workflow on multiple runners.
+
+
+## Action format in `uses:`
+
+Example input:
+
+```yaml
+on: push
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ # ERROR: ref is missing
+ - uses: actions/checkout
+ # ERROR: owner name is missing
+ - uses: checkout@v2
+ # ERROR: tag is empty
+ - uses: 'docker://image:'
+ # ERROR: local action must start with './'
+ - uses: .github/my-actions/do-something
+```
+
+Output:
+
+```
+test.yaml:7:15: specifying action "actions/checkout" in invalid format because ref is missing. available formats are "{owner}/{repo}@{ref}" or "{owner}/{repo}/{path}@{ref}" [action]
+ |
+7 | - uses: actions/checkout
+ | ^~~~~~~~~~~~~~~~
+test.yaml:9:15: specifying action "checkout@v2" in invalid format because owner is missing. available formats are "{owner}/{repo}@{ref}" or "{owner}/{repo}/{path}@{ref}" [action]
+ |
+9 | - uses: checkout@v2
+ | ^~~~~~~~~~~
+test.yaml:11:15: tag of Docker action should not be empty: "docker://image" [action]
+ |
+11 | - uses: 'docker://image:'
+ | ^~~~~~~~~~~~~~~~~
+test.yaml:13:15: specifying action ".github/my-actions/do-something" in invalid format because ref is missing. available formats are "{owner}/{repo}@{ref}" or "{owner}/{repo}/{path}@{ref}" [action]
+ |
+13 | - uses: .github/my-actions/do-something
+ | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+```
+
+[Playground](https://rhysd.github.io/actionlint#eJxdzTEOgzAMBdCdU3hjSi119NSrJKlFUkqMsF2pty8UsWTy139fsjSC1bUML0lKA4Cx2nEBNm8aZHdP3szDOx72JzVe9VwBBHBlJYjZqjTFXDjP4tbxVT8+907Gp+SZN0KsS5yYxs5vOFUrnvD6sHzDGX98DjoH)
+
+Action needs to be specified in a format defined in [the document][action-uses-doc]. There are 3 types of actions:
+
+- action hosted on GitHub: `owner/repo/path@ref`
+- local action: `./path/to/my-action`
+- Docker action: `docker://image:tag`
+
+actionlint checks values at `uses:` sections follow one of these formats.
+
+Note that actionlint does not report any error when a directory for a local action does not exist in the repository because it is
+a common case where the action is managed in a separate repository and the action directory is cloned at running the workflow.
+(See [#25][issue-25] and [#40][issue-40] for more details).
+
+
+## Local action inputs validation at `with:`
+
+My action definition at `.github/actions/my-action/action.yaml`:
+
+```yaml
+name: 'My action'
+author: 'rhysd '
+description: 'my action'
+
+inputs:
+ name:
+ description: your name
+ default: anonymous
+ message:
+ description: message to this action
+ required: true
+ addition:
+ description: additional information
+ required: false
+
+runs:
+ using: 'node20'
+ main: 'index.js'
+```
+
+Example input:
+
+```yaml
+on: push
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ # missing required input "message"
+ - uses: ./.github/actions/my-action
+ # unexpected input "additions"
+ - uses: ./.github/actions/my-action
+ with:
+ name: rhysd
+ message: hello
+ additions: foo, bar
+```
+
+Output:
+
+```
+test.yaml:7:15: missing input "message" which is required by action "My action" defined at "./.github/actions/my-action". all required inputs are "message" [action]
+ |
+7 | - uses: ./.github/actions/my-action
+ | ^~~~~~~~~~~~~~~~~~~~~~~~~~~
+test.yaml:13:11: input "additions" is not defined in action "My action" defined at "./.github/actions/my-action". available inputs are "addition", "message", "name" [action]
+ |
+13 | additions: foo, bar
+ | ^~~~~~~~~~
+```
+
+When a local action is run in `uses:` of `step:`, actionlint reads `action.yml` file in the local action directory and
+validates inputs at `with:` in the workflow are correct. Missing required inputs and unexpected inputs can be detected.
+
+
+## Popular action inputs validation at `with:`
+
+Example input:
+
+```yaml
+on: push
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/cache@v3
+ with:
+ keys: |
+ ${{ hashFiles('**/*.lock') }}
+ ${{ hashFiles('**/*.cache') }}
+ path: ./packages
+ - run: make
+```
+
+Output:
+
+```
+test.yaml:7:15: missing input "key" which is required by action "actions/cache@v3". all required inputs are "key", "path" [action]
+ |
+7 | - uses: actions/cache@v3
+ | ^~~~~~~~~~~~~~~~
+test.yaml:9:11: input "keys" is not defined in action "actions/cache@v3". available inputs are "key", "path", "restore-keys", "upload-chunk-size" [action]
+ |
+9 | keys: |
+ | ^~~~~
+```
+
+[Playground](https://rhysd.github.io/actionlint#eJyFj0EKwjAQRfc9xV8I1UJbcJmVK+8xDYOpqUlwEkVq725apYgbV8PMe/Dne6cQkpiiOPtOVAFEljhP4Jqc1D4LqUsupnqgmS1IIgd5W0CNJCwKpGPvnbSatOHDbf/BwL2PRq0bYPmR9efXBdiMIwyJOfYDy7asqrZqBq9tucM0/TWXyF81UI5F0wbSlk4s67u5mMKFLL8A+h9EEw==)
+
+actionlint checks inputs of many popular actions such as `actions/checkout@v3`. It checks
+
+- some input is required by the action but it is not set at `with:`
+- input set at `with:` is not defined in the action (this commonly occurs by a typo)
+
+this is done by checking `with:` section items with a small database collected at building `actionlint` binary. actionlint
+can check popular actions without fetching any `action.yml` of the actions from the remote so that it can run efficiently.
+
+Note that it only supports the case of specifying major versions like `actions/checkout@v3`. Fixing version of action like
+`actions/checkout@v3.0.2` and using the HEAD of action like `actions/checkout@main` are not supported for now.
+
+So far, actionlint supports more than 100 popular actions The data set is embedded at [`popular_actions.go`](../popular_actions.go)
+and were automatically collected by [a script][generate-popular-actions]. If you want more checks for other actions, please
+make a request [as an issue][issue-form].
+
+
+## Outdated popular actions detection at `with:`
+
+Example input:
+
+```yaml
+on: push
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ # ERROR: actions/checkout@v2 is using the outdated runner 'node12'
+ - uses: actions/checkout@v2
+```
+
+Output:
+
+```
+test.yaml:8:15: the runner of "actions/checkout@v2" action is too old to run on GitHub Actions. update the action's version to fix this issue [action]
+ |
+8 | - uses: actions/checkout@v2
+ | ^~~~~~~~~~~~~~~~~~~
+```
+
+[Playground](https://rhysd.github.io/actionlint#eJwlyjkOwCAMRNGeU8wFUKSUVLkKICSyyEYZO+fPVv3ifZWE4ewhbFqYAmCN9hY4XRj1Gby4mMcjv/YRrQ3+FxDhbEzI1VYVTrW3uqvbcs03dIgdzQ==)
+
+In addition to the checks for inputs of actions described in [the previous section](#check-popular-action-inputs), actionlint
+reports an error when a popular action is 'outdated'. An action is outdated when the runner used by the action is no longer
+supported by GitHub Actions runtime. For example, `node12` is no longer available so any actions can use `node12` runner.
+
+Note that this check doesn't report that the action version is up-to-date. For example, even if you use `actions/checkout@v3` and
+newer version `actions/checkout@v4` is available, actionlint reports no error as long as `actions/checkout@v3` is not outdated.
+If you want to keep actions used by your workflows up-to-date, consider to use [Dependabot][dependabot-doc].
+
+
+## Shell name validation at `shell:`
+
+Example input:
+
+```yaml
+on: push
+jobs:
+ linux:
+ runs-on: ubuntu-latest
+ steps:
+ - run: echo 'hello'
+ # ERROR: Unavailable shell
+ shell: dash
+ - run: echo 'hello'
+ # ERROR: 'powershell' is only available on Windows
+ shell: powershell
+ mac:
+ runs-on: macos-latest
+ defaults:
+ run:
+ # ERROR: default config is also checked. fish is not supported
+ shell: fish
+ steps:
+ - run: echo 'hello'
+ # OK: Custom shell
+ shell: 'perl {0}'
+ windows:
+ runs-on: windows-latest
+ steps:
+ - run: echo 'hello'
+ # ERROR: 'sh' is only available on Windows
+ shell: sh
+ - run: echo 'hello'
+ # OK: 'powershell' is only available on Windows
+ shell: powershell
+```
+
+Output:
+
+```
+test.yaml:8:16: shell name "dash" is invalid. available names are "bash", "pwsh", "python", "sh" [shell-name]
+ |
+8 | shell: dash
+ | ^~~~
+test.yaml:11:16: shell name "powershell" is invalid on macOS or Linux. available names are "bash", "pwsh", "python", "sh" [shell-name]
+ |
+11 | shell: powershell
+ | ^~~~~~~~~~
+test.yaml:14:16: shell name "powershell" is invalid on macOS or Linux. available names are "bash", "pwsh", "python", "sh" [shell-name]
+ |
+14 | shell: powershell
+ | ^~~~~~~~~~
+test.yaml:20:16: shell name "fish" is invalid. available names are "bash", "pwsh", "python", "sh" [shell-name]
+ |
+20 | shell: fish
+ | ^~~~
+test.yaml:30:16: shell name "sh" is invalid on Windows. available names are "bash", "pwsh", "python", "cmd", "powershell" [shell-name]
+ |
+30 | shell: sh
+ | ^~
+```
+
+[Playground](https://rhysd.github.io/actionlint#eJylkLsKwzAMRfd8hbZMhs7+GydWcIpqGcsihdJ/r52GUjz1sUm6R48rjhaSShjOPIkdAGiNem0BQNYohiugk8aihlxBKbskBZM8KQDTSAs4B4YxIBGPh1LBllvwrq74mE68Yd7jH3subu4s1ArLuwOPi1MqLxNtQT9zWY+rv7U7JswEt9O9KdsaPW/SHXRU/3mqhAdbk36k)
+
+Available shells for runners are defined in [the documentation][shell-doc]. actionlint checks shell names at `shell:`
+configuration are properly using the available shells.
+
+
+## Job ID and step ID uniqueness
+
+Example input:
+
+```yaml
+on: push
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - run: echo 'hello'
+ id: step_id
+ - run: echo 'bye'
+ # ERROR: Duplicate step ID
+ id: STEP_ID
+ # ERROR: Duplicate job ID
+ TEST:
+ runs-on: ubuntu-latest
+ steps:
+ - run: echo 'hello'
+ # OK. Step ID uniqueness is job-local
+ id: step_id
+```
+
+Output:
+
+```
+test.yaml:10:13: step ID "STEP_ID" duplicates. previously defined at line:7,col:13. step ID must be unique within a job. note that step ID is case insensitive [id]
+ |
+10 | id: STEP_ID
+ | ^~~~~~~
+test.yaml:12:3: key "TEST" is duplicated in "jobs" section. previously defined at line:3,col:3. note that key names are case insensitive [syntax-check]
+ |
+12 | TEST:
+ | ^~~~~
+```
+
+[Playground](https://rhysd.github.io/actionlint#eJzLz7NSKCgtzuDKyk8qtuJSUChJLS4B0QoKRaV5xbr5QPnSpNK8klLdnESQHFiquCS1oBiiSkFBF6TSSiE1OSNfQT0jNScnXx0qo6CQmWIFVhyfmYJNdVJlKqra4BDXgHhPF6BYiGtwCE3cAQCKgUNq)
+
+Job IDs and step IDs in each jobs must be unique. IDs are compared in case insensitive. actionlint checks all job IDs
+and step IDs, and reports errors when some IDs duplicate.
+
+
+## Hardcoded credentials
+
+Example input:
+
+```yaml
+on: push
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ container:
+ image: 'example.com/owner/image'
+ credentials:
+ username: user
+ # ERROR: Hardcoded password
+ password: pass
+ services:
+ redis:
+ image: redis
+ credentials:
+ username: user
+ # ERROR: Hardcoded password
+ password: pass
+ steps:
+ - run: echo 'hello'
+```
+
+Output:
+
+```
+test.yaml:10:19: "password" section in "container" section should be specified via secrets. do not put password value directly [credentials]
+ |
+10 | password: pass
+ | ^~~~
+test.yaml:17:21: "password" section in "redis" service should be specified via secrets. do not put password value directly [credentials]
+ |
+17 | password: pass
+ | ^~~~
+```
+
+[Playground](https://rhysd.github.io/actionlint#eJx1kLEOwyAMRPd8hTemNDt/4xCroQKMMDT9/AJNUYd0wrp357PgoCEW2acHr6IngEyS2wuQSpCZKy9rCbnMDhvryHDIaAOljxPAeryTBkUv9NHRzbBf+KiGpRN12kyijUK26OSbBChCKaCv8TYNOaLIwWnTfepyxU9raGTrNvuz6Dyiq0O8rPxbel2bKY7w3P5FA5mdQe3kHKs3Uktdww==)
+
+[Credentials for container][credentials-doc] can be put in `container:` configuration. Password should be put in secrets
+and the value should be expanded with `${{ }}` syntax at `password:`. actionlint checks hardcoded credentials, and reports
+them as an error.
+
+
+## Environment variable names
+
+Example input:
+
+```yaml
+on: push
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ env:
+ FOO=BAR: foo
+ FOO BAR: foo
+ steps:
+ - run: echo 'hello'
+```
+
+Output:
+
+```
+test.yaml:6:7: environment variable name "FOO=BAR" is invalid. '&', '=' and spaces should not be contained [env-var]
+ |
+6 | FOO=BAR: foo
+ | ^~~~~~~~
+test.yaml:7:7: environment variable name "FOO BAR" is invalid. '&', '=' and spaces should not be contained [env-var]
+ |
+7 | FOO BAR: foo
+ | ^~~
+```
+
+[Playground](https://rhysd.github.io/actionlint#eJzLz7NSKCgtzuDKyk8qtuJSUChJLS4B0QoKRaV5xbr5QPnSpNK8klLdnESQHFgqNa8MokZBwc3f39bJMchKIS0/HyGkgCJUXJJaUAzToAsy2EohNTkjX0E9IzUnJ18dAPhYJMc=)
+
+`=` must not be included in environment variable names. And `&` and spaces should not be included in them. In almost all
+cases they are mistakes and they may cause some issues on using them in shell since they have special meaning in shell syntax.
+
+actionlint checks environment variable names are correct in `env:` configuration.
+
+
+## Permissions
+
+Example input:
+
+```yaml
+on: push
+
+# ERROR: Available values for whole permissions are "write-all", "read-all" or "none"
+permissions: write
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ permissions:
+ # ERROR: "checks" is correct scope name
+ check: write
+ # ERROR: Available values are "read", "write" or "none"
+ issues: readable
+ steps:
+ - run: echo hello
+```
+
+Output:
+
+```
+test.yaml:4:14: "write" is invalid for permission for all the scopes. available values are "read-all" and "write-all" [permissions]
+ |
+4 | permissions: write
+ | ^~~~~
+test.yaml:11:7: unknown permission scope "check". all available permission scopes are "actions", "attestations", "checks", "contents", "deployments", "discussions", "id-token", "issues", "packages", "pages", "pull-requests", "repository-projects", "security-events", "statuses" [permissions]
+ |
+11 | check: write
+ | ^~~~~~
+test.yaml:13:15: "readable" is invalid for permission of scope "issues". available values are "read", "write" or "none" [permissions]
+ |
+13 | issues: readable
+ | ^~~~~~~~
+```
+
+[Playground](https://rhysd.github.io/actionlint#eJxNjd0NwyAMhN89xS3AAmwDxBK0FCOMlfUDiVr16aTv/qR5dNNM1Hl8imqRph7nKJOJXhLVEzBZ51ZgWFMnq2TR2jRXw/Zu63/gBkDKnN7ftQethPF6GByOEOuDdXL/ldw+8eCUBZlrlQvntjLp)
+
+Permissions of `GITHUB_TOKEN` token can be configured at workflow-level or job-level by [`permissions:` section][perm-config-doc].
+Each permission scopes have its access levels. The default levels are described in [the document][permissions-doc].
+
+actionlint checks permission scopes and access levels in a workflow are correct.
+
+
+## Reusable workflows
+
+[Reusable workflows][reusable-workflow-doc] is a feature to call a workflow from another workflow.
+
+actionlint does several checks for both workflow calls (caller) and reusable workflows (callee):
+
+- syntax of workflow calls and reusable workflows
+- type checks for inputs (respecting `type:` field of each input) in both workflow calls and reusable workflows
+- type checks for `inputs`, `outputs` and `secrets` context objects in reusable workflows
+- optional/required/undefined inputs and secrets at `uses:` in workflow calls
+- type checks for `outputs` objects used by downstream jobs of workflow calls
+
+These checks are described in this section.
+
+### Check input definitions of `workflow_call` event in reusable workflow
+
+Example input:
+
+```yaml
+on:
+ workflow_call:
+ inputs:
+ scheme:
+ description: Scheme of URL
+ # OK: Type is string
+ default: https
+ type: string
+ host:
+ default: example.com
+ type: string
+ port:
+ description: Port of URL
+ # ERROR: Type is number but default value is string
+ default: ':1234'
+ type: number
+ query:
+ description: Query of URL
+ # ERROR: Type must be one of number, string, boolean
+ type: object
+ path:
+ description: Path of URL
+ required: true
+ # ERROR: Default value is never used since this input is required
+ default: ''
+ type: string
+jobs:
+ do:
+ runs-on: ubuntu-latest
+ steps:
+ - run: echo "${{ inputs.scheme }}://${{ inputs.host }}:${{ inputs.port }}${{ inputs.path }}"
+```
+
+Output:
+
+```
+test.yaml:15:18: input of workflow_call event "port" is typed as number but its default value ":1234" cannot be parsed as a float number: strconv.ParseFloat: parsing ":1234": invalid syntax [events]
+ |
+15 | default: ':1234'
+ | ^~~~~~~
+test.yaml:20:15: invalid value "object" for input type of workflow_call event. it must be one of "boolean", "number", or "string" [syntax-check]
+ |
+20 | type: object
+ | ^~~~~~
+test.yaml:25:18: input "path" of workflow_call event has the default value "", but it is also required. if an input is marked as required, its default value will never be used [events]
+ |
+25 | default: ''
+ | ^~
+```
+
+[Playground](https://rhysd.github.io/actionlint#eJx9kctuwyAQRff9ilFUKSsn6mPFN3TRh7quMB4XUswQGJRGkf+9JjiR5dbdwZnhzmUuOXEDcKDw1Vo6fChpbQYAxvnEsZwBotLY4eUG0GBUwXg25AS8nYtALby/Pk1aWpksC9DMPl4xHz0KiByM+xyhpsji9zv8lp23uFHU/ffaU+AFY89DadHWWtzdPzyuZ9IudTWGEe4ThuOC9kuuzcWLBtU7VHyxJ1kv2RtKc4WA+2QCNgI4JPzD9dzwuIsd1eewGirDQnKxykNSnRynykrGWDxFRn8Ntsqdw66VJljdnk5j7psSOPS92G4nOEeV4QTl/Q9oSvK/+n71A7U5rsA=)
+
+Unlike inputs of action, inputs of a workflow must specify their types. actionlint validates input types and checks the default
+values are correctly typed. For more details, see [the official document][create-reusable-workflow-doc].
+
+### Check workflow call syntax
+
+Example input:
+
+```yaml
+on: push
+jobs:
+ job1:
+ uses: owner/repo/path/to/workflow.yml@v1
+ # ERROR: 'runs-on' is not available on calling reusable workflow
+ runs-on: ubuntu-latest
+ job2:
+ # ERROR: Local file path with ref is not available
+ uses: ./.github/workflows/ci.yml@main
+ job3:
+ # ERROR: 'with' is only available on calling reusable workflow
+ with:
+ foo: bar
+ runs-on: ubuntu-latest
+ steps:
+ - run: echo hello
+ job4:
+ # ERROR: This workflow does not exist
+ uses: ./.github/workflows/not-existing.yml
+```
+
+Output:
+
+```
+test.yaml:6:5: when a reusable workflow is called with "uses", "runs-on" is not available. only following keys are allowed: "name", "uses", "with", "secrets", "needs", "if", and "permissions" in job "job1" [syntax-check]
+ |
+6 | runs-on: ubuntu-latest
+ | ^~~~~~~~
+test.yaml:9:11: reusable workflow call "./.github/workflows/ci.yml@main" at "uses" is not following the format "owner/repo/path/to/workflow.yml@ref" nor "./path/to/workflow.yml". see https://docs.github.com/en/actions/learn-github-actions/reusing-workflows for more details [workflow-call]
+ |
+9 | uses: ./.github/workflows/ci.yml@main
+ | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+test.yaml:12:5: "with" is only available for a reusable workflow call with "uses" but "uses" is not found in job "job3" [syntax-check]
+ |
+12 | with:
+ | ^~~~~
+test.yaml:19:11: could not read reusable workflow file for "./.github/workflows/not-existing.yml": open /path/to/.github/workflows/not-existing.yml: no such file or directory [workflow-call]
+ |
+19 | uses: ./.github/workflows/not-existing.yml
+ | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+```
+
+[Playground](https://rhysd.github.io/actionlint#eJyFjkESwiAMRfeeIhdomaorVl4FOmlBKWFIEL29bRkdV7r6Wfz38ilqSIXd4UqW9QFgzWFLgMLIGqhGzCpjIpWMOCWkKuXbFKj2zyVc7sNeziVyR6us2BKldMEIsjTf8dvXq3724or9aFiNflctxsdGnBpR12K7ACYiDdbk398AWDDxG+q2pgYcHYHDEKjpz/8GRZIOH57Fx3mb9gJtJVzI)
+
+When calling an external workflow, [only specific keys are available][reusable-workflow-call-keys] at job configuration.
+For example, `secrets:` is not available when running steps in a normal job. And `runs-on:` is not available when calling
+a reusable workflow since the called workflow determines which OS is used. actionlint checks such keys are used correctly
+to call a reusable workflow or to run steps in a normal job.
+
+And the workflow syntax at `uses:` must follow the format `owner/repo/path/to/workflow.yml@ref` as described in
+[the official document][create-reusable-workflow-doc]. actionlint checks if the value follows the format.
+
+actionlint also validates the called workflow file is actually existing when it is a local workflow (starting with `./`).
+actionlint reports an error when it does not exist.
+
+### Check types of `inputs.*` and `secrets.*` in reusable workflow
+
+Example input:
+
+```yaml
+on:
+ workflow_call:
+ inputs:
+ url:
+ description: 'your URL'
+ type: string
+ lucky_number:
+ description: 'your lucky number'
+ type: number
+ secrets:
+ credential:
+ description: 'your credential'
+
+jobs:
+ test:
+ runs-on: ubuntu-20.04
+ steps:
+ - name: Send data
+ # ERROR: uri is typo of url
+ run: curl ${{ inputs.uri }} -d ${{ inputs.lucky_number }}
+ env:
+ # ERROR: credentials is typo of credential
+ TOKEN: ${{ secrets.credentials }}
+```
+
+Output:
+
+```
+test.yaml:20:23: property "uri" is not defined in object type {url: string; lucky_number: number} [expression]
+ |
+20 | run: curl ${{ inputs.uri }} -d ${{ inputs.lucky_number }}
+ | ^~~~~~~~~~
+test.yaml:23:22: property "credentials" is not defined in object type {credential: string} [expression]
+ |
+23 | TOKEN: ${{ secrets.credentials }}
+ | ^~~~~~~~~~~~~~~~~~~
+```
+
+[Playground](https://rhysd.github.io/actionlint#eJx9UD1PwzAQ3fMr3oCUKVGFmLwzgUAqMFeOfSDT9BzZZ6qo6n/HjUNSMXS7e/c+7s6zqoCjD/vP3h93Rvf9BQAcD0liqYEU+r8SsBRNcIM4zwr16FPAx/a5XuYyDqQQJTj+msE+mf2443ToKNw0mogoxP+OBZ3ASCbQul5uLLE4fXvLlVZX1bfvJr1QlKIKiWNzYacusaTmftNuHkqc0LCENWB9yOu8EVtYLXqJzAYKJv8Kd6fT/ME2BYfzGY29Bq//kaeLA/HPegHw/vr0+KIm4Xxxu94Qs/AXqVaEog==)
+
+Inputs of reusable workflow calls are set to `inputs.*` properties following the definitions at `on.workflow_call.inputs`.
+And in a job of a reusable workflow, `secrets.*` are passed from caller of the workflow so it is set following the definitions at
+`on.workflow_call.secrets`. See [the official document][create-reusable-workflow-doc] for more details.
+
+actionlint contextually defines types of `inputs` and `secrets` contexts looking at `workflow_call` event. Keys of `inputs` only
+allow keys at `on.workflow_call.inputs` and their values are typed based on `on.workflow_call.inputs..type`. Type of
+`secrets` is also strictly typed following `on.workflow_call.secrets`.
+
+[From May 3, 2022][inherit-secrets-announce], GitHub Actions allows inheriting secrets by calling reusable workflows. The caller
+declares to inherit all secrets.
+
+```yaml
+jobs:
+ pass-secrets-to-workflow:
+ uses: ./.github/workflows/called-workflow.yml
+ secrets: inherit
+```
+
+This means that actionlint cannot know whether the workflow inherits secrets or not when checking a reusable workflow.
+To solve this issue, actionlint assumes that
+
+- when `secrets:` is omitted in a reusable workflow, the workflow inherits secrets from a caller
+- when `secrets:` exists in a reusable workflow, the workflow inherits no other secret
+
+Following the assumptions,
+
+```yaml
+on:
+ workflow_call:
+
+jobs:
+ pass-secret-to-action:
+ runs-on: ubuntu-latest
+ steps:
+ # OK: This reports no error. FOO is assumed to be inherited from caller
+ - run: echo ${{ secrets.FOO }}
+```
+
+this workflow causes no error. And
+
+```yaml
+on:
+ workflow_call:
+ secrets:
+
+jobs:
+ pass-secret-to-action:
+ runs-on: ubuntu-latest
+ steps:
+ # ERROR: Secret FOO is not defined
+ - run: echo ${{ secrets.FOO }}
+```
+
+this workflow causes 'no such secret' error at `secrets.FOO`.
+
+### Check outputs in reusable workflow
+
+Example input:
+
+```yaml
+on:
+ workflow_call:
+ outputs:
+ image-version:
+ description: "Docker image version"
+ # ERROR: 'imagetag' does not exist (typo of 'image_tag')
+ value: ${{ jobs.gen-image-version.outputs.imagetag }}
+jobs:
+ gen-image-version:
+ runs-on: ubuntu-latest
+ outputs:
+ image_tag: "${{ steps.get_tag.outputs.tag }}"
+ steps:
+ - run: ./output_image_tag.sh
+ id: get_tag
+```
+
+Output:
+
+```
+test.yaml:6:20: property "imagetag" is not defined in object type {image_tag: string} [expression]
+ |
+6 | value: ${{ jobs.gen-image-version.outputs.imagetag }}
+ | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+```
+
+[Playground](https://rhysd.github.io/actionlint#eJx1j0EOgyAQRfc9xcR0C92z7j0M6tRSKRgYdGG8ex1Rkqbpkp/Hf3+8UxeA2YfhYf1ct9paDgB8ojFRzA8A89Y9iglDNN6dIUCHsQ1mJA6huvt2wJBZONiqsJO2CRVclwVevomyRye+auXhlHtKuod1vTDKvh86jwjJRcHy1CRHSVhNGOnvBfXWug3lDZFw5BHEWVFnax69E+d3wSoF8pbJutTJ+Cwnmk7B0fgB2ORuYw==)
+
+Outputs of a reusable workflow can be defined at `on.workflow_call.outputs` as described in [the document][reusable-workflow-outputs].
+The `jobs` context is available to define an output value to refer the outputs of jobs in the workflow. actionlint checks
+the context is used correctly.
+
+### Check inputs and secrets in workflow call
+
+Example reusable workflow:
+
+```yaml
+# .github/workflows/reusable.yaml
+on:
+ workflow_call:
+ inputs:
+ name:
+ type: string
+ required: true
+ id:
+ type: number
+ message:
+ type: string
+ secrets:
+ password:
+ required: true
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - run: echo '${{ outputs.required_input }}'
+```
+
+Example input:
+
+```yaml
+on: push
+
+jobs:
+ # Check required/undefined inputs and secrets
+ missing-required:
+ uses: ./.github/workflows/reusable.yaml
+ with:
+ # ERROR: Undefined input
+ user: rhysd
+ # ERROR: Required input "name" is missing
+ secrets:
+ # ERROR: Undefined secret
+ credentials: my-token
+ # ERROR: Required secret "password" is missing
+
+ # Check types of inputs defined in reusable workflow
+ type-checks:
+ uses: ./.github/workflows/reusable.yaml
+ with:
+ name: rhysd
+ # ERROR: Cannot assign bool value to number input
+ id: true
+ # ERROR: Cannot assign null to string input. If you want to pass string "null", use ${{ 'null' }}
+ message: null
+ secrets:
+ password: p@ssw0rd
+```
+
+Output:
+
+```
+test.yaml:6:11: input "name" is required by "./.github/workflows/reusable.yaml" reusable workflow [workflow-call]
+ |
+6 | uses: ./.github/workflows/reusable.yaml
+ | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+test.yaml:6:11: secret "password" is required by "./.github/workflows/reusable.yaml" reusable workflow [workflow-call]
+ |
+6 | uses: ./.github/workflows/reusable.yaml
+ | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+test.yaml:9:7: input "user" is not defined in "./.github/workflows/reusable.yaml" reusable workflow. defined inputs are "id", "message", "name" [workflow-call]
+ |
+9 | user: rhysd
+ | ^~~~~
+test.yaml:13:7: secret "credentials" is not defined in "./.github/workflows/reusable.yaml" reusable workflow. defined secret is "password" [workflow-call]
+ |
+13 | credentials: my-token
+ | ^~~~~~~~~~~~
+test.yaml:22:11: input "id" is typed as number by reusable workflow "./.github/workflows/reusable.yaml". bool value cannot be assigned [expression]
+ |
+22 | id: true
+ | ^~~~
+test.yaml:24:16: input "message" is typed as string by reusable workflow "./.github/workflows/reusable.yaml". null value cannot be assigned [expression]
+ |
+24 | message: null
+ | ^~~~
+```
+
+Reusable workflows can define required/optional inputs and secrets. When they are missing or some undefined input is used in a
+workflow call, actionlint reports an error.
+
+And reusable workflows must define types of their inputs by `type:` field. Workflow calls pass constants (`input: 42`) or
+expressions (`inputs: ${{ ... }}`) to the inputs or secrets. actionlint checks types of values passed to inputs in workflow call.
+When a type of input doesn't match to its definition, actionlint reports an error.
+
+Note that this check only works with local reusable workflow (it starts with `./`).
+
+### Check outputs of workflow call in downstream jobs
+
+Example reusable workflow:
+
+```yaml
+# .github/workflows/get-build-info.yaml
+on:
+ workflow_call:
+ outputs:
+ version:
+ value: ${{ outputs.version }}
+ description: version of software
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ outputs:
+ version: ${{ steps.get_version.outputs.version }}
+ steps:
+ - run: ...
+ id: get_version
+```
+
+Example input:
+
+```yaml
+on: push
+
+jobs:
+ get_build_info:
+ uses: ./.github/workflows/get-build-info.yaml
+ downstream:
+ needs: [get_build_info]
+ runs-on: ubuntu-latest
+ steps:
+ # OK. `version` is defined in the reusable workflow
+ - run: echo '${{ needs.get_build_info.outputs.version }}'
+ # ERROR: `tag` is not defined in the reusable workflow
+ - run: echo '${{ needs.get_build_info.outputs.tag }}'
+```
+
+Output:
+
+```
+test.yaml:13:24: property "tag" is not defined in object type {version: string} [expression]
+ |
+13 | - run: echo '${{ needs.get_build_info.outputs.tag }}'
+ | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+```
+
+Outputs of workflow call are set to the job's outputs object. They can be accessed by downstream jobs specified with `needs:`.
+What outputs are set is defined in the reusable workflow. actionlint types outputs objects from workflow calls and check the
+object types in downstream jobs.
+
+In the above example, `get-build-info.yaml` has one output `version`. actionlint types the outputs object of workflow call job
+as `{version: string}`. In the downstream job, actionlint can report an error at undefined key `tag` in the object.
+
+Note that this check only works with local reusable workflow (starting with `./`).
+
+
+## ID naming convention
+
+Example input:
+
+```yaml
+on: push
+
+jobs:
+ # ERROR: '.' cannot be contained in ID
+ foo-v1.2.3:
+ runs-on: ubuntu-latest
+ steps:
+ - run: echo 'job ID with version'
+ # ERROR: ID cannot contain spaces
+ id: echo for test
+ # ERROR: ID cannot start with '-'
+ -hello-world-:
+ runs-on: ubuntu-latest
+ steps:
+ - run: echo 'oops'
+ # ERROR: ID cannot start with numbers
+ 2d-game:
+ runs-on: ubuntu-latest
+ steps:
+ - run: echo 'oops'
+```
+
+Output:
+
+```
+test.yaml:5:3: invalid job ID "foo-v1.2.3". job ID must start with a letter or _ and contain only alphanumeric characters, -, or _ [id]
+ |
+5 | foo-v1.2.3:
+ | ^~~~~~~~~~~
+test.yaml:10:13: invalid step ID "echo for test". step ID must start with a letter or _ and contain only alphanumeric characters, -, or _ [id]
+ |
+10 | id: echo for test
+ | ^~~~
+test.yaml:12:3: invalid job ID "-hello-world-". job ID must start with a letter or _ and contain only alphanumeric characters, -, or _ [id]
+ |
+12 | -hello-world-:
+ | ^~~~~~~~~~~~~~
+test.yaml:17:3: invalid job ID "2d-game". job ID must start with a letter or _ and contain only alphanumeric characters, -, or _ [id]
+ |
+17 | 2d-game:
+ | ^~~~~~~~
+```
+
+[Playground](https://rhysd.github.io/actionlint#eJylzTEOwyAMheGdU7wtkyM13Zi79BhJIYWKYoQhuX5Cyw0yWv70P44aqYpT6sOLaAWszLTdxmm8twvINQrxyepSY6kU5mKl/F5SbJK/AqhJDftyjOGM4fnA7ovDZrN4jkN3gDedrZzRY+RsCEw752DowjBzkrY0GXrPX3u1dACDIlSH)
+
+IDs must start with a letter or `_` and contain only alphanumeric characters, `-` or `_`. actionlint checks the naming
+convention, and reports invalid IDs as errors.
+
+
+## Contexts and special functions availability
+
+Example input:
+
+```yaml
+on: push
+
+env:
+ NAME: rhysd
+
+jobs:
+ test:
+ strategy:
+ matrix:
+ directory:
+ # OK: 'github' context is available here
+ - ${{ github.workflow }}
+ # ERROR: 'runner' context is not available here
+ - ${{ runner.temp }}
+ runs-on: ubuntu-latest
+ env:
+ # ERROR: 'env' context is not available here
+ NAME: ${{ env.NAME }}
+ steps:
+ - env:
+ # OK: 'env' context is available here
+ NAME: ${{ env.NAME }}
+ # ERROR: 'success()' function is not available here
+ run: echo 'Success? ${{ success() }}'
+ # OK: 'success()' function is available here
+ if: success()
+```
+
+Output:
+
+```
+test.yaml:14:17: context "runner" is not allowed here. available contexts are "github", "inputs", "needs", "vars". see https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability for more details [expression]
+ |
+14 | - ${{ runner.temp }}
+ | ^~~~~~~~~~~
+test.yaml:18:17: context "env" is not allowed here. available contexts are "github", "inputs", "matrix", "needs", "secrets", "strategy", "vars". see https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability for more details [expression]
+ |
+18 | NAME: ${{ env.NAME }}
+ | ^~~~~~~~
+test.yaml:24:33: calling function "success" is not allowed here. "success" is only available in "jobs..if", "jobs..steps.if". see https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability for more details [expression]
+ |
+24 | run: echo 'Success? ${{ success() }}'
+ | ^~~~~~~~~
+```
+
+[Playground](https://rhysd.github.io/actionlint#eJx9jkEOgjAURPc9xSxM1AUcoBvjwqVuPAGUr6DQkv5flRDvLhVRExO7aWbmTTvOarSBS6XIXrQCduvtRsOXHRdKnVzO0RRiiTfA4jOhYzcqoMnEV7dJAUXlyYjz3ccCEsz6HsdKypCnV+fPh9pdcb//ID5YSz4VatopHixO3LAy5MFKSOosjnlGr8XxjKvjE4OZRjX1WajlCUu+O/97r781yJQO830whphXT5ZHsVgO8PxNVwf9SR5WsV7P)
+
+Some contexts are only available in some places. For example, `env` context is not available at `jobs..env` but it is
+available at `jobs..steps.env`.
+
+Similarly, some status functions are special since they limit where they can be called. For example, `success()`, `failure()`,
+`always()`, and `cancelled()` are only available at `if:` section. At the time of writing this document, the following functions
+are special.
+
+- `hashFiles()`
+- `always()`
+- `success()`
+- `failure()`
+- `cancelled()`
+
+[The official contexts document][availability-doc] describes which contexts and special functions are available at which workflow
+keys.
+
+actionlint checks if these contexts and special functions are used correctly. It reports an error when it finds that some context
+or special function is not available in your workflow.
+
+
+## Check deprecated workflow commands
+
+Example input:
+
+```yaml
+on: push
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ # ERROR: 'set-output' workflow command was deprecated
+ - run: echo '::set-output name=foo::bar'
+ # OK: Use this instead
+ - run: echo "foo=bar" >> "$GITHUB_OUTPUT"
+ # OK: 'debug' command is not deprecated
+ - run: echo "::debug::Set the Octocat variable"
+```
+
+Output:
+
+```
+test.yaml:8:14: workflow command "set-output" was deprecated. use `echo "{name}={value}" >> $GITHUB_OUTPUT` instead: https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions [deprecated-commands]
+ |
+8 | - run: echo '::set-output name=foo::bar'
+ | ^~~~
+```
+
+[Playground](https://rhysd.github.io/actionlint#eJxtyjEOgkAQRuGeU/zZmFDtBSaBwkatMBFqs4ujaHCHsDOeX9CW6hXvk0SYLA9F8ZKYqQCUs64FZkvZywIsWlLzY1jfb2XlKf8V4FdJ4H4QlESZ1YvpZIoU3lzdRYhimMsN7pZZLc+hruF2h1N77PbXpmvPXeu2PNGNoz2ILqzQgdH0Kn1QfML8DHFk9wXMjT7o)
+
+GitHub deprecated the following workflow commands.
+
+- [`set-output`][deprecate-set-output-save-state]
+- [`save-state`][deprecate-set-output-save-state]
+- [`set-env`][deprecate-set-env-add-path]
+- [`add-path`][deprecate-set-env-add-path]
+
+actionlint detects these commands are used in `run:` and reports them as errors suggesting alternatives. See
+[the official document][workflow-commands-doc] for the comprehensive list of workflow commands to know the usage.
+
+
+## Conditions always evaluated to true at `if:`
+
+Example input:
+
+```yaml
+on: push
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - run: echo 'Commit is pushed'
+ # OK
+ if: ${{ github.event_name == 'push' }}
+ - run: echo 'Commit is pushed'
+ # OK
+ if: |
+ github.event_name == 'push'
+ - run: echo 'Commit is pushed'
+ # ERROR: It is always evaluated to true
+ if: |
+ ${{ github.event_name == 'push' }}
+ - run: echo 'Commit is pushed'
+ # ERROR: It is always evaluated to true
+ if: "${{ github.event_name == 'push' }} "
+ - run: echo 'Commit is pushed to main'
+ # OK
+ if: github.event_name == 'push' && github.ref_name == 'main'
+ - run: echo 'Commit is pushed to main'
+ # ERROR: It is always evaluated to true
+ if: ${{ github.event_name == 'push' }} && ${{ github.ref_name == 'main' }}
+```
+
+Output:
+
+```
+test.yaml:16:13: if: condition "${{ github.event_name == 'push' }}\n" is always evaluated to true because extra characters are around ${{ }} [if-cond]
+ |
+16 | if: |
+ | ^
+test.yaml:20:13: if: condition "${{ github.event_name == 'push' }} " is always evaluated to true because extra characters are around ${{ }} [if-cond]
+ |
+20 | if: "${{ github.event_name == 'push' }} "
+ | ^~~~
+test.yaml:26:13: if: condition "${{ github.event_name == 'push' }} && ${{ github.ref_name == 'main' }}" is always evaluated to true because extra characters are around ${{ }} [if-cond]
+ |
+26 | if: ${{ github.event_name == 'push' }} && ${{ github.ref_name == 'main' }}
+ | ^~~
+```
+
+[Playground](https://rhysd.github.io/actionlint#eJy1j00OgjAQhfec4oUYusIDNGHlQQzoIDW2JXTqBrm7FP8wJogaV5PJ+/K9GWskau+qKNrbwskIYHIcJtB441LbA77whn16yEM2RI6pdhcKSAMpQZvKQqys1oqh3KClrbhCgColFm2LneLKF0s6kuG1yTUhyyACLdB1nztP9w1T7t/E/zg8fi9FPEcLttC5Ms/6KXOS3OKGykc4lnzROOOfvnhEvZb3zBmiAMLK)
+
+Evaluation of `${{ }}` at `if:` condition is tricky. When the expression in `${{ }}` is evaluated to boolean value and there is
+no extra characters around the `${{ }}`, the condition is evaluated to the boolean value. Otherwise the condition is treated as
+string hence it is **always** evaluated to `true`.
+
+It means that multi-line string must not be used at `if:` condition (`if: |`) because the condition is always evaluated to true.
+Multi-line string inserts newline character at end of each line.
+
+```yaml
+if: |
+ ${{ false }}
+```
+
+is equivalent to
+
+```yaml
+if: "${{ false }}\n"
+```
+
+Unlike using `${{ }}`, putting an expression directly ignores white spaces around it. It's the reason why
+
+```yaml
+if: |
+ false
+```
+
+works as intended.
+
+actionlint checks all `if:` conditions in workflow and reports error when some condition is always evaluated to true due to extra
+characters around `${{ }}`.
+
+
+## Action metadata syntax validation
+
+Example workflow input:
+
+```yaml
+on: push
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ # actionlint checks an action when it is actually used in a workflow
+ - uses: ./.github/actions/my-invalid-action
+```
+
+Example action metadata:
+
+```yaml
+# .github/actions/my-invalid-action/action.yml
+
+name: 'My action'
+author: '...'
+# ERROR: 'description' section is required
+
+branding:
+ # ERROR: Invalid icon name
+ icon: dog
+ # ERROR: Unsupported icon color
+ color: black
+
+runs:
+ # ERROR: Node.js runtime version is too old
+ using: 'node14'
+ # ERROR: The source file being run by this action does not exist
+ main: 'this-file-does-not-exist.js'
+ # ERROR: 'env' configuration is only allowed for Docker actions
+ env:
+ SOME_VAR: SOME_VALUE
+```
+
+Output:
+
+```
+action_metadata_syntax_validation.yaml:8:15: description is required in metadata of "My action" action at "path/to/.github/actions/my-invalid-action/action.yml" [action]
+ |
+8 | - uses: ./.github/actions/my-invalid-action
+ | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+action_metadata_syntax_validation.yaml:8:15: incorrect icon name "dog" at branding.icon in metadata of "My action" action at "path/to/.github/actions/my-invalid-action/action.yml". see the official document to know the exhaustive list of supported icons: https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#brandingicon [action]
+ |
+8 | - uses: ./.github/actions/my-invalid-action
+ | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+action_metadata_syntax_validation.yaml:8:15: incorrect color "black" at branding.icon in metadata of "My action" action at "path/to/.github/actions/my-invalid-action/action.yml". see the official document to know the exhaustive list of supported colors: https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#brandingcolor [action]
+ |
+8 | - uses: ./.github/actions/my-invalid-action
+ | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+action_metadata_syntax_validation.yaml:8:15: invalid runner name "node14" at runs.using in "My action" action defined at "path/to/.github/actions/my-invalid-action". valid runners are "composite", "docker", "node16", and "node20". see https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#runs [action]
+ |
+8 | - uses: ./.github/actions/my-invalid-action
+ | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+action_metadata_syntax_validation.yaml:8:15: file "this-file-does-not-exist.js" does not exist in "path/to/.github/actions/my-invalid-action". it is specified at "main" key in "runs" section in "My action" action [action]
+ |
+8 | - uses: ./.github/actions/my-invalid-action
+ | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+action_metadata_syntax_validation.yaml:8:15: "env" is not allowed in "runs" section because "My action" is a JavaScript action. the action is defined at "path/to/.github/actions/my-invalid-action" [action]
+ |
+8 | - uses: ./.github/actions/my-invalid-action
+ | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+```
+
+All actions require a metadata file `action.yml` or `aciton.yaml`. The syntax is defined in [the official document][action-metadata-doc].
+
+actionlint checks metadata files used in workflows and reports errors when they are not following the syntax.
+
+- `name:`, `description:`, `runs:` sections are required
+- Runner name at `using:` is one of `composite`, `docker`, `node16`, `node20`
+- Keys under `runs:` section are correct. Required/Valid keys are different depending on the type of action; Docker action or
+ Composite action or JavaScript action (e.g. `image:` is required for Docker action).
+- Files specified in some keys under `runs` are existing. For example, JavaScript action defines a script file path for
+ entrypoint at `main:`.
+- Icon name at `icon:` in `branding:` section is correct. Supported icon names are listed in
+ [the official document][branding-icons-doc].
+- Icon color at `color:` in `branding:` section is correct. Supported icon colors are white, yellow, blue, green, orange, red,
+ purple, or gray-dark.
+
+actionlint checks action metadata files which are used by workflows. Currently it is not supported to specify `action.yml`
+directly via command line arguments.
+
+Note that `steps` in Composite action's metadata is not checked at this point. It will be supported in the future.
+
+---
+
+[Installation](install.md) | [Usage](usage.md) | [Configuration](config.md) | [Go API](api.md) | [References](reference.md)
+
+[yamllint]: https://github.com/adrienverge/yamllint
+[issue-form]: https://github.com/rhysd/actionlint/issues/new
+[syntax-doc]: https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions
+[filter-pattern-doc]: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#filter-pattern-cheat-sheet
+[shellcheck]: https://github.com/koalaman/shellcheck
+[shellcheck-install]: https://github.com/koalaman/shellcheck#installing
+[SC1091]: https://github.com/koalaman/shellcheck/wiki/SC1091
+[SC2050]: https://github.com/koalaman/shellcheck/wiki/SC2050
+[SC2194]: https://github.com/koalaman/shellcheck/wiki/SC2194
+[SC2154]: https://github.com/koalaman/shellcheck/wiki/SC2154
+[SC2157]: https://github.com/koalaman/shellcheck/wiki/SC2157
+[shellcheck-env-var]: https://github.com/koalaman/shellcheck/wiki/Integration#environment-variables
+[pyflakes]: https://github.com/PyCQA/pyflakes
+[expr-doc]: https://docs.github.com/en/actions/learn-github-actions/expressions
+[contexts-doc]: https://docs.github.com/en/actions/learn-github-actions/contexts
+[funcs-doc]: https://docs.github.com/en/actions/learn-github-actions/expressions#functions
+[needs-doc]: https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#jobsjob_idneeds
+[needs-context-doc]: https://docs.github.com/en/actions/learn-github-actions/contexts#needs-context
+[shell-doc]: https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#using-a-specific-shell
+[matrix-doc]: https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#jobsjob_idstrategymatrix
+[webhook-doc]: https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#webhook-events
+[schedule-event-doc]: https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#scheduled-events
+[cron-syntax]: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/crontab.html#tag_20_25_07
+[gh-hosted-runner]: https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners
+[self-hosted-runner]: https://docs.github.com/en/actions/hosting-your-own-runners/about-self-hosted-runners
+[action-uses-doc]: https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#jobsjob_idstepsuses
+[dependabot-doc]: https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot
+[credentials-doc]: https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#jobsjob_idcontainercredentials
+[actions-cache]: https://github.com/actions/cache
+[permissions-doc]: https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token
+[perm-config-doc]: https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#permissions
+[generate-webhook-events]: https://github.com/rhysd/actionlint/tree/main/scripts/generate-webhook-events
+[generate-popular-actions]: https://github.com/rhysd/actionlint/tree/main/scripts/generate-popular-actions
+[issue-25]: https://github.com/rhysd/actionlint/issues/25
+[issue-40]: https://github.com/rhysd/actionlint/issues/40
+[security-doc]: https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions
+[reusable-workflow-doc]: https://docs.github.com/en/actions/learn-github-actions/reusing-workflows
+[create-reusable-workflow-doc]: https://docs.github.com/en/actions/learn-github-actions/reusing-workflows#creating-a-reusable-workflow
+[reusable-workflow-call-keys]: https://docs.github.com/en/actions/learn-github-actions/reusing-workflows#supported-keywords-for-jobs-that-call-a-reusable-workflow
+[object-filter-syntax]: https://docs.github.com/en/actions/learn-github-actions/expressions#object-filters
+[github-script]: https://github.com/actions/github-script
+[workflow-dispatch-event]: https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#workflow_dispatch
+[workflow-dispatch-input-type-announce]: https://github.blog/changelog/2021-11-10-github-actions-input-types-for-manual-workflows/
+[reusable-workflow-outputs]: https://docs.github.com/en/actions/using-workflows/reusing-workflows#using-outputs-from-a-reusable-workflow
+[inherit-secrets-announce]: https://github.blog/changelog/2022-05-03-github-actions-simplify-using-secrets-with-reusable-workflows/
+[specific-paths-doc]: https://docs.github.com/en/actions/using-workflows/triggering-a-workflow#using-filters-to-target-specific-paths-for-pull-request-or-push-events
+[availability-doc]: https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability
+[deprecate-set-output-save-state]: https://github.blog/changelog/2022-10-11-github-actions-deprecating-save-state-and-set-output-commands/
+[deprecate-set-env-add-path]: https://github.blog/changelog/2020-10-01-github-actions-deprecating-set-env-and-add-path-commands/
+[workflow-commands-doc]: https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions
+[action-metadata-doc]: https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions
+[branding-icons-doc]: https://github.com/github/docs/blob/main/content/actions/creating-actions/metadata-syntax-for-github-actions.md#exhaustive-list-of-all-currently-supported-icons
+[operators-doc]: https://docs.github.com/en/actions/learn-github-actions/expressions#operators
diff --git a/docs/archive/plans/actionlint-config.md b/docs/archive/plans/actionlint-config.md
new file mode 100644
index 0000000000..2d5775f0f6
--- /dev/null
+++ b/docs/archive/plans/actionlint-config.md
@@ -0,0 +1,52 @@
+Configuration
+=============
+
+This document describes how to configure [actionlint](..) behavior.
+
+Note that configuration file is optional. The author tries to keep configuration file as minimal as possible not to
+bother users to configure behavior of actionlint. Running actionlint without configuration file would work fine in most
+cases.
+
+## Configuration file
+
+Configuration file `actionlint.yaml` or `actionlint.yml` can be put in `.github` directory.
+
+Note: If you're using [Super-Linter][], the file should be placed in a different directory. Please check the project's document.
+
+You don't need to write the first configuration file by your hand. `actionlint` command can generate a default configuration
+with `-init-config` flag.
+
+```sh
+actionlint -init-config
+vim .github/actionlint.yaml
+```
+
+Currently only one item can be configured.
+
+```yaml
+self-hosted-runner:
+ # Labels of self-hosted runner in array of string
+ labels:
+ - linux.2xlarge
+ - windows-latest-xl
+ - linux-multi-gpu
+# Configuration variables in array of strings defined in your repository or organization
+config-variables:
+ - DEFAULT_RUNNER
+ - JOB_NAME
+ - ENVIRONMENT_STAGE
+```
+
+- `self-hosted-runner`: Configuration for your self-hosted runner environment.
+ - `labels`: Label names added to your self-hosted runners as list of pattern. Glob syntax supported by [`path.Match`][pat]
+ is available.
+- `config-variables`: [Configuration variables][vars]. When an array is set, actionlint will check `vars` properties strictly.
+ An empty array means no variable is allowed. The default value `null` disables the check.
+
+---
+
+[Checks](checks.md) | [Installation](install.md) | [Usage](usage.md) | [Go API](api.md) | [References](reference.md)
+
+[Super-Linter]: https://github.com/super-linter/super-linter
+[pat]: https://pkg.go.dev/path#Match
+[vars]: https://docs.github.com/en/actions/learn-github-actions/variables
diff --git a/docs/archive/plans/actionlint-install-reference.md b/docs/archive/plans/actionlint-install-reference.md
new file mode 100644
index 0000000000..22dd9922d8
--- /dev/null
+++ b/docs/archive/plans/actionlint-install-reference.md
@@ -0,0 +1,179 @@
+Installation
+============
+
+This document describes how to install [actionlint](../docs).
+
+## Windows
+
+### [Chocolatey](https://chocolatey.org/)
+
+[`actionlint` package][chocolatey] is available in the community repo:
+
+```powershell
+choco install actionlint
+```
+
+### [Scoop](https://scoop.sh/)
+
+[`actionlint` package][scoop] is available in the main bucket:
+
+```powershell
+scoop install actionlint
+```
+
+### [Winget](https://learn.microsoft.com/en-us/windows/package-manager/)
+
+[`actionlint` package][winget] is available in the winget-pkgs repo:
+
+```powershell
+winget install actionlint
+```
+
+## Linux
+
+### [Arch Linux](https://archlinux.org/)
+
+[`actionlint` package][archlinux] is available in the official repo:
+
+```sh
+pacman -S actionlint
+```
+
+Alternatively actionlint is also available on [AUR][aur]. The packages can be installed via [`paru`][paru] command.
+
+- [actionlint-bin](https://aur.archlinux.org/packages/actionlint-bin)
+- [actionlint-git](https://aur.archlinux.org/packages/actionlint-git)
+
+### [Nix](https://nixos.wiki/)
+
+[`actionlint` package][nixpkgs] is available in the Nix ecosystem:
+
+On NixOS:
+
+```sh
+nix-env -iA nixos.actionlint
+```
+
+On Non NixOS:
+
+```sh
+nix-env -iA nixpkgs.actionlint
+```
+
+## macOS
+
+### [Homebrew][homebrew]
+
+[`actionlint`][formula] formula is provided by Homebrew officially.
+
+```sh
+brew install actionlint
+```
+
+Alternatively rhysd/actionlint repository also provides its own Homebrew package, which is automatically updated on new release.
+If you prefer it, tap the repository before running `brew install`.
+
+```sh
+brew tap "rhysd/actionlint" "https://github.com/rhysd/actionlint"
+brew install actionlint
+```
+
+## Prebuilt binaries
+
+Download an archive file from [the releases page][releases] for your platform, unarchive it and put the executable file to a
+directory in `$PATH`.
+
+Prebuilt binaries are built at each release by CI for the following OS and arch:
+
+- macOS (x86_64, arm64)
+- Linux (i386, x86_64, arm32, arm64)
+- Windows (i386, x86_64, arm64)
+- FreeBSD (i386, x86_64)
+
+Note: The following targets are not tested since GitHub Actions doesn't support them:
+
+- Linux i386, arm32, arm64
+- Windows i386, arm64
+- FreeBSD i386, x86_64
+
+
+## Download script
+
+To install `actionlint` executable with one command, [the download script](../scripts/download-actionlint.bash) is available.
+It downloads the latest version of actionlint (`actionlint.exe` on Windows and `actionlint` on other OSes) to the current
+directory automatically. This is a recommended way if you install actionlint in some shell script.
+
+```sh
+bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash)
+```
+
+When you need to install specific version of actionlint, please give the version to the 1st command line argument. The following
+example installs v1.6.17.
+
+```sh
+bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash) 1.6.17
+```
+
+This script downloads `actionlint` (or `actionlint.exe` on Windows) binary to the current working directory. When you need to put
+the downloaded binary to some other directory, please give the directory path to the 2nd command line argument. The following
+example installs the latest version to `/usr/bin`.
+
+```sh
+bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash) latest /usr/bin
+```
+
+For the usage of actionlint on GitHub Actions, see [the usage document](usage.md#on-github-actions).
+
+## Docker image
+
+See [the usage document](./usage.md#docker) to know how to install and use an official actionlint Docker image.
+
+## asdf
+
+You can install actionlint with the [asdf version manager][asdf] using the [asdf-actionlint][asdf-plugin] plugin, which
+automates the process of installing (and switching between) various versions of GitHub release binaries. With asdf already
+installed, run these commands to install actionlint:
+
+```bash
+# Add actionlint plugin
+asdf plugin add actionlint
+
+# Show all installable versions
+asdf list-all actionlint
+
+# Install specific version
+asdf install actionlint latest
+
+# Set a version globally (on your ~/.tool-versions file)
+asdf global actionlint latest
+```
+
+## Build from source
+
+Recent [Go][] toolchain is necessary to build actionlint from source. Use Go 1.16 or later.
+
+```sh
+# Install the latest stable version
+go install github.com/rhysd/actionlint/cmd/actionlint@latest
+
+# Install the head of main branch
+go install github.com/rhysd/actionlint/cmd/actionlint
+```
+
+---
+
+[Checks](checks.md) | [Usage](usage.md) | [Configuration](config.md) | [Go API](api.md) | [References](reference.md)
+
+[formula]: https://formulae.brew.sh/formula/actionlint
+[homebrew]: https://brew.sh/
+[releases]: https://github.com/rhysd/actionlint/releases
+[Go]: https://golang.org/
+[asdf]: https://asdf-vm.com/
+[asdf-plugin]: https://github.com/crazy-matt/asdf-actionlint
+[chocolatey]: https://community.chocolatey.org/packages/actionlint
+[scoop]: https://scoop.sh/#/apps?q=actionlint&s=0&d=1&o=true
+[winget]: https://github.com/microsoft/winget-pkgs/tree/master/manifests/r/rhysd/actionlint
+[archlinux]: https://archlinux.org/packages/extra/x86_64/actionlint/
+[aur]: https://aur.archlinux.org/
+[paru]: https://github.com/Morganamilo/paru
+[nixpkgs]: https://github.com/NixOS/nixpkgs/blob/master/pkgs/development/tools/analysis/actionlint/default.nix
diff --git a/docs/archive/plans/actionlint-reference.md b/docs/archive/plans/actionlint-reference.md
new file mode 100644
index 0000000000..9aa8e26b82
--- /dev/null
+++ b/docs/archive/plans/actionlint-reference.md
@@ -0,0 +1,24 @@
+References
+==========
+
+This document describes links to resources.
+
+- Repository: https://github.com/rhysd/actionlint
+- Playground: https://rhysd.github.io/actionlint/
+- GitHub Actions official documentations
+ - Workflow syntax: https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions
+ - Expression syntax: https://docs.github.com/en/actions/learn-github-actions/expressions
+ - Built-in functions: https://docs.github.com/en/actions/learn-github-actions/expressions#functions
+ - Webhook events: https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#webhook-events
+ - Self-hosted runner: https://docs.github.com/en/actions/hosting-your-own-runners/about-self-hosted-runners
+ - Security: https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions
+- CRON syntax: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/crontab.html#tag_20_25_07
+- shellcheck: https://github.com/koalaman/shellcheck
+- pyflakes: https://github.com/PyCQA/pyflakes
+- Japanese blog posts
+ - GitHub Actions のワークフローをチェックする actionlint をつくった: https://rhysd.hatenablog.com/entry/2021/07/11/214313
+ - actionlint v1.4 → v1.6 で実装した新機能の紹介: https://rhysd.hatenablog.com/entry/2021/08/11/221044
+
+---
+
+[Checks](checks.md) | [Installation](install.md) | [Usage](usage.md) | [Configuration](config.md) | [Go API](api.md)
diff --git a/docs/archive/plans/actionlint-usage.md b/docs/archive/plans/actionlint-usage.md
new file mode 100644
index 0000000000..6e769fd6cc
--- /dev/null
+++ b/docs/archive/plans/actionlint-usage.md
@@ -0,0 +1,486 @@
+Usage
+=====
+
+This document describes how to use [actionlint](..).
+
+## `actionlint` command
+
+With no argument, actionlint finds all workflow files in the current repository and checks them.
+
+```sh
+actionlint
+```
+
+When paths to YAML workflow files are given as arguments, actionlint checks them.
+
+```sh
+actionlint path/to/workflow1.yaml path/to/workflow2.yaml
+```
+
+When `-` argument is given, actionlint reads inputs from stdin and checks it as workflow source.
+
+```sh
+cat path/to/workflow.yaml | actionlint -
+```
+
+To know all flags and options, see an output of `actionlint -h` or [the online command manual][cmd-manual].
+
+### Ignore some errors
+
+To ignore some errors, `-ignore` option offers to filter errors by messages using regular expression. The option is repeatable.
+The regular expression syntax is the same as [RE2][re2].
+
+```sh
+actionlint -ignore 'label ".+" is unknown' -ignore '".+" is potentially untrusted'
+```
+
+`-shellcheck` and `-pyflakes` specifies file paths of executables. Setting empty string to them disables `shellcheck` and
+`pyflakes` rules. As a bonus, disabling them makes actionlint much faster Since these external linter integrations spawn many
+processes.
+
+```sh
+actionlint -shellcheck= -pyflakes=
+```
+
+
+### Format error messages
+
+`-format` option can flexibly format error messages with [Go template syntax][go-template].
+
+Before explaining the formatting details, let's see some examples.
+
+#### Example: Serialized into JSON
+
+```sh
+actionlint -format '{{json .}}'
+```
+
+Output:
+
+```
+[{"message":"unexpected key \"branch\" for ...
+```
+
+#### Example: Markdown
+
+````sh
+actionlint -format '{{range $err := .}}### Error at line {{$err.Line}}, col {{$err.Column}} of `{{$err.Filepath}}`\n\n{{$err.Message}}\n\n```\n{{$err.Snippet}}\n```\n\n{{end}}'
+````
+
+Output:
+
+````markdown
+### Error at line 21, col 20 of `test.yaml`
+
+property "platform" is not defined in object type {os: string}
+
+```
+ key: ${{ matrix.platform }}-node-${{ hashFiles('**/package-lock.json') }}
+ ^~~~~~~~~~~~~~~
+```
+````
+
+#### Example: Serialized in [JSON Lines][jsonl]
+
+```sh
+actionlint -format '{{range $err := .}}{{json $err}}{{end}}'
+```
+
+Output:
+
+```
+{"message":"unexpected key \"branch\" for ...
+{"message":"character '\\' is invalid for branch ...
+{"message":"label \"linux-latest\" is unknown. ...
+```
+
+#### Example: [Error annotation][ga-annotate-error] on GitHub Actions
+
+````sh
+actionlint -format '{{range $err := .}}::error file={{$err.Filepath}},line={{$err.Line}},col={{$err.Column}}::{{$err.Message}}%0A```%0A{{replace $err.Snippet "\\n" "%0A"}}%0A```\n{{end}}' -ignore 'SC2016:'
+````
+
+Output:
+
+
+
+To include newlines in the annotation body, it prints `%0A`. (ref [actions/toolkit#193](https://github.com/actions/toolkit/issues/193)).
+And it suppresses `SC2016` shellcheck rule error since it complains about the template argument.
+
+Basically it is more recommended to use [Problem Matchers](#problem-matchers) or reviewdog as explained in
+['Tools integration' section](#tools-integ) below.
+
+#### Example: [SARIF format][sarif]
+
+[The Static Analysis Results Interchange Format (SARIF)][sarif] is a standardized format for the results of static analysis tools.
+
+Since this practical format is much more complex than the above examples, the template is not written here. Please read
+[the template file in test data](../testdata/format/sarif_template.txt).
+
+Outputs are also too large to be written here. Please read [the output example in test data](../testdata/format/test.sarif).
+
+#### Formatting syntax
+
+In [Go template syntax][go-template], `.` within `{{ }}` means the target object. Here, the target object is a sequence of error
+objects.
+
+The sequence can be traversed with `range` action, which is like `for ... = range ... {}` in Go.
+
+```
+{{range $err := .}} this part iterates error objects with the iteration variable $err {{end}}
+```
+
+The error object has the following fields.
+
+| Field | Description | Example |
+|----------------------|-------------------------------------------------------|------------------------------------------------------------------|
+| `{{$err.Message}}` | Body of error message | `property "platform" is not defined in object type {os: string}` |
+| `{{$err.Snippet}}` | Code snippet to indicate error position | ` node_version: 16.x\n ^~~~~~~~~~~~~` |
+| `{{$err.Kind}}` | Name of rule the error belongs to | `expression` |
+| `{{$err.Filepath}}` | Canonical relative file path of the error position | `.github/workflows/ci.yaml` |
+| `{{$err.Line}}` | Line number of the error position (1-based) | `9` |
+| `{{$err.Column}}` | Column number of the error's start position (1-based) | `11` |
+| `{{$err.EndColumn}}` | Column number of the error's end position (1-based) | `23` |
+
+Functions called in `{{ }}` placeholder are template actions. There are many actions defined by Go standard library. In addition,
+there are a few custom actions defined by actionlint. Most useful action would be `json` as we already used it in the above JSON
+example. List of all custom actions are as follows:
+
+| Action | Description | Example usage |
+|------------------|----------------------------------------------------------------------------------|-------------------------------------------|
+| `json x` | Serialize `x` as JSON string followed by newline character | `{{json $err}}` |
+| `replace x y z` | Replace string `y` with `z` in `x` | `{{replace $err.Filepath "\\" "/"}}` |
+| `toPascalCase x` | Convert `x` into PascalCase (e.g. 'foo-bar' to 'FooBar') | `{{toPascalCase $err.Kind}}` |
+| `allKinds` | Return an array of kind objects. The kind object is explained in the below table | `{{range $ = allKinds}}{{$.Name}}{{end}}` |
+| `getVersion` | Return the version of actionlint as string | `{{getVersion}}` |
+
+The kind object returned from `allKinds` action has the following fields.
+
+| Field | Description | Example |
+|-------------------------|-------------------------------|---------------------------------------------|
+| `{{$kind.Name}}` | Name of the kind | `syntax-check` |
+| `{{$kind.Description}}` | Short description of the kind | `Checks for GitHub Actions workflow syntax` |
+
+For example, the following simple iteration body
+
+```
+line is {{$err.Line}}, col is {{$err.Column}}, message is {{$err.Message | printf "%q"}}
+```
+
+will produce output like below.
+
+```
+line is 21, col is 20, message is "property \"platform\" is not defined in object type {os: string}"
+```
+
+In `{{ }}` placeholder, input can be piped and action can be used to transform texts. In above example, the message is piped with
+`|` and transformed with `printf "%q"`.
+
+Note that special characters escaped with back slash like `\n` in the format string are automatically unespcaed.
+
+### Exit status
+
+`actionlint` command exits with one of the following exit statuses.
+
+| Status | Description |
+|--------|---------------------------------------------------------|
+| `0` | The command ran successfully and no problem was found |
+| `1` | The command ran successfully and some problem was found |
+| `2` | The command failed due to invalid command line option |
+| `3` | The command failed due to some fatal error |
+
+
+## Use actionlint on GitHub Actions
+
+Preparing `actionlint` executable with the download script is recommended. See [the instruction](install.md#download-script) for
+more details. It sets an absolute file path of downloaded executable to `executable` output in order to use the executable in the
+following steps easily.
+
+Here is an example of simple workflow to run actionlint on GitHub Actions. Please ensure `shell: bash` since the default
+shell for Windows runners is `pwsh`.
+
+```yaml
+name: Lint GitHub Actions workflows
+on: [push, pull_request]
+
+jobs:
+ actionlint:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Download actionlint
+ id: get_actionlint
+ run: bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash)
+ shell: bash
+ - name: Check workflow files
+ run: ${{ steps.get_actionlint.outputs.executable }} -color
+ shell: bash
+```
+
+Or simply download the executable and run it in one step:
+
+```yaml
+- name: Check workflow files
+ run: |
+ bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash)
+ ./actionlint -color
+ shell: bash
+```
+
+The download script allows to specify the version of actionlint and the download directory. Try to give `--help` argument
+to the script for more usage details.
+
+If you want to enable [shellcheck integration](checks.md#check-shellcheck-integ), install `shellcheck` command. Note that
+shellcheck is [pre-installed on Ubuntu worker][preinstall-ubuntu].
+
+If you want to [annotate errors][ga-annotate-error] from actionlint on GitHub, consider to use
+[Problem Matchers](#problem-matchers).
+
+If you prefer Docker image to running a downloaded executable, using [actionlint Docker image](#docker) is another option.
+
+```yaml
+name: Lint GitHub Actions workflows
+on: [push, pull_request]
+
+jobs:
+ actionlint:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Check workflow files
+ uses: docker://rhysd/actionlint:latest
+ with:
+ args: -color
+```
+
+## Online playground
+
+Thanks to WebAssembly, actionlint playground is available on your browser. It never sends any data to outside of your browser.
+
+https://rhysd.github.io/actionlint/
+
+Paste your workflow content to the code editor at left pane. It automatically shows the results at right pane. When editing
+the workflow content in the code editor, the results will be updated on the fly. Clicking an error message in the results
+table moves a cursor to position of the error in the code editor.
+
+
+## [Docker][docker] image
+
+[Official Docker image][docker-image] is available. The image contains `actionlint` executable and all dependencies (shellcheck
+and pyflakes).
+
+Available tags are:
+
+- `actionlint:latest`: Latest stable version of actionlint. This image is recommended.
+- `actionlint:{version}`: Specific version of actionlint. (e.g. `actionlint:1.7.1`)
+
+Just run the image with `docker run`:
+
+```sh
+docker run --rm rhysd/actionlint:latest -version
+```
+
+To check all workflows in your repository, mount your repository's root directory as a volume and run actionlint in the mounted
+directory. When you are at a root directory of your repository:
+
+```sh
+docker run --rm -v $(pwd):/repo --workdir /repo rhysd/actionlint:latest -color
+```
+
+To check a file with actionlint in a Docker container, pass the file content via stdin and use `-` argument:
+
+```sh
+cat /path/to/workflow.yml | docker run --rm -i rhysd/actionlint:latest -color -
+```
+
+Or mount the workflows directory and pass the paths as arguments:
+
+```sh
+docker run --rm -v /path/to/workflows:/workflows rhysd/actionlint:latest -color /workflows/ci.yml
+```
+
+## Using actionlint from Go program
+
+Go APIs are available. See [the Go API document](api.md) for more details.
+
+
+
+## Tools integration
+
+### reviewdog
+
+[reviewdog][] is an automated review tool for various code hosting services. It officially [supports actionlint][reviewdog-actionlint].
+You can check errors from actionlint easily with inline review comments at pull request review.
+
+The usage is easy. Run `reviewdog/action-actionlint` action in your workflow as follows.
+
+```yaml
+name: reviewdog
+on: [pull_request]
+jobs:
+ actionlint:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: reviewdog/action-actionlint@v1
+```
+
+
+### Problem Matchers
+
+[Problem Matchers][problem-matchers] is a feature to extract GitHub Actions annotations from terminal outputs of linters.
+
+Copy [actionlint-matcher.json][actionlint-matcher] to `.github/actionlint-matcher.json` in your repository.
+
+Then enable the matcher using `add-matcher` command before running `actionlint` in the step of your workflow.
+
+```yaml
+- name: Check workflow files
+ run: |
+ echo "::add-matcher::.github/actionlint-matcher.json"
+ bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash)
+ ./actionlint -color
+ shell: bash
+```
+
+When you change your workflow and the changed line causes a new error, CI will annotate the diff with the extracted error message.
+
+
+
+### super-linter
+
+[super-linter][] is a Bash script for a simple combination of various linters, provided by GitHub. It has support for actionlint.
+Running super-linter in your repository automatically runs actionlint.
+
+To ignore some errors, please add `-ignore` option by using [`GITHUB_ACTIONS_COMMAND_ARGS` environment variable][super-linter-env-var].
+Please see [super-linter/super-linter#1852](https://github.com/super-linter/super-linter/issues/1852) for the discussion.
+
+### pre-commit
+
+[pre-commit][] is a framework for managing and maintaining multi-language Git pre-commit hooks. actionlint is available as a
+pre-commit hook to check workflow files in `.github/workflows/` directory.
+
+Add this to your `.pre-commit-config.yaml` in your repository:
+
+```yaml
+---
+repos:
+ - repo: https://github.com/rhysd/actionlint
+ rev: v1.7.1
+ hooks:
+ - id: actionlint
+```
+
+As alternatives to `actionlint` hook, `actionlint-docker` or `actionlint-system` hooks are available.
+
+| Hook ID | Explanation |
+|-|-|
+| `actionlint` | Automatically installs `actionlint` command in isolated `$GOPATH` directory using [Go toolchain][go-install]. |
+| `actionlint-docker` | Automatically pulls [the actionlint Docker image](#docker). |
+| `actionlint-system` | Uses system-installed `actionlint` command. The command is necessary to be [installed manually](install.md). |
+
+### VS Code
+
+[Linter extension][vsc-extension] for [VS Code][vscode] is available. The extension automatically detects `.github/workflows`
+directory, runs `actionlint` command, and reports errors in the code editor while editing workflow files.
+
+### Emacs
+
+Plugins for both [Flycheck][emacs-flycheck] and [Flymake][emacs-flymake] are available via [MELPA][emacs-melpa].
+
+Their respective repositories are [flycheck-actionlint][emacs-flycheck-extension] and [flymake-actionlint][emacs-flymake-extension].
+
+### Vim and Neovim
+
+[nvim-lint][] supports actionlint on Neovim. The plugin automatically and asynchronously runs actionlint and notifies errors
+on the fly when you edit GitHub Actions CI workflows. Please read the plugin's documentation for more details.
+
+[ALE][vim-ale] supports actionlint on Vim and Neovim. Similar to nvim-lint, The plugin automatically and asynchronously runs
+actionlint and notifies errors on the fly when you edit GitHub Actions CI workflows. Please read the plugin's documentation for
+more details.
+
+### Pulsar Edit
+
+A [Linter package][pulsar-linter] for [Pulsar Edit][pulsar] is available. The package automatically detects a `workflows`
+directory, executes the `actionlint` command on any detected GitHub Actions files within the directory, and reports returned
+information in the code editor display tab while editing workflow files.
+
+### Nova
+
+[Nova.app][nova] is a MacOS only editor and IDE. The [Actionlint for Nova][nova-extension] allows you to get inline feedback
+while editing actions.
+
+### trunk
+
+[trunk][trunk-io] is an extendable superlinter with a builtin language server and preexisting issue detection. Actionlint is
+integrated [here](https://github.com/trunk-io/plugins).
+
+Once you have [initialized trunk in your repo](https://docs.trunk.io/docs/check-get-started), to enable at the latest actionlint
+version, just run:
+
+```bash
+trunk check enable actionlint
+```
+
+or if you'd like a specific version:
+
+```bash
+trunk check enable actionlint@1.7.1
+```
+
+or modify `.trunk/trunk.yaml` in your repo to contain:
+
+```yaml
+lint:
+ enabled:
+ - actionlint@1.7.1
+```
+
+Then just run:
+
+```bash
+trunk check
+```
+
+and it will check your modified files via actionlint, if applicable, and show you the results. Trunk also will detect preexisting
+issues and highlight only the newly added actionlint issues. For more information, check the [trunk docs][trunk-docs].
+
+You can also see actionlint issues inline in VS Code via the [Trunk VS Code extension][trunk-vscode].
+
+---
+
+[Checks](checks.md) | [Installation](install.md) | [Configuration](config.md) | [Go API](api.md) | [References](reference.md)
+
+[reviewdog-actionlint]: https://github.com/reviewdog/action-actionlint
+[reviewdog]: https://github.com/reviewdog/reviewdog
+[cmd-manual]: https://rhysd.github.io/actionlint/usage.html
+[re2]: https://golang.org/s/re2syntax
+[go-template]: https://pkg.go.dev/text/template
+[jsonl]: https://jsonlines.org/
+[ga-annotate-error]: https://docs.github.com/en/actions/learn-github-actions/workflow-commands-for-github-actions#setting-an-error-message
+[sarif]: https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html
+[problem-matchers]: https://github.com/actions/toolkit/blob/master/docs/problem-matchers.md
+[super-linter]: https://github.com/github/super-linter
+[super-linter-env-var]: https://github.com/super-linter/super-linter#environment-variables
+[actionlint-matcher]: https://raw.githubusercontent.com/rhysd/actionlint/main/.github/actionlint-matcher.json
+[preinstall-ubuntu]: https://github.com/actions/virtual-environments/blob/main/images/linux/Ubuntu2004-README.md
+[pre-commit]: https://pre-commit.com
+[go-install]: https://go.dev/doc/install
+[docker]: https://www.docker.com/
+[docker-image]: https://hub.docker.com/r/rhysd/actionlint
+[vsc-extension]: https://marketplace.visualstudio.com/items?itemName=arahata.linter-actionlint
+[vscode]: https://code.visualstudio.com/
+[emacs-melpa]: https://melpa.org/
+[emacs-flymake]: https://www.gnu.org/software/emacs/manual/html_node/flymake/
+[emacs-flymake-extension]: https://github.com/ROCKTAKEY/flymake-actionlint
+[emacs-flycheck]: https://www.flycheck.org/
+[emacs-flycheck-extension]: https://github.com/tirimia/flycheck-actionlint
+[nvim-lint]: https://github.com/mfussenegger/nvim-lint
+[vim-ale]: https://github.com/dense-analysis/ale
+[pulsar]: https://pulsar-edit.dev/
+[pulsar-linter]: https://web.pulsar-edit.dev/packages/linter-github-actions
+[nova-extension]: https://extensions.panic.com/extensions/org.netwrk/org.netwrk.actionlint/
+[nova]: https://nova.app
+[trunk-io]: https://docs.trunk.io/docs
+[trunk-docs]: https://docs.trunk.io/docs/check
+[trunk-vscode]: https://marketplace.visualstudio.com/items?itemName=trunk.io
diff --git a/docs/agent_assignment_verification_plan.md b/docs/archive/plans/agent_assignment_verification_plan.md
similarity index 100%
rename from docs/agent_assignment_verification_plan.md
rename to docs/archive/plans/agent_assignment_verification_plan.md
diff --git a/docs/agents-guard-plan.md b/docs/archive/plans/agents-guard-plan.md
similarity index 100%
rename from docs/agents-guard-plan.md
rename to docs/archive/plans/agents-guard-plan.md
diff --git a/docs/autofix_workflow_plan.md b/docs/archive/plans/autofix_workflow_plan.md
similarity index 100%
rename from docs/autofix_workflow_plan.md
rename to docs/archive/plans/autofix_workflow_plan.md
diff --git a/docs/ci_reuse_consolidation_plan.md b/docs/archive/plans/ci_reuse_consolidation_plan.md
similarity index 100%
rename from docs/ci_reuse_consolidation_plan.md
rename to docs/archive/plans/ci_reuse_consolidation_plan.md
diff --git a/docs/coverage_progress.md b/docs/archive/plans/coverage_progress.md
similarity index 100%
rename from docs/coverage_progress.md
rename to docs/archive/plans/coverage_progress.md
diff --git a/docs/coverage_trend_plan.md b/docs/archive/plans/coverage_trend_plan.md
similarity index 100%
rename from docs/coverage_trend_plan.md
rename to docs/archive/plans/coverage_trend_plan.md
diff --git a/docs/efficient-validation.md b/docs/archive/plans/efficient-validation.md
similarity index 100%
rename from docs/efficient-validation.md
rename to docs/archive/plans/efficient-validation.md
diff --git a/docs/failure_tracker_gate_alignment_plan.md b/docs/archive/plans/failure_tracker_gate_alignment_plan.md
similarity index 100%
rename from docs/failure_tracker_gate_alignment_plan.md
rename to docs/archive/plans/failure_tracker_gate_alignment_plan.md
diff --git a/docs/failure_tracker_pr_scope_plan.md b/docs/archive/plans/failure_tracker_pr_scope_plan.md
similarity index 100%
rename from docs/failure_tracker_pr_scope_plan.md
rename to docs/archive/plans/failure_tracker_pr_scope_plan.md
diff --git a/docs/gate_protection_plan.md b/docs/archive/plans/gate_protection_plan.md
similarity index 100%
rename from docs/gate_protection_plan.md
rename to docs/archive/plans/gate_protection_plan.md
diff --git a/docs/health-40-repo-selfcheck-plan.md b/docs/archive/plans/health-40-repo-selfcheck-plan.md
similarity index 100%
rename from docs/health-40-repo-selfcheck-plan.md
rename to docs/archive/plans/health-40-repo-selfcheck-plan.md
diff --git a/docs/issue-2523-plan.md b/docs/archive/plans/issue-2523-plan.md
similarity index 100%
rename from docs/issue-2523-plan.md
rename to docs/archive/plans/issue-2523-plan.md
diff --git a/docs/issue-2528-doc-alignment-plan.md b/docs/archive/plans/issue-2528-doc-alignment-plan.md
similarity index 100%
rename from docs/issue-2528-doc-alignment-plan.md
rename to docs/archive/plans/issue-2528-doc-alignment-plan.md
diff --git a/docs/issue-2562-plan.md b/docs/archive/plans/issue-2562-plan.md
similarity index 100%
rename from docs/issue-2562-plan.md
rename to docs/archive/plans/issue-2562-plan.md
diff --git a/docs/issue-2566-plan.md b/docs/archive/plans/issue-2566-plan.md
similarity index 100%
rename from docs/issue-2566-plan.md
rename to docs/archive/plans/issue-2566-plan.md
diff --git a/docs/issue-2649-pr02-autofix-plan.md b/docs/archive/plans/issue-2649-pr02-autofix-plan.md
similarity index 100%
rename from docs/issue-2649-pr02-autofix-plan.md
rename to docs/archive/plans/issue-2649-pr02-autofix-plan.md
diff --git a/docs/issue-2683-branch-protection-plan.md b/docs/archive/plans/issue-2683-branch-protection-plan.md
similarity index 100%
rename from docs/issue-2683-branch-protection-plan.md
rename to docs/archive/plans/issue-2683-branch-protection-plan.md
diff --git a/docs/issue-2688-plan.md b/docs/archive/plans/issue-2688-plan.md
similarity index 100%
rename from docs/issue-2688-plan.md
rename to docs/archive/plans/issue-2688-plan.md
diff --git a/docs/issue-2963-progress.md b/docs/archive/plans/issue-2963-progress.md
similarity index 100%
rename from docs/issue-2963-progress.md
rename to docs/archive/plans/issue-2963-progress.md
diff --git a/docs/issue-3260-keepalive-validation-log.md b/docs/archive/plans/issue-3260-keepalive-validation-log.md
similarity index 100%
rename from docs/issue-3260-keepalive-validation-log.md
rename to docs/archive/plans/issue-3260-keepalive-validation-log.md
diff --git a/docs/issue-3260-progress.md b/docs/archive/plans/issue-3260-progress.md
similarity index 100%
rename from docs/issue-3260-progress.md
rename to docs/archive/plans/issue-3260-progress.md
diff --git a/docs/issue-3261-keepalive-detection-log.md b/docs/archive/plans/issue-3261-keepalive-detection-log.md
similarity index 100%
rename from docs/issue-3261-keepalive-detection-log.md
rename to docs/archive/plans/issue-3261-keepalive-detection-log.md
diff --git a/docs/issue-3646-positions-contract.md b/docs/archive/plans/issue-3646-positions-contract.md
similarity index 100%
rename from docs/issue-3646-positions-contract.md
rename to docs/archive/plans/issue-3646-positions-contract.md
diff --git a/docs/issue_1683_task_list.md b/docs/archive/plans/issue_1683_task_list.md
similarity index 100%
rename from docs/issue_1683_task_list.md
rename to docs/archive/plans/issue_1683_task_list.md
diff --git a/docs/issue_2812_task_list.md b/docs/archive/plans/issue_2812_task_list.md
similarity index 100%
rename from docs/issue_2812_task_list.md
rename to docs/archive/plans/issue_2812_task_list.md
diff --git a/docs/issue_2883_task_list.md b/docs/archive/plans/issue_2883_task_list.md
similarity index 100%
rename from docs/issue_2883_task_list.md
rename to docs/archive/plans/issue_2883_task_list.md
diff --git a/docs/issues-3260-3261-keepalive-log.md b/docs/archive/plans/issues-3260-3261-keepalive-log.md
similarity index 100%
rename from docs/issues-3260-3261-keepalive-log.md
rename to docs/archive/plans/issues-3260-3261-keepalive-log.md
diff --git a/docs/maint_post_ci_plan.md b/docs/archive/plans/maint_post_ci_plan.md
similarity index 100%
rename from docs/maint_post_ci_plan.md
rename to docs/archive/plans/maint_post_ci_plan.md
diff --git a/docs/post_ci_hygiene_plan.md b/docs/archive/plans/post_ci_hygiene_plan.md
similarity index 100%
rename from docs/post_ci_hygiene_plan.md
rename to docs/archive/plans/post_ci_hygiene_plan.md
diff --git a/docs/post_ci_summary_plan.md b/docs/archive/plans/post_ci_summary_plan.md
similarity index 100%
rename from docs/post_ci_summary_plan.md
rename to docs/archive/plans/post_ci_summary_plan.md
diff --git a/docs/repo-health-self-check-plan.md b/docs/archive/plans/repo-health-self-check-plan.md
similarity index 100%
rename from docs/repo-health-self-check-plan.md
rename to docs/archive/plans/repo-health-self-check-plan.md
diff --git a/docs/reusable_agents_plan.md b/docs/archive/plans/reusable_agents_plan.md
similarity index 100%
rename from docs/reusable_agents_plan.md
rename to docs/archive/plans/reusable_agents_plan.md
diff --git a/docs/selftest_81_reusable_ci_plan.md b/docs/archive/plans/selftest_81_reusable_ci_plan.md
similarity index 100%
rename from docs/selftest_81_reusable_ci_plan.md
rename to docs/archive/plans/selftest_81_reusable_ci_plan.md
diff --git a/docs/selftest_manual_plan.md b/docs/archive/plans/selftest_manual_plan.md
similarity index 100%
rename from docs/selftest_manual_plan.md
rename to docs/archive/plans/selftest_manual_plan.md
diff --git a/docs/test_coverage_plan.md b/docs/archive/plans/test_coverage_plan.md
similarity index 100%
rename from docs/test_coverage_plan.md
rename to docs/archive/plans/test_coverage_plan.md
diff --git a/docs/validation-performance-improvements.md b/docs/archive/plans/validation-performance-improvements.md
similarity index 100%
rename from docs/validation-performance-improvements.md
rename to docs/archive/plans/validation-performance-improvements.md
diff --git a/docs/validation-scripts.md b/docs/archive/plans/validation-scripts.md
similarity index 100%
rename from docs/validation-scripts.md
rename to docs/archive/plans/validation-scripts.md
diff --git a/docs/verification-log.md b/docs/archive/plans/verification-log.md
similarity index 100%
rename from docs/verification-log.md
rename to docs/archive/plans/verification-log.md
diff --git a/docs/workflow_renaming_plan.md b/docs/archive/plans/workflow_renaming_plan.md
similarity index 100%
rename from docs/workflow_renaming_plan.md
rename to docs/archive/plans/workflow_renaming_plan.md
diff --git a/docs/checks.md b/docs/checks.md
index 82fa9f379a..769908e402 100644
--- a/docs/checks.md
+++ b/docs/checks.md
@@ -1,2896 +1,58 @@
-All checks done by actionlint
-=============================
+# Actionlint Checks Reference
-This document describes all checks done by [actionlint](..) with example inputs, outputs, and playground links.
+This project uses [actionlint](https://github.com/rhysd/actionlint) for GitHub Actions workflow validation.
-List of checks:
+## Workflow
-- [Unexpected keys](#check-unexpected-keys)
-- [Missing required keys or key duplicates](#check-missing-required-duplicate-keys)
-- [Unexpected empty mappings](#check-empty-mapping)
-- [Unexpected mapping values](#check-mapping-values)
-- [Syntax check for expression `${{ }}`](#check-syntax-expression)
-- [Type checks for expression syntax in `${{ }}`](#check-type-check-expression)
-- [Contexts and built-in functions](#check-contexts-and-builtin-func)
-- [Contextual typing for `steps.` objects](#check-contextual-step-object)
-- [Contextual typing for `matrix` object](#check-contextual-matrix-object)
-- [Contextual typing for `needs` object](#check-contextual-needs-object)
-- [Strict type checks for comparison operators](#check-comparison-types)
-- [shellcheck integration for `run:`](#check-shellcheck-integ)
-- [pyflakes integration for `run:`](#check-pyflakes-integ)
-- [Script injection by potentially untrusted inputs](#untrusted-inputs)
-- [Job dependencies validation](#check-job-deps)
-- [Matrix values](#check-matrix-values)
-- [Webhook events validation](#check-webhook-events)
-- [Workflow dispatch event validation](#check-workflow-dispatch-events)
-- [Glob filter pattern syntax validation](#check-glob-pattern)
-- [CRON syntax check at `schedule:`](#check-cron-syntax)
-- [Runner labels](#check-runner-labels)
-- [Action format in `uses:`](#check-action-format)
-- [Local action inputs validation at `with:`](#check-local-action-inputs)
-- [Popular action inputs validation at `with:`](#check-popular-action-inputs)
-- [Outdated popular actions detection at `with:`](#detect-outdated-popular-actions)
-- [Shell name validation at `shell:`](#check-shell-names)
-- [Job ID and step ID uniqueness](#check-job-step-ids)
-- [Hardcoded credentials](#check-hardcoded-credentials)
-- [Environment variable names](#check-env-var-names)
-- [Permissions](#permissions)
-- [Reusable workflows](#check-reusable-workflows)
-- [ID naming convention](#id-naming-convention)
-- [Contexts and special functions availability](#ctx-spfunc-availability)
-- [Deprecated workflow commands](#check-deprecated-workflow-commands)
-- [Conditions always evaluated to true at `if:`](#if-cond-always-true)
-- [Action metadata syntax validation](#action-metadata-syntax)
+**File**: `.github/workflows/health-42-actionlint.yml`
-Note that actionlint focuses on catching mistakes in workflow files. If you want some general code style checks, please consider
-using a general YAML checker like [yamllint][].
+The actionlint workflow runs on pull requests to validate all workflow YAML files.
-
-## Unexpected keys
+## Documentation
-Example input:
+For the complete list of checks performed by actionlint, see:
+- [Actionlint Checks Documentation](https://github.com/rhysd/actionlint/blob/main/docs/checks.md)
+- [Actionlint Usage Guide](https://github.com/rhysd/actionlint#readme)
-```yaml
-on: push
-jobs:
- test:
- runs-on: ubuntu-latest
- # ERROR: Typo of `defaults:`
- default:
- run:
- working-directory: /path/to/dir
- steps:
- - run: echo hello
- # ERROR: `shell:` must be in lower case
- Shell: bash
-```
-
-Output:
-
-```
-test.yaml:6:5: unexpected key "default" for "job" section. expected one of "concurrency", "container", "continue-on-error", "defaults", "env", "environment", "if", "name", "needs", "outputs", "permissions", "runs-on", "secrets", "services", "steps", "strategy", "timeout-minutes", "uses", "with" [syntax-check]
- |
-6 | default:
- | ^~~~~~~~
-test.yaml:12:9: unexpected key "Shell" for "step" section. expected one of "continue-on-error", "env", "id", "if", "name", "run", "shell", "timeout-minutes", "uses", "with", "working-directory" [syntax-check]
- |
-12 | Shell: bash
- | ^~~~~~
-```
-
-[Playground](https://rhysd.github.io/actionlint#eJw9jEEOwyAMBO95xX4AcecbfQEkTkmLMMK2ov6+ATU92d6ZNdeAZpKXFycJC6AkOibQrYrji1uyquZKHGyijfZo5edN816Bk/v7qE+3HZ1W5f4J8C1q9sr+yqYnSk3uipt90JoZmUrh/6vHOANSlPwFtPsxjA==)
-
-[Workflow syntax][syntax-doc] defines what keys can be defined in which mapping object. When unknown key is defined, it makes
-the workflow run fail.
-
-actionlint can detect unexpected keys while parsing workflow syntax and report them as an error.
-
-Key names are basically case sensitive (though some specific key names are case insensitive). This check is useful to catch
-case-sensitivity mistakes.
-
-
-## Missing required keys and key duplicates
-
-Example input:
-
-```yaml
-on: push
-jobs:
- test:
- strategy:
- # ERROR: Matrix name is duplicated. These keys are case insensitive
- matrix:
- version_name: [v1, v2]
- VERSION_NAME: [V1, V2]
- # ERROR: runs-on is missing
- steps:
- - run: echo 'hello'
-```
-
-Output:
-
-```
-test.yaml:3:3: "runs-on" section is missing in job "test" [syntax-check]
- |
-3 | test:
- | ^~~~~
-test.yaml:8:9: key "version_name" is duplicated in "matrix" section. previously defined at line:7,col:9. note that key names are case insensitive [syntax-check]
- |
-8 | VERSION_NAME: [V1, V2]
- | ^~~~~~~~~~~~~
-```
-
-[Playground](https://rhysd.github.io/actionlint#eJzLz7NSKCgtzuDKyk8qtuJSUChJLS4B0QoKxSVFiSWp6ZUQnoJCbmJJUWYFjKegUJZaVJyZnxefl5ibaqUQXWaoo1BmFAuXDnMNCvb094v3c/R1BUqHAaXDoNLFJakFxTCDdBWKSoGOSE3OyFdQz0jNyclXBwA2byiy)
-
-Some mappings must include specific keys. For example, job mappings must include `runs-on:` and `steps:`.
-
-And duplicate keys are not allowed. In workflow syntax, comparing some keys is **case insensitive**. For example, the job ID
-`test` in lower case and the job ID `TEST` in upper case are not able to exist in the same workflow.
-
-actionlint checks these missing required keys and duplicate keys while parsing, and reports an error.
-
-
-## Unexpected empty mappings
-
-Example input:
-
-```yaml
-on: push
-jobs:
-```
-
-Output:
-
-```
-test.yaml:2:6: "jobs" section should not be empty. please remove this section if it's unnecessary [syntax-check]
- |
-2 | jobs:
- | ^
-```
-
-[Playground](https://rhysd.github.io/actionlint#eJzLz7NSKCgtzuDKyk8qtgIAJQsE6g==)
-
-Some mappings and sequences should not be empty. For example, `steps:` must include at least one step.
-
-actionlint checks such mappings and sequences are not empty while parsing, and reports the empty mappings and sequences as an
-error.
-
-
-## Unexpected mapping values
-
-Example input:
-
-```yaml
-on: push
-jobs:
- test:
- strategy:
- # ERROR: Boolean value "true" or "false" is expected
- fail-fast: off
- # ERROR: Integer value is expected
- max-parallel: 1.5
- runs-on: ubuntu-latest
- steps:
- - run: sleep 200
- # ERROR: Float value is expected
- timeout-minutes: two minutes
-```
-
-Output:
-
-```
-test.yaml:6:18: expecting a single ${{...}} expression or boolean literal "true" or "false", but found plain text node [syntax-check]
- |
-6 | fail-fast: off
- | ^~~
-test.yaml:8:21: expected scalar node for integer value but found scalar node with "!!float" tag [syntax-check]
- |
-8 | max-parallel: 1.5
- | ^~~
-test.yaml:13:26: expecting a single ${{...}} expression or float number literal, but found plain text node [syntax-check]
- |
-13 | timeout-minutes: two minutes
- | ^~~
-```
-
-[Playground](https://rhysd.github.io/actionlint#eJw1jssNAjEMRO9bxTQQtCBxSTdeyYGg/BTbArongXCy5o395Fo8msl9e9RD/AYoi84JiHZSvr1/CQgUkws0atQQFsz0co06pcTJ43y6fnm3Iq4OtR1W1FyiqV1WbvJXurnpIYm54bLvC48vYuZq6nIsNk499FmxwgdsuTVm)
-
-Some mapping values are restricted to some constant strings. Several mapping values expect boolean value like `true` or
-`false`. And some mapping values expect integer or floating number values.
-
-actionlint checks such constant strings are used properly while parsing and reports an error when an unexpected value is
-specified.
-
-
-## Syntax check for expression `${{ }}`
-
-Example input:
-
-```yaml
-on: push
-jobs:
- test:
- runs-on: ubuntu-latest
- steps:
- # " is not available for string literal delimiter
- - run: echo '${{ "hello" }}'
- # + operator does not exist
- - run: echo '${{ 1 + 1 }}'
- # Missing ')' paren
- - run: echo "${{ toJson(hashFiles('**/lock', '**/cache/') }}"
- # unexpected end of input
- - run: echo '${{ github.event. }}'
-```
-
-Output:
-
-```
-test.yaml:7:24: got unexpected character '"' while lexing expression, expecting 'a'..'z', 'A'..'Z', '_', '0'..'9', ''', '}', '(', ')', '[', ']', '.', '!', '<', '>', '=', '&', '|', '*', ',', ' '. do you mean string literals? only single quotes are available for string delimiter [expression]
- |
-7 | - run: echo '${{ "hello" }}'
- | ^~~~~~~
-test.yaml:9:26: got unexpected character '+' while lexing expression, expecting 'a'..'z', 'A'..'Z', '_', '0'..'9', ''', '}', '(', ')', '[', ']', '.', '!', '<', '>', '=', '&', '|', '*', ',', ' ' [expression]
- |
-9 | - run: echo '${{ 1 + 1 }}'
- | ^
-test.yaml:11:65: unexpected end of input while parsing arguments of function call. expecting ",", ")" [expression]
- |
-11 | - run: echo "${{ toJson(hashFiles('**/lock', '**/cache/') }}"
- | ^~~
-test.yaml:13:38: unexpected end of input while parsing object property dereference like 'a.b' or array element dereference like 'a.*'. expecting "IDENT", "*" [expression]
- |
-13 | - run: echo '${{ github.event. }}'
- | ^~~
-```
-
-[Playground](https://rhysd.github.io/actionlint#eJx1jTEOwjAMRfeewoqQUgptxZoDMHCLJLJwIYor7LBUvTsNrHT48pfekz9nB3MRah4cxDUAiqL1ArxKlp43XkLJWvrkK/siUZzlZwH01XSAkRjsYVnAEKbEBtbV7ikXOG35L5gqKN+Ec0te6DollNZ23Zg4Pu0Zao0+Eo72uP0weyP3SamEAd+YdahjH8ffRDM=)
-
-actionlint lexes and parses expression in `${{ }}` following [the expression syntax document][expr-doc]. It can detect
-many syntax errors like invalid characters, missing parens, unexpected end of input, ...
-
-
-## Type checks for expression syntax in `${{ }}`
-
-actionlint checks types of expressions in `${{ }}` placeholders of templates. The following types are supported by the type
-checker.
-
-| Type | Description | Notation |
-|---------------|--------------------------------------------------------------------------------------------|--------------------------|
-| Any | Any value like `any` type in TypeScript. Fallback type when a value can no longer be typed | `any` |
-| Number | Number value (integer or float) | `number` |
-| Bool | Boolean value | `bool` |
-| String | String value | `string` |
-| Null | Type of `null` value | `null` |
-| Array | Array of specific type elements | `array` |
-| Loose object | Object which can contain any properties | `object` |
-| Strict object | Object whose properties are strictly typed | `{prop1: T1, prop2: T2}` |
-| Map object | Object who has specific type values like `env` context | `{string => T}` |
-
-Type check by actionlint is more strict than GitHub Actions runtime.
-
-- Only `any` and `number` are allowed to be converted to string implicitly
-- Implicit conversion to `number` is not allowed
-- Object, array, and null are not allowed to be evaluated at `${{ }}`
-
-Example input:
-
-```yaml
-on: push
-jobs:
- test:
- runs-on: ubuntu-latest
- steps:
- # ERROR: `env` is object. Index access object is invalid
- - run: echo '${{ env[0] }}'
- # ERROR: Properties in objects are strongly typed. Missing property can be caught
- - run: echo '${{ job.container.os }}'
- # ERROR: `github.repository` is string. Trying to access .owner property is invalid
- - run: echo '${{ github.repository.owner }}'
- # ERROR: Objects, arrays and null should not be evaluated at ${{ }} since the outputs are useless
- - run: echo '${{ env }}'
-```
-
-Output:
-
-```
-test.yaml:7:28: property access of object must be type of string but got "number" [expression]
- |
-7 | - run: echo '${{ env[0] }}'
- | ^~
-test.yaml:9:24: property "os" is not defined in object type {id: string; network: string} [expression]
- |
-9 | - run: echo '${{ job.container.os }}'
- | ^~~~~~~~~~~~~~~~
-test.yaml:11:24: receiver of object dereference "owner" must be type of object but got "string" [expression]
- |
-11 | - run: echo '${{ github.repository.owner }}'
- | ^~~~~~~~~~~~~~~~~~~~~~~
-test.yaml:13:20: object, array, and null values should not be evaluated in template with ${{ }} but evaluating the value of type {string => string} [expression]
- |
-13 | - run: echo '${{ env }}'
- | ^~~
-```
-
-[Playground](https://rhysd.github.io/actionlint#eJx9jrEKAjEQRPv7iimEqxKs8yticTkWE5FsyO4qcty/m2jtVVO8N8xwCagmabpzlDABSqIjgWZFHHdu0YqaeyyDfZEoVflZgBtmAK2JMZ+2DVSel/MV+z7/M/qYX7nokgs1z3Lk3rImi75RZcnK7e351VtHlX5g4A+nCkLw)
-
-Type checks for expression syntax in `${{ }}` are done by semantics checker. Note that actual type checks by GitHub Actions
-runtime is loose.
-
-Any object value can be assigned into string value as string `'Object'`. `echo '${{ env }}'` will be replaced with
-`echo 'Object'`. And an array can also be converted into `'Array'` string. Such loose conversions are bugs in almost all cases.
-actionlint checks types more strictly. actionlint checks values evaluated at `${{ }}` are not object (replaced with string
-`'Object'`), array (replaced with string `'Array'`), nor null (replaced with string `''`). If you want to check a content of
-object or array, use `toJSON()` function.
-
-```
-echo '${{ toJSON(github.event) }}'
-```
-
-There are two object types internally. One is an object which is strict for properties, which causes a type error when trying to
-access unknown properties. And another is an object which is not strict for properties, which allows to access unknown properties.
-In the case, accessing unknown property is typed as `any`.
-
-When the type check cannot be done statically, the type is deduced to `any` (e.g. return type of `toJSON()`).
-
-As special case of `${{ }}`, it can be used for expanding object and array values.
-
-Example input:
-
-```yaml
-on: push
-jobs:
- test:
- strategy:
- matrix:
- env_string:
- - 'FOO=BAR'
- - 'FOO=PIYO'
- env_object:
- - FOO: BAR
- - FOO: PIYO
- runs-on: ubuntu-latest
- steps:
- # OK: Expanding object at 'env:' section
- - run: echo "$FOO"
- env: ${{ matrix.env_object }}
- # ERROR: String value cannot be expanded as object
- - run: echo "$FOO"
- env: ${{ matrix.env_string }}
-```
-
-Output:
-
-```
-test.yaml:19:14: type of expression at "env" must be object but found type string [expression]
- |
-19 | env: ${{ matrix.env_string }}
- | ^~~
-```
-
-[Playground](https://rhysd.github.io/actionlint#eJydkM0KgzAQhO8+xSCCp/QBAj20h0JPFm89lUSCP7SJmE1pEd+9SVWseOtp2WHm20mM5midraLGSMsjgJSlMAFLnSBVvscNeAjq6te8AUo/b95T63LRAIb0lGX74yFPt+rlfM3SFcDIRhW0BngnhwdsxZD/qp3Tlhnf3UmnybG7CL2n2qq1M5AFJ4cqKoM48Yz49zpH0vfTu3ZLGwzDf/HxN3z8A4EEWVQ=)
-
-In above example, environment variables mapping is expanded at `env:` section. actionlint checks type of the expanded value.
-
-
-## Contexts and built-in functions
-
-Example input:
-
-```yaml
-on: push
-jobs:
- test:
- runs-on: ubuntu-latest
- steps:
- # Access undefined context
- - run: echo '${{ unknown_context }}'
- # Access undefined property of context
- - run: echo '${{ github.events }}'
- # Calling undefined function (start's'With is correct)
- - run: echo "${{ startWith('hello, world', 'lo,') }}"
- # Wrong number of arguments
- - run: echo "${{ startsWith('hello, world') }}"
- # Wrong type of parameter
- - run: echo "${{ startsWith('hello, world', github.event) }}"
- # Function overloads can be handled properly. contains() has string version and array version
- - run: echo "${{ contains('hello, world', 'lo,') }}"
- - run: echo "${{ contains(github.event.labels.*.name, 'enhancement') }}"
- # format() has a special check for formatting string
- - run: echo "${{ format('{0}{1}', 1, 2, 3) }}"
-```
-
-Output:
-
-```
-test.yaml:7:24: undefined variable "unknown_context". available variables are "env", "github", "job", "matrix", "needs", "runner", "secrets", "steps", "strategy", "vars" [expression]
- |
-7 | - run: echo '${{ unknown_context }}'
- | ^~~~~~~~~~~~~~~
-test.yaml:9:24: property "events" is not defined in object type {workspace: string; env: string; event_name: string; event_path: string; ...} [expression]
- |
-9 | - run: echo '${{ github.events }}'
- | ^~~~~~~~~~~~~
-test.yaml:11:24: undefined function "startWith". available functions are "always", "cancelled", "contains", "endswith", "failure", "format", "fromjson", "hashfiles", "join", "startswith", "success", "tojson" [expression]
- |
-11 | - run: echo "${{ startWith('hello, world', 'lo,') }}"
- | ^~~~~~~~~~~~~~~~~
-test.yaml:13:24: number of arguments is wrong. function "startsWith(string, string) -> bool" takes 2 parameters but 1 arguments are given [expression]
- |
-13 | - run: echo "${{ startsWith('hello, world') }}"
- | ^~~~~~~~~~~~~~~~~~
-test.yaml:15:51: 2nd argument of function call is not assignable. "object" cannot be assigned to "string". called function type is "startsWith(string, string) -> bool" [expression]
- |
-15 | - run: echo "${{ startsWith('hello, world', github.event) }}"
- | ^~~~~~~~~~~~~
-test.yaml:20:24: format string "{0}{1}" does not contain placeholder {2}. remove argument which is unused in the format string [expression]
- |
-20 | - run: echo "${{ format('{0}{1}', 1, 2, 3) }}"
- | ^~~~~~~~~~~~~~~~
-```
-
-[Playground](https://rhysd.github.io/actionlint#eJydkNGKwjAQRd/9ikGEuJIWdd/6Iz5KWmeNazojnYkKpf9uorAorH3wKYR7zs0lTBWcovjJL9dSTQAURfMJ0EWSglMe60gai+Bydo9E8SQPCqDIZAXYeAYz63uIdCS+0LZhUrwqDIN5h+4P6mNd4hlJ5Q04zaCo63ST6LnxGAJbuHAXdsaCSRfzldzpqCv/yJ9Z9mX1aEf+AXcg+WD0n/r8WBlcjUHKRUmuxVSD5B012KZsvO6Hu9bp3PTLoV8NacHKwtrC9126AZ31neg=)
-
-[Contexts][contexts-doc] and [built-in functions][funcs-doc] are strongly typed. Typos in property access of contexts and
-function names can be checked. And invalid function calls like wrong number of arguments or type mismatch at parameter also
-can be checked thanks to type checker.
-
-The semantics checker can properly handle that
-
-- some functions are overloaded (e.g. `contains(str, substr)` and `contains(array, item)`)
-- some parameters are optional (e.g. `join(strings, sep)` and `join(strings)`)
-- some parameters are repeatable (e.g. `hashFiles(file1, file2, ...)`)
-
-In addition, `format()` function has a special check for placeholders in the first parameter which represents the formatting
-string.
-
-Note that context names and function names are case insensitive. For example, `toJSON` and `toJson` are the same function.
-
-
-## Contextual typing for `steps.` objects
-
-Example input:
-
-```yaml
-on: push
-jobs:
- test:
- runs-on: ubuntu-latest
- outputs:
- # Step outputs can be used in job outputs since this section is evaluated after all steps were run
- foo: '${{ steps.get_value.outputs.name }}'
- steps:
- # ERROR: Access undefined step outputs
- - run: echo '${{ steps.get_value.outputs.name }}'
- # Outputs are set here
- - run: echo "foo=value" >> "$GITHUB_OUTPUT"
- id: get_value
- # OK
- - run: echo '${{ steps.get_value.outputs.name }}'
- # OK
- - run: echo '${{ steps.get_value.conclusion }}'
- other:
- runs-on: ubuntu-latest
- steps:
- # ERROR: Access undefined step outputs. Step objects are job-local
- - run: echo '${{ steps.get_value.outputs.name }}'
-```
-
-Output:
-
-```
-test.yaml:10:24: property "get_value" is not defined in object type {} [expression]
- |
-10 | - run: echo '${{ steps.get_value.outputs.name }}'
- | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~
-test.yaml:22:24: property "get_value" is not defined in object type {} [expression]
- |
-22 | - run: echo '${{ steps.get_value.outputs.name }}'
- | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~
-```
-
-[Playground](https://rhysd.github.io/actionlint#eJytkEsOglAMRees4g5MGD0W0MS1GMAqGHwltHVC2Ls+Pg6MiTE66uCec9tUIqF3bbKLVEoZYKyWJjB41CCP3CuP5qErUzZH4ta76cIBJxFCvhtHqHGvxZntcCs752IFi1heGdOUz8IMbW5IewhcN/JFxYtHpGxhIZHAfTqJ5oJNANoj4dn7z/XvvFpi3bm2EldLrOHh42d/+80ddrSUCw==)
-
-Outputs of step can be accessed via `steps.` objects. The `steps` context is dynamic:
-
-- Accessing the outputs before running the step causes `null`
-- Outputs of steps only in the job can be accessed. It cannot access steps across jobs
-
-It is a common mistake to access the wrong step outputs since people often forget to fix placeholders on copying&pasting
-steps. actionlint can catch invalid accesses to step outputs and reports them as errors.
-
-When the outputs are set by popular actions, the outputs object is more strictly typed.
-
-Example input:
-
-```yaml
-on: push
-
-jobs:
- test:
- runs-on: ubuntu-latest
- steps:
- # ERROR: The step is not run yet at this point
- - run: echo ${{ steps.cache.outputs.cache-hit }}
- # actions/cache sets cache-hit output
- - uses: actions/cache@v3
- id: cache
- with:
- key: ${{ hashFiles('**/*.lock') }}
- path: ./packages
- # OK
- - run: echo ${{ steps.cache.outputs.cache-hit }}
- # ERROR: Typo at output name
- - run: echo ${{ steps.cache.outputs.cache_hit }}
-```
-
-Output:
-
-```
-test.yaml:8:23: property "cache" is not defined in object type {} [expression]
- |
-8 | - run: echo ${{ steps.cache.outputs.cache-hit }}
- | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-test.yaml:18:23: property "cache_hit" is not defined in object type {cache-hit: string} [expression]
- |
-18 | - run: echo ${{ steps.cache.outputs.cache_hit }}
- | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-```
-
-[Playground](https://rhysd.github.io/actionlint#eJyNTksKwjAQ3fcUbyFUC0nBZVauvIakMZjY0gRnokjp3W3TUl26Gt53XugVYiJXFPfQkCoAtsTzBR6pJxEmQ2pSz0l0etayRGwjLS5AIJElBW3Yh55qo42zp+dxlQF/Vcjkxrw8O7UhoLVvhd0wwGlyZ99Z2pdVVVeyC6YtDxjHH3PUUxiyjtq0+mZpmzENVrDGhVyVN8r8V4bEMfGKhPP8bfw7dlliH1xHWso=)
-
-In the above example, [actions/cache][actions-cache] action sets `cache-hit` output so that the following steps can know
-whether the cache was hit or not. At line 8, the cache action is not run yet. So `cache` property does not exist in the
-`steps` context yet. On running the step whose ID is `cache`, `steps.cache` object is typed as
-`{outputs: {cache-hit: any}, conclusion: string, outcome: string}`. At line 18, the expression has a typo in the output
-name. actionlint can check it because properties of `steps.cache.outputs` are typed.
-
-This strict typing for outputs is also applied to local actions. Let's say we have the following local action.
-
-```yaml
-name: 'My action with output'
-author: 'rhysd '
-description: 'my action with outputs'
-
-outputs:
- some_value:
- description: some value returned from this action
-
-runs:
- using: 'node20'
- main: 'index.js'
-```
-
-Example input:
-
-```yaml
-on: push
-
-jobs:
- test:
- runs-on: ubuntu-latest
- steps:
- # ERROR: The step is not yet run
- - run: echo ${{ steps.my_action.outputs.some_value }}
- # The action runs here and sets its outputs
- - uses: ./.github/actions/my-action-with-output
- id: my_action
- # OK
- - run: echo ${{ steps.my_action.outputs.some_value }}
- # ERROR: No output named 'some-value' (typo)
- - run: echo ${{ steps.my_action.outputs.some-value }}
-```
-
-Output:
-
-```
-test.yaml:8:23: property "my_action" is not defined in object type {} [expression]
- |
-8 | - run: echo ${{ steps.my_action.outputs.some_value }}
- | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-test.yaml:15:23: property "some-value" is not defined in object type {some_value: string} [expression]
- |
-15 | - run: echo ${{ steps.my_action.outputs.some-value }}
- | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-```
-
-The 'My action with output' action defines one output `some_value`. The property is typed at `steps.my_action.outputs` object
-so that actionlint can check incorrect property accesses like a typo in the output name.
-
-
-## Contextual typing for `matrix` object
-
-Example input:
-
-```yaml
-on: push
-jobs:
- test:
- strategy:
- matrix:
- os: [ubuntu-latest, windows-latest]
- node: [14, 15]
- package:
- - name: 'foo'
- optional: true
- - name: 'bar'
- optional: false
- include:
- - node: 15
- npm: 7.5.4
- runs-on: ${{ matrix.os }}
- steps:
- # Access undefined matrix value
- - run: echo '${{ matrix.platform }}'
- # Matrix value is strongly typed. Below line causes an error since matrix.package is {name: string, optional: bool}
- - run: echo '${{ matrix.package.dev }}'
- # OK
- - run: |
- echo 'os: ${{ matrix.os }}'
- echo 'node version: ${{ matrix.node }}'
- echo 'package: ${{ matrix.package.name }} (optional=${{ matrix.package.optional }})'
- # Additional matrix values in 'include:' are supported
- - run: echo 'npm version is specified'
- if: ${{ contains(matrix.npm, '7.5') }}
- test2:
- runs-on: ubuntu-latest
- steps:
- # Matrix values in other job is not accessible
- - run: echo '${{ matrix.os }}'
-```
-
-Output:
-
-```
-test.yaml:19:24: property "platform" is not defined in object type {os: string; node: number; package: {name: string; optional: bool}; npm: string} [expression]
- |
-19 | - run: echo '${{ matrix.platform }}'
- | ^~~~~~~~~~~~~~~
-test.yaml:21:24: property "dev" is not defined in object type {name: string; optional: bool} [expression]
- |
-21 | - run: echo '${{ matrix.package.dev }}'
- | ^~~~~~~~~~~~~~~~~~
-test.yaml:34:24: property "os" is not defined in object type {} [expression]
- |
-34 | - run: echo '${{ matrix.os }}'
- | ^~~~~~~~~
-```
-
-[Playground](https://rhysd.github.io/actionlint#eJyNUstuwyAQvOcr5lCJRIotpUpUCalfUvVAbJzQ2oBYSFql+fdCHOo8XLUntMsMOzuD0Rw20HbyZtbEJ4CX5NMJkHfCy81nXwGd8E595AowxPES1kH7ULQi8ebYK12bPZ3r1x+sNrWM6MVyjsVqaFtRvYuNHN4ECmjRRSxrjGEX/TjPemW0aDm8C3KMshbuN0ojWho4SldtqG/nnjQuVlcvaNtxPJWrcnlqu6CpMNGzh8PhbEhpCMfj2TFpKT9aJDCHrLYG7AJuozeNcV0ksb+gvT1lLXf36K8LnT0zBXKri92h0prYSUfqZo/TxRgjp4QRacn5SMI0W/08Asp3ETgb3Tm6nCVBEcjKSjVK1oMW1fTjK6O9UJqmWbTt5mAxIDbrU0j/7pFfh3X1Sf+fVG/gN6xO5N4=)
-
-Types of `matrix` context are contextually checked by the semantics checker. Type of matrix values in `matrix:` section
-is deduced from element values of its array. When the matrix value is an array of objects, objects' properties are checked
-strictly like `package.name` in above example.
-
-When a type of the array elements is not persistent, the type of the matrix value falls back to `any`.
-
-```yaml
-strategy:
- matrix:
- foo:
- - 'string value'
- - 42
- - {aaa: true, bbb: null}
- bar:
- - [42]
- - [true]
- - [{aaa: true, bbb: null}]
- - []
-steps:
- # matrix.foo is any type value
- - run: echo ${{ matrix.foo }}
- # matrix.bar is array type value
- - run: echo ${{ matrix.bar[0] }}
- # ERROR: Array cannot be evaluated as string
- - run: echo ${{ matrix.bar }}
-```
-
-
-## Contextual typing for `needs` object
-
-Example input:
-
-```yaml
-on: push
-jobs:
- install:
- outputs:
- installed: '...'
- runs-on: ubuntu-latest
- steps:
- - run: echo 'install something'
- prepare:
- outputs:
- prepared: '...'
- runs-on: ubuntu-latest
- steps:
- - run: echo 'parepare something'
- # ERROR: Outputs in other job is not accessible
- - run: echo '${{ needs.prepare.outputs.prepared }}'
- build:
- needs: [install, prepare]
- outputs:
- built: '...'
- runs-on: ubuntu-latest
- steps:
- # OK: Accessing job results
- - run: echo 'build something with ${{ needs.install.outputs.installed }} and ${{ needs.prepare.outputs.prepared }}'
- # ERROR: Accessing undefined output causes an error
- - run: echo '${{ needs.install.outputs.foo }}'
- # ERROR: Accessing undefined job ID
- - run: echo '${{ needs.some_job }}'
- other:
- runs-on: ubuntu-latest
- steps:
- # ERROR: Cannot access outputs across jobs
- - run: echo '${{ needs.build.outputs.built }}'
-```
-
-Output:
-
-```
-test.yaml:16:24: property "prepare" is not defined in object type {} [expression]
- |
-16 | - run: echo '${{ needs.prepare.outputs.prepared }}'
- | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-test.yaml:26:24: property "foo" is not defined in object type {installed: string} [expression]
- |
-26 | - run: echo '${{ needs.install.outputs.foo }}'
- | ^~~~~~~~~~~~~~~~~~~~~~~~~
-test.yaml:28:24: property "some_job" is not defined in object type {install: {outputs: {installed: string}; result: string}; prepare: {outputs: {prepared: string}; result: string}} [expression]
- |
-28 | - run: echo '${{ needs.some_job }}'
- | ^~~~~~~~~~~~~~
-test.yaml:33:24: property "build" is not defined in object type {} [expression]
- |
-33 | - run: echo '${{ needs.build.outputs.built }}'
- | ^~~~~~~~~~~~~~~~~~~~~~~~~
-```
-
-[Playground](https://rhysd.github.io/actionlint#eJylUs1uwjAMvvMUPiD1QvMAeRU0oZZ4S6cujmpHHBDvTh1CKjY0EBwiJfHn78cJBQsxsV99U892BTAElm4cdQtASWISvhxqDZ2FxhjT5OspBW5ppkl9CpLasRNkySUWjLW5VaQF3HuCpjAB0w+KH8KXcsUJYzfhXelSe19ZWXTdSv+BrY9HCIiOTVE2xdD17OB00s4+DaO7KGW8hW0Jt7ma/rgXSPvk7TRZfYkCh0E8LN6Lk+q9PuBsHrrg4OmY/wzot8gn0eMmtbyb/1xBknic7OtzWIjzRKqXPGXVOAPMHOsV)
-
-Job dependencies can be defined at [`needs:`][needs-doc]. A job runs after all jobs defined in `needs:` are done.
-Outputs from the jobs can be accessed only from jobs following them via [`needs` context][needs-context-doc].
-
-actionlint defines a type of `needs` variable contextually by looking at each job's `outputs:` section and `needs:` section.
-
-
-## Strict type checks for comparison operators
-
-```yaml
-on:
- workflow_call:
- inputs:
- timeout:
- type: boolean
-
-jobs:
- test:
- runs-on: ubuntu-latest
- steps:
- - run: echo 'called!'
- # ERROR: Comparing string to object is always evaluated to false
- if: ${{ github.event == 'workflow_call' }}
- - run: echo 'timeout is too long'
- # ERROR: Comparing boolean value with `>` doesn't make sense
- if: ${{ inputs.timeout > 60 }}
-```
-
-Output:
-
-```
-test.yaml:13:17: "object" value cannot be compared to "string" value with "==" operator [expression]
- |
-13 | if: ${{ github.event == 'workflow_call' }}
- | ^~~~~~~~~~~~
-test.yaml:16:17: "bool" value cannot be compared to "number" value with ">" operator [expression]
- |
-16 | if: ${{ inputs.timeout > 60 }}
- | ^~~~~~~~~~~~~~
-```
-
-Expressions in `${{ }}` placeholders support `==`, `!=`, `>`, `>=`, `<`, `<=` comparison operators. Arbitrary types of operands
-can be compared. When different type values are compared, they are implicitly converted to numbers before the comparison. Please
-see [the official document][operators-doc] to know the details of operators behavior.
-
-However, comparisons between some types are actually meaningless:
-
-- Objects and arrays are converted to `NaN`. Comparing an object or an array with other type is always evaluated to false.
-- Comparing booleans, null, objects, and arrays with `>`, `>=`, `<`, `<=` makes no sense.
-
-actionlint checks operands of comparison operators and reports errors in these cases.
-
-There are some additional surprising behaviors, but actioonlint allows them not to cause false positives as much as possible.
-
-- `0 == null`, `'0' == null`, `false == null` are true since they are implicitly converted to `0 == 0`
-- `'0' == false` and `0 == false` are true due to the same reason as above
-- Objects and arrays are only considered equal when they are the same instance
-
-
-## [shellcheck][] integration for `run:`
-
-Example input:
-
-```yaml
-on: push
-jobs:
- test:
- runs-on: ubuntu-latest
- steps:
- - run: echo $FOO
- test-win:
- runs-on: windows-latest
- steps:
- # Shell on Windows is PowerShell by default.
- # shellcheck is not run in this case.
- - run: echo $FOO
- # This script is run with bash due to 'shell:' configuration
- - run: echo $FOO
- shell: bash
-```
-
-Output:
-
-```
-test.yaml:6:9: shellcheck reported issue in this script: SC2086:info:1:6: Double quote to prevent globbing and word splitting [shellcheck]
- |
-6 | - run: echo $FOO
- | ^~~~
-test.yaml:14:9: shellcheck reported issue in this script: SC2086:info:1:6: Double quote to prevent globbing and word splitting [shellcheck]
- |
-14 | - run: echo $FOO
- | ^~~~
-```
-
-[shellcheck][] is a famous linter for ShellScript. actionlint runs shellcheck for scripts at `run:` step in a workflow.
-For installing shellcheck, see [the official installation document][shellcheck-install].
-
-actionlint detects which shell is used to run the scripts following [the documentation][shell-doc]. On Linux or macOS the
-default shell is `bash`, and on Windows it is `pwsh`. Shell can be configured by `shell:` configuration at a workflow
-level or job level. Each step can configure shell to run scripts by `shell:`.
-
-In the above example output, `SC2086:info:1:6:` means that shellcheck reported SC2086 rule violation and the location is at
-line 1, column 6. Note that the location is relative to the script of the `run:` section.
-
-actionlint remembers the default shell and checks what OS the job runs on. Only when the shell is `bash` or `sh`, actionlint
-applies shellcheck to scripts.
-
-By default, actionlint checks if `shellcheck` command exists in your system and uses it when it is found. The `-shellcheck`
-option on running `actionlint` command specifies the executable path of shellcheck. Setting empty string by `shellcheck=`
-disables shellcheck integration explicitly.
-
-Since both `${{ }}` expression syntax and ShellScript's variable access `$FOO` use `$`, the remaining `${{ }}` confuses
-shellcheck. To avoid it, actionlint replaces `${{ }}` with underscores. For example `echo '${{ matrix.os }}'` is replaced
-with `echo '________________'`.
-
-Some shellcheck rules conflict with the `${{ }}` expression syntax. To avoid errors due to the syntax, [SC1091][], [SC2050][],
-[SC2194][], [SC2154][], [SC2157][] are disabled.
-
-When what shell is used cannot be determined statically, actionlint assumes `shell: bash` optimistically. For example,
-
-```yaml
-strategy:
- matrix:
- os: [ubuntu-latest, macos-latest, windows-latest]
-runs-on: ${{ matrix.os }}
-steps:
- - name: Show file content
- run: Get-Content -Path xxx\yyy.txt
- if: ${{ matrix.os == 'windows-latest' }}
-```
-
-The 'Show file content' script is only run by `pwsh` due to `matrix.os == 'windows-latest'` guard. However actionlint does not
-know that. It checks the script with shellcheck and it'd probably cause a false-positive (due to file separator). This kind of
-false positives can be avoided by showing the shell name explicitly. It is also better in terms of maintenance of the workflow.
-
-```yaml
-- name: Show file content
- run: Get-Content -Path xxx\yyy.txt
- if: ${{ matrix.os == 'windows-latest' }}
- shell: pwsh
-```
-
-When you want to control shellcheck behavior, [`SHELLCHECK_OPTS` environment variable][shellcheck-env-var] is useful.
-
-From command line:
-
-```sh
-# Enable some optional rules
-SHELLCHECK_OPTS='--enable=avoid-nullary-conditions' actionlint
-
-# Disable some rules
-SHELLCHECK_OPTS='--exclude=SC2129' actionlint
-```
-
-On GitHub Actions:
-
-```yaml
-- run: actionlint
- env:
- SHELLCHECK_OPTS: --exclude=SC2129
-```
-
-
-## [pyflakes][] integration for `run:`
-
-Example input:
-
-```yaml
-on: push
-jobs:
- linux:
- runs-on: ubuntu-latest
- steps:
- # Yay! No error
- - run: print('${{ runner.os }}')
- shell: python
- # ERROR: Undefined variable
- - run: print(hello)
- shell: python
- linux2:
- runs-on: ubuntu-latest
- defaults:
- run:
- # Run script with Python by default
- shell: python
- steps:
- - run: |
- import sys
- for sys in ['system1', 'system2']:
- print(sys)
- - run: |
- from time import sleep
- print(100)
-```
-
-Output:
-
-```
-test.yaml:10:9: pyflakes reported issue in this script: 1:7: undefined name 'hello' [pyflakes]
- |
-10 | - run: print(hello)
- | ^~~~
-test.yaml:19:9: pyflakes reported issue in this script: 2:5: import 'sys' from line 1 shadowed by loop variable [pyflakes]
- |
-19 | - run: |
- | ^~~~
-test.yaml:23:9: pyflakes reported issue in this script: 1:1: 'time.sleep' imported but unused [pyflakes]
- |
-23 | - run: |
- | ^~~~
-```
-
-Python script can be written in `run:` when `shell: python` is configured.
-
-[pyflakes][] is a famous linter for Python. It is suitable for linting small code like scripts at `run:` since it focuses
-on finding mistakes (not a code style issue) and tries to make false positives as minimal as possible. Install pyflakes
-by `pip install pyflakes`.
-
-actionlint runs pyflakes for scripts at `run:` steps in a workflow and reports errors found by pyflakes. actionlint detects
-Python scripts in a workflow by checking `shell: python` at each step and `defaults:` configurations at workflows and jobs.
-
-By default, actionlint checks if `pyflakes` command exists in your system and uses it when found. The `-pyflakes` option
-of `actionlint` command allows to specify the executable path of pyflakes. Setting empty string by `pyflakes=` disables
-pyflakes integration explicitly.
-
-Since both `${{ }}` expression syntax is invalid as Python, remaining `${{ }}` might confuse pyflakes. To avoid it,
-actionlint replaces `${{ }}` with underscores. For example `print('${{ matrix.os }}')` is replaced with
-`print('________________')`.
-
-
-## Script injection by potentially untrusted inputs
-
-Example input:
-
-```yaml
-name: Test
-on: pull_request
-
-jobs:
- test:
- runs-on: ubuntu-latest
- steps:
- - name: Print pull request title
- # ERROR: Using the potentially untrusted input can cause script injection
- run: echo '${{ github.event.pull_request.title }}'
- - uses: actions/stale@v9
- with:
- repo-token: ${{ secrets.TOKEN }}
- # This is OK because action input is not evaluated by shell
- stale-pr-message: ${{ github.event.pull_request.title }} was closed
- - uses: actions/github-script@v7
- with:
- # ERROR: Using the potentially untrusted input can cause script injection
- script: console.log('${{ github.event.head_commit.author.name }}')
- - name: Get comments
- # ERROR: Accessing untrusted inputs via `.*` object filter; bodies of comment, review, and review_comment
- run: echo '${{ toJSON(github.event.*.body) }}'
-```
-
-Output:
-
-```
-test.yaml:10:24: "github.event.pull_request.title" is potentially untrusted. avoid using it directly in inline scripts. instead, pass it through an environment variable. see https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions for more details [expression]
- |
-10 | run: echo '${{ github.event.pull_request.title }}'
- | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-test.yaml:19:36: "github.event.head_commit.author.name" is potentially untrusted. avoid using it directly in inline scripts. instead, pass it through an environment variable. see https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions for more details [expression]
- |
-19 | script: console.log('${{ github.event.head_commit.author.name }}')
- | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-test.yaml:22:31: object filter extracts potentially untrusted properties "github.event.comment.body", "github.event.discussion.body", "github.event.issue.body", "github.event.pull_request.body", "github.event.review.body", "github.event.review_comment.body". avoid using the value directly in inline scripts. instead, pass the value through an environment variable. see https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions for more details [expression]
- |
-22 | run: echo '${{ toJSON(github.event.*.body) }}'
- | ^~~~~~~~~~~~~~~~~~~~
-```
-
-[Playground](https://rhysd.github.io/actionlint#eJyFkUFLAzEQhe/9FXMQ2gqJRzGnXkRQaAV7L9ns0F3NZtbMpEVK/7vJbilVKZ5CMm++93gJtkMDa2SZUDDQJ+83ET9TeZi8U8VmAiD5Vk6AmAKrIkxVCpKUt2U2jFiw51EFoCAM4NfYBhmocKKCtOLxJBuABtA1BNObwwG2rTSp0rjDIPoyjB7W4Hicnh0SIxuwTloKfMdiPS52D2fyPqPM+ZadsCcl9IHZsFgxuojCer16eVxm8IV0YKk+qg6Z7RbHhf+zwd4yOE+M9ZWUI0Oxi20vi9391bSjwoDLW+RRe9rO/jbUoK03jrquFW2TNBR16b3UNP/1E08oUJR5ja+1L/T8tlrOfljc6orqr3lBfgPRHa9N)
-
-Since `${{ }}` placeholders are evaluated and replaced directly by GitHub Actions runtime, you need to use them carefully in
-inline scripts at `run:`. For example, if we have step as follows,
-
-```yaml
-- run: echo 'issue ${{github.event.issue.title}}'
-```
-
-an attacker can create a new issue with the title `'; malicious_command ...`, and the inline script will run
-`echo 'issue'; malicious_command ...` in your workflow. The remediation of such script injection is passing potentially untrusted
-inputs via environment variables. See [the official document][security-doc] for more details.
-
-```yaml
-- run: echo "issue ${TITLE}"
- env:
- TITLE: ${{github.event.issue.title}}
-```
-
-actionlint recognizes the following inputs as potentially untrusted and checks your inline scripts at `run:`. When they are used
-directly in a script, actionlint will report it as an error.
-
-- `github.event.issue.title`
-- `github.event.issue.body`
-- `github.event.pull_request.title`
-- `github.event.pull_request.body`
-- `github.event.comment.body`
-- `github.event.review.body`
-- `github.event.review_comment.body`
-- `github.event.pages.*.page_name`
-- `github.event.commits.*.message`
-- `github.event.head_commit.message`
-- `github.event.head_commit.author.email`
-- `github.event.head_commit.author.name`
-- `github.event.commits.*.author.email`
-- `github.event.commits.*.author.name`
-- `github.event.pull_request.head.ref`
-- `github.event.pull_request.head.label`
-- `github.event.pull_request.head.repo.default_branch`
-- `github.head_ref`
-
-Not only direct access to the untrusted properties, actionlint also detects those properties indirectly accessed via
-[object filter syntax][object-filter-syntax]. For example, `github.event.*.body` collects all `body` properties in child objects
-of `github.event` as array. Those properties include untrusted inputs like `github.event.comment.body`,
-`github.event.pull_request.body`, ...
-
-```sh
-# Echo list of github.event.comment.body, github.event.pull_request.body, ...
-echo '${{ toJSON(github.event.*.body) }}'
-```
-
-Instead, you should store the JSON string in an environment variable:
-
-```sh
-- run: echo "${BODIES}"
- env:
- BODIES: '${{ toJSON(github.event.*.body) }}'
-```
-
-At last, the popular action [actions/github-script][github-script] has the same issue in its `script` input. actionlint also
-checks the input.
-
-
-## Job dependencies validation
-
-Example input:
-
-```yaml
-on: push
-jobs:
- prepare:
- needs: [build]
- runs-on: ubuntu-latest
- steps:
- - run: echo 'prepare'
- install:
- needs: [prepare]
- runs-on: ubuntu-latest
- steps:
- - run: echo 'install'
- build:
- needs: [install]
- runs-on: ubuntu-latest
- steps:
- - run: echo 'build'
-```
-
-Output:
-
-```
-test.yaml:8:3: cyclic dependencies in "needs" configurations of jobs are detected. detected cycle is "install" -> "prepare", "prepare" -> "build", "build" -> "install" [job-needs]
- |
-8 | install:
- | ^~~~~~~~
-```
-
-[Playground](https://rhysd.github.io/actionlint#eJyljjEOxCAMBPu8YjsqPsBXTikgsZREyCBs/z+Bo0mdzvJ4Z104oJocy1WShAWojWps1EeAiXYJ+CU7876OVTMWX56UJWM1n6OS6ECiVOUfBHy/DKDtKHBT6h52smjM+e2f/EPD1PaG8ezbP+kH/5C6G78nW+Q=)
-
-Job dependencies can be defined at [`needs:`][needs-doc]. If cyclic dependencies exist, jobs never start to run. actionlint
-detects cyclic dependencies in `needs:` sections of jobs and reports it as an error.
-
-actionlint also detects undefined jobs and duplicate jobs in `needs:` section.
-
-Example input:
-
-```yaml
-on: push
-jobs:
- foo:
- needs: [bar, BAR]
- runs-on: ubuntu-latest
- steps:
- - run: echo 'hi'
- bar:
- needs: [unknown]
- runs-on: ubuntu-latest
- steps:
- - run: echo 'hi'
-```
-
-Output:
-
-```
-test.yaml:4:18: job ID "BAR" duplicates in "needs" section. note that job ID is case insensitive [job-needs]
- |
-4 | needs: [bar, BAR]
- | ^~~~
-test.yaml:8:3: job "bar" needs job "unknown" which does not exist in this workflow [job-needs]
- |
-8 | bar:
- | ^~~~
-```
-
-[Playground](https://rhysd.github.io/actionlint#eJyljD0OgiEQRHtOMR2NXIDO7wi2xgJ0v+BPdgnLxusLWFlbTfJm5glHVNPiHpI1OmAXmQEw0U0jzjm1A7bj6bJoM9Yg42TZuFt4pU7aV6Wdqn6/QJjLCLoWgS93P/AQ/ZqNnyxv/k/8AXoNOHs=)
-
-
-## Matrix values
-
-Example input:
-
-```yaml
-on: push
-jobs:
- test:
- strategy:
- matrix:
- node: [10, 12, 14, 14]
- os: [ubuntu-latest, macos-latest]
- exclude:
- - node: 13
- os: ubuntu-latest
- - node: 10
- platform: ubuntu-latest
- runs-on: ${{ matrix.os }}
- steps:
- - run: echo ...
-```
-
-Output:
-
-```
-test.yaml:6:28: duplicate value "14" is found in matrix "node". the same value is at line:6,col:24 [matrix]
- |
-6 | node: [10, 12, 14, 14]
- | ^~~
-test.yaml:9:19: value "13" in "exclude" does not match in matrix "node" combinations. possible values are "10", "12", "14", "14" [matrix]
- |
-9 | - node: 13
- | ^~
-test.yaml:12:13: "platform" in "exclude" section does not exist in matrix. available matrix configurations are "node", "os" [matrix]
- |
-12 | platform: ubuntu-latest
- | ^~~~~~~~~
-```
-
-[Playground](https://rhysd.github.io/actionlint#eJxtkMEOgjAQRO9+xRw80gbUU3/FeACsooEu6bYJhvDvtqGgJBw2zezOvOyWjELvuTm8qWJ1AJxmF1+AnS2dfn5mBXSls69hUYChu1a4FnmG4hTqEuu2jonD0FfeOC/aMmKzgKiJk/o59VC3PrDWBiASvTj/NWfmBrkXyTeRPhgfZLu9oPWGBYUfOI5jOk8SY5rS/brnZSkRzQq6bghSyi9UwlNB)
-
-[`matrix:`][matrix-doc] defines combinations of multiple values. Nested `include:` and `exclude:` can add/remove specific
-combination of matrix values. actionlint checks
-
-- values in `exclude:` appear in `matrix:` or `include:`
-- duplicate variations of matrix values
-
-
-## Webhook events validation
-
-Example input:
-
-```yaml
-on:
- push:
- # ERROR: Incorrect filter. 'branches' is correct
- branch: foo
- # ERROR: Both 'paths' and 'paths-ignore' filters cannot be used for the same event
- paths: path/to/foo
- paths-ignore: path/to/foo
- issues:
- # ERROR: Incorrect type. 'opened' is correct
- types: created
- release:
- # ERROR: 'tags' filter is not available for 'release' event
- tags: v*.*.*
- # ERROR: Unknown event name
- pullreq:
-
-jobs:
- test:
- runs-on: ubuntu-latest
- steps:
- - run: echo ...
-```
-
-Output:
-
-```
-test.yaml:4:5: unexpected key "branch" for "push" section. expected one of "branches", "branches-ignore", "paths", "paths-ignore", "tags", "tags-ignore", "types", "workflows" [syntax-check]
- |
-4 | branch: foo
- | ^~~~~~~
-test.yaml:7:5: both "paths" and "paths-ignore" filters cannot be used for the same event "push". note: use '!' to negate patterns [events]
- |
-7 | paths-ignore: path/to/foo
- | ^~~~~~~~~~~~~
-test.yaml:10:12: invalid activity type "created" for "issues" Webhook event. available types are "assigned", "closed", "deleted", "demilestoned", "edited", "labeled", "locked", "milestoned", "opened", "pinned", "reopened", "transferred", "unassigned", "unlabeled", "unlocked", "unpinned" [events]
- |
-10 | types: created
- | ^~~~~~~
-test.yaml:13:5: "tags" filter is not available for release event. it is only for push event [events]
- |
-13 | tags: v*.*.*
- | ^~~~~
-test.yaml:15:3: unknown Webhook event "pullreq". see https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#webhook-events for list of all Webhook event names [events]
- |
-15 | pullreq:
- | ^~~~~~~~
-```
-
-[Playground](https://rhysd.github.io/actionlint#eJxdjkEOAyEIRfeegnUTnb23UUvHaYxYwCa9fdWZTRsWH3g/H6h6A9C65KkAkUNN2cODaM0taBa/ZFPaftb22Csx/tNDpKOccfppo4XEGBTvY8VYMAheNOwDvm9u1PqiFMaXN+ZJcQUoip5W7lUsVQ899qrdljDZQqLYrnMAdjo9YMoEzrkvdVRCRg==)
-
-At `on:`, Webhook events can be specified to trigger the workflow. [Webhook event documentation][webhook-doc] defines
-which Webhook events are available and what types can be specified at `types:` for each event.
-
-actionlint validates the Webhook configurations:
-
-- Webhook event name
-- types for Webhook event
-- filter names
-- filter usages
- - `paths` and `paths-ignore`, `branches` and `branches-ignore`, `tags` and `tags-ignore` are exclusive. They can not
- be used for the same event.
- - Some filters are only available for specific events as explained in [the official document][specific-paths-doc]
- (see the following table).
-
-| Filter name | Events where the filter is available |
-|-------------------|-----------------------------------------------|
-| `paths` | `push`, `pull_request`, `pull_request_target` |
-| `paths-ignore` | `push`, `pull_request`, `pull_request_target` |
-| `branches` | `push`, `pull_request`, `pull_request_target` |
-| `branches-ignore` | `push`, `pull_request`, `pull_request_target` |
-| `tags` | `push` |
-| `tags-ignore` | `push` |
-
-The table of available Webhooks and their types are defined in [`all_webhooks.go`](../all_webhooks.go). It is generated
-by [a script][generate-webhook-events] and kept to the latest by CI workflow triggered weekly.
-
-
-## Workflow dispatch event validation
-
-Example input:
-
-```yaml
-on:
- workflow_dispatch:
- inputs:
- # Unknown input type
- id:
- type: text
- # ERROR: No options for 'choice' input type
- kind:
- type: choice
- name:
- type: choice
- options:
- - Tama
- - Mike
- # ERROR: Default value is not in options
- default: Chobi
- message:
- type: string
- verbose:
- type: boolean
- # ERROR: Boolean value must be 'true' or 'false'
- default: yes
- age:
- type: number
- # ERROR: Number value must be parsed as a float number
- default: teen
-
-jobs:
- test:
- runs-on: ubuntu-latest
- steps:
- # ERROR: Undefined input
- - run: echo "${{ inputs.massage }}"
- # ERROR: Bool value is not available for object key
- - run: echo "${{ env[inputs.verbose] }}"
- # ERROR: Number value is not available for object key
- - run: echo "${{ env[inputs.age] }}"
- # ERROR: `github.event.inputs` is also not defined
- - run: echo "${{ github.event.inputs.massage }}"
-```
-
-Output:
-
-```
-test.yaml:6:15: input type of workflow_dispatch event must be one of "string", "number", "boolean", "choice", "environment" but got "text" [syntax-check]
- |
-6 | type: text
- | ^~~~
-test.yaml:8:7: input type of "kind" is "choice" but "options" is not set [events]
- |
-8 | kind:
- | ^~~~~
-test.yaml:16:18: default value "Chobi" of "name" input is not included in its options "\"Tama\", \"Mike\"" [events]
- |
-16 | default: Chobi
- | ^~~~~
-test.yaml:22:18: type of "verbose" input is "boolean". its default value "yes" must be "true" or "false" [events]
- |
-22 | default: yes
- | ^~~
-test.yaml:26:18: type of "age" input is "number" but its default value "teen" cannot be parsed as a float number: strconv.ParseFloat: parsing "teen": invalid syntax [events]
- |
-26 | default: teen
- | ^~~~
-test.yaml:33:24: property "massage" is not defined in object type {age: number; id: any; kind: string; message: string; name: string; verbose: bool} [expression]
- |
-33 | - run: echo "${{ inputs.massage }}"
- | ^~~~~~~~~~~~~~
-test.yaml:35:28: property access of object must be type of string but got "bool" [expression]
- |
-35 | - run: echo "${{ env[inputs.verbose] }}"
- | ^~~~~~~~~~~~~~~
-test.yaml:37:28: property access of object must be type of string but got "number" [expression]
- |
-37 | - run: echo "${{ env[inputs.age] }}"
- | ^~~~~~~~~~~
-test.yaml:39:24: property "massage" is not defined in object type {age: string; id: string; kind: string; message: string; name: string; verbose: string} [expression]
- |
-39 | - run: echo "${{ github.event.inputs.massage }}"
- | ^~~~~~~~~~~~~~~~~~~~~~~~~~~
-```
-
-[Playground](https://rhysd.github.io/actionlint#eJyNkcFugzAMQO98hVX1Ch+Q68697TZNUwIuZICNYoeuqvrvA5qtmrJVuznPz07sMJkC4MShPw58emu8TFbrboUAnqaocouXU/MVAeh5QgOKH5pQ7ylL1x37GhMkO+JDAYAn9UxytwBKeLaj/QEOvr+XNHi0cVADTx07n/CIIrbNbhMNntoEZwyOJXMc84CW8v5nlAR/6UxxdBjyIkWkonhnt82kKHqrDJGkZDIQXSSN5WDX3JYSxel7A+VqGsBlT7DbXy7pQ6rRbgPC9br7y0SaX5KdRn39p740fqi2XrvoKpyRtMof9AmbKqYK)
-
-[`workflow_dispatch`][workflow-dispatch-event] is an event to trigger a workflow manually. The event can have parameters called
-'inputs'. Each input has its name, description, default value, and [input type][workflow-dispatch-input-type-announce].
-
-actionlint checks several mistakes around `workflow_dispatch` configuration.
-
-- Input type must be one of 'choice', 'string', 'number', 'boolean', 'environment'
-- `options:` must be set for 'choice' input type
-- The default value of 'choice' input must be included in options
-- The default value of 'boolean' input must be `true` or `false`
-- The default value of 'number' input must be parsed as a float number
-
-In addition, `github.event.inputs` and `inputs` objects are typed based on the input definitions. Properties not defined in
-`inputs:` will cause a type error thanks to a type checker.
-
-For example,
-
-```yaml
-inputs:
- string_input:
- type: string
- choice_input:
- type: choice
- options: ['hello']
- bool_input:
- type: boolean
- num_input:
- type: number
- env_input:
- type: environment
- no_type_input:
-```
-
-`inputs` is typed as follows from these definitions:
-
-```
-{
- "string_input": string;
- "choice_input": string;
- "bool_input": bool;
- "num_input": number;
- "env_input": string;
- "no_type_input": any;
-}
-```
-
-`github.event.inputs` is typed as follows since all properties of it are strings unlike `inputs`:
-
-```
-{
- "string_input": string;
- "choice_input": string;
- "bool_input": string;
- "num_input": string;
- "env_input": string;
- "no_type_input": string;
-}
-```
-
-
-## Glob filter pattern syntax validation
-
-Example input:
-
-```yaml
-on:
- push:
- branches:
- # ^ is not available for branch name. This kind of mistake is usually caused by misunderstanding
- # that regular expression is available here
- - '^foo-'
- tags:
- # Invalid syntax. + cannot follow special character *
- - 'v*+'
- # Invalid character range 9-1
- - 'v[9-1]'
-
-jobs:
- test:
- runs-on: ubuntu-latest
- steps:
- - run: echo ...
-```
-
-Output:
-
-```
-test.yaml:6:10: character '^' is invalid for branch and tag names. ref name cannot contain spaces, ~, ^, :, [, ?, *. see `man git-check-ref-format` for more details. note that regular expression is unavailable. note: filter pattern syntax is explained at https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#filter-pattern-cheat-sheet [glob]
- |
-6 | - '^foo-'
- | ^~~~~~
-test.yaml:9:12: invalid glob pattern. unexpected character '+' while checking special character + (one or more). the preceding character must not be special character. note: filter pattern syntax is explained at https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#filter-pattern-cheat-sheet [glob]
- |
-9 | - 'v*+'
- | ^~
-test.yaml:11:14: invalid glob pattern. unexpected character '1' while checking character range in []. start of range '9' (57) is larger than end of range '1' (49). note: filter pattern syntax is explained at https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#filter-pattern-cheat-sheet [glob]
- |
-11 | - 'v[9-1]'
- | ^~~
-```
-
-[Playground](https://rhysd.github.io/actionlint#eJxNjEEKAjEQBO95Rd8CygQ8mq+IQrJEg8jMsjPj+3Wzl5yaoooWzgFYXfu+QN0KL73pQQAhPp4iFAdbec3mezrHiW5XutxjCG+po7KmdtSbs5Jwhldnc/qU3Q2l1tbp819mtKULUko/10snvA==)
-
-For filtering branches, tags and paths in Webhook events, [glob syntax][filter-pattern-doc] is available.
-actionlint validates glob patterns `branches:`, `branches-ignore:`, `tags:`, `tags-ignore:`, `paths:`, `paths-ignore:` in a
-workflow. It checks:
-
-- syntax errors like missing closing brackets for character range `[..]`
-- invalid usage like `?` following `*`, invalid character range `[9-1]`, ...
-- invalid character usage for Git ref names (branch name, tag name)
- - ref name cannot start/end with `/`
- - ref name cannot contain `[`, `:`, `\`, ...
-
-Most common mistake I have ever seen here is a misunderstanding that regular expression is available for filtering.
-This rule can catch the mistake so that users can notice their mistakes.
-
-
-## CRON syntax check at `schedule:`
-
-Example input:
-
-```yaml
-on:
- schedule:
- # ERROR: Cron syntax is not correct
- - cron: '0 */3 * *'
- # ERROR: Interval of scheduled job is too small (job runs too frequently)
- - cron: '* */3 * * *'
-
-jobs:
- test:
- runs-on: ubuntu-latest
- steps:
- - run: echo ...
-```
-
-Output:
-
-```
-test.yaml:4:13: invalid CRON format "0 */3 * *" in schedule event: expected exactly 5 fields, found 4: [0 */3 * *] [events]
- |
-4 | - cron: '0 */3 * *'
- | ^~
-test.yaml:6:13: scheduled job runs too frequently. it runs once per 60 seconds. the shortest interval is once every 5 minutes [events]
- |
-6 | - cron: '* */3 * * *'
- | ^~
-```
-
-[Playground](https://rhysd.github.io/actionlint#eJxVjEEKgDAMBO99xd6EQKvgrb/RGhAprTTN/zWKB2/LzuzWEh0gaedNM1sGPFKrJWKYQOMMAg3/nr7eiDvqKjbsLP09aFrEm6mrlq4+L8YeJJ1PeS07vM0ITntFCOEChKgjxA==)
-
-To trigger a workflow in specific interval, [scheduled event][schedule-event-doc] can be defined in [POSIX CRON syntax][cron-syntax].
-
-actionlint checks the CRON syntax and frequency of running a job. [The official document][schedule-event-doc] says:
-
-> The shortest interval you can run scheduled workflows is once every 5 minutes.
-
-When the job is run more frequently than once every 5 minutes, actionlint reports it as an error.
-
-
-## Runner labels
-
-Example input:
-
-```yaml
-on: push
-jobs:
- test:
- strategy:
- matrix:
- runner:
- # OK
- - macos-latest
- # ERROR: Unknown runner
- - linux-latest
- # OK: Preset labels for self-hosted runner
- - [self-hosted, linux, x64]
- # OK: Single preset label for self-hosted runner
- - arm64
- # ERROR: Unknown label "gpu". Custom label must be defined in actionlint.yaml config file
- - gpu
- runs-on: ${{ matrix.runner }}
- steps:
- - run: echo ...
-
- test2:
- # ERROR: Too old macOS worker
- runs-on: macos-10.13
- steps:
- - run: echo ...
-```
-
-Output:
-
-```
-test.yaml:10:13: label "linux-latest" is unknown. available labels are "windows-latest", "windows-2022", "windows-2019", "ubuntu-latest", ... [runner-label]
- |
-10 | - linux-latest
- | ^~~~~~~~~~~~
-test.yaml:16:13: label "gpu" is unknown. available labels are "windows-latest", "windows-2022", "windows-2019", "ubuntu-latest", ... [runner-label]
- |
-16 | - gpu
- | ^~~
-test.yaml:23:14: label "macos-10.13" is unknown. available labels are "windows-latest", "windows-2022", "windows-2019", "ubuntu-latest", ... [runner-label]
- |
-23 | runs-on: macos-10.13
- | ^~~~~~~~~~~
-```
-
-[Playground](https://rhysd.github.io/actionlint#eJyFj8EKgzAQRO/5ijn0aEJtxUN+pfSQ2lQtNpFsAhbx36skUgKFnpbZnR3mWSMxBurY095IMsBr8tsEyDvldfuOCngp7/ppV4ALxmj31QBfPY0lPqgtJTsMvQnTr8OF9PDgnSWv70W0FZjq6pq5lHvVVbZpx8BSC+J2pTjMc6ooYjMsS+LQI+01+fYgoZvOQgjBEvFJ5mGRozyK8vw34wNI+VUQ)
-
-GitHub Actions provides two kinds of job runners, [GitHub-hosted runner][gh-hosted-runner] and [self-hosted runner][self-hosted-runner].
-Each runner has one or more labels. GitHub Actions runtime finds a proper runner based on label(s) specified at `runs-on:`
-to run the job. So specifying proper labels at `runs-on:` is important.
-
-actionlint checks proper label is used at `runs-on:` configuration. Even if an expression is used in the section like
-`runs-on: ${{ matrix.foo }}`, actionlint parses the expression and resolves the possible values, then validates the values.
-
-When you define some custom labels for your self-hosted runner, actionlint does not know the labels. Please set the label
-names in [`actionlint.yaml` configuration file](config.md) to let actionlint know them.
-
-In addition to checking label values, actionlint checks combinations of labels. `runs-on:` section can be an array that contains
-multiple labels. In this case, a runner which has all the labels will be selected. However, those labels combinations can have
-conflicts.
-
-Example input:
-
-```yaml
-on: push
-jobs:
- test:
- runs-on: [ubuntu-latest, windows-latest]
- steps:
- - run: echo ...
-```
-
-Output:
-
-```
-test.yaml:4:30: label "windows-latest" conflicts with label "ubuntu-latest" defined at line:4,col:15. note: to run your job on each worker, use matrix [runner-label]
- |
-4 | runs-on: [ubuntu-latest, windows-latest]
- | ^~~~~~~~~~~~~~~
-```
-
-[Playground](https://rhysd.github.io/actionlint#eJwti0EOgCAMBO+8Yh8gPICvGA+iJEhMS2wbvq+op81mZpgimklxlZNEB2gWHQtcRuL54bMlIzV/rgNO6Aft3OX/yyuL5iZfB/jRRuStMEIIN17iHww=)
-
-In most cases, this is a misunderstanding that a matrix combination can be specified at `runs-on:` directly. It should use
-`matrix:` and expand it with `${{ }}` at `runs-on:` to run the workflow on multiple runners.
-
-
-## Action format in `uses:`
-
-Example input:
-
-```yaml
-on: push
-jobs:
- test:
- runs-on: ubuntu-latest
- steps:
- # ERROR: ref is missing
- - uses: actions/checkout
- # ERROR: owner name is missing
- - uses: checkout@v2
- # ERROR: tag is empty
- - uses: 'docker://image:'
- # ERROR: local action must start with './'
- - uses: .github/my-actions/do-something
-```
-
-Output:
-
-```
-test.yaml:7:15: specifying action "actions/checkout" in invalid format because ref is missing. available formats are "{owner}/{repo}@{ref}" or "{owner}/{repo}/{path}@{ref}" [action]
- |
-7 | - uses: actions/checkout
- | ^~~~~~~~~~~~~~~~
-test.yaml:9:15: specifying action "checkout@v2" in invalid format because owner is missing. available formats are "{owner}/{repo}@{ref}" or "{owner}/{repo}/{path}@{ref}" [action]
- |
-9 | - uses: checkout@v2
- | ^~~~~~~~~~~
-test.yaml:11:15: tag of Docker action should not be empty: "docker://image" [action]
- |
-11 | - uses: 'docker://image:'
- | ^~~~~~~~~~~~~~~~~
-test.yaml:13:15: specifying action ".github/my-actions/do-something" in invalid format because ref is missing. available formats are "{owner}/{repo}@{ref}" or "{owner}/{repo}/{path}@{ref}" [action]
- |
-13 | - uses: .github/my-actions/do-something
- | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-```
-
-[Playground](https://rhysd.github.io/actionlint#eJxdzTEOgzAMBdCdU3hjSi119NSrJKlFUkqMsF2pty8UsWTy139fsjSC1bUML0lKA4Cx2nEBNm8aZHdP3szDOx72JzVe9VwBBHBlJYjZqjTFXDjP4tbxVT8+907Gp+SZN0KsS5yYxs5vOFUrnvD6sHzDGX98DjoH)
-
-Action needs to be specified in a format defined in [the document][action-uses-doc]. There are 3 types of actions:
-
-- action hosted on GitHub: `owner/repo/path@ref`
-- local action: `./path/to/my-action`
-- Docker action: `docker://image:tag`
-
-actionlint checks values at `uses:` sections follow one of these formats.
-
-Note that actionlint does not report any error when a directory for a local action does not exist in the repository because it is
-a common case where the action is managed in a separate repository and the action directory is cloned at running the workflow.
-(See [#25][issue-25] and [#40][issue-40] for more details).
-
-
-## Local action inputs validation at `with:`
-
-My action definition at `.github/actions/my-action/action.yaml`:
-
-```yaml
-name: 'My action'
-author: 'rhysd '
-description: 'my action'
-
-inputs:
- name:
- description: your name
- default: anonymous
- message:
- description: message to this action
- required: true
- addition:
- description: additional information
- required: false
-
-runs:
- using: 'node20'
- main: 'index.js'
-```
-
-Example input:
-
-```yaml
-on: push
-jobs:
- test:
- runs-on: ubuntu-latest
- steps:
- # missing required input "message"
- - uses: ./.github/actions/my-action
- # unexpected input "additions"
- - uses: ./.github/actions/my-action
- with:
- name: rhysd
- message: hello
- additions: foo, bar
-```
-
-Output:
-
-```
-test.yaml:7:15: missing input "message" which is required by action "My action" defined at "./.github/actions/my-action". all required inputs are "message" [action]
- |
-7 | - uses: ./.github/actions/my-action
- | ^~~~~~~~~~~~~~~~~~~~~~~~~~~
-test.yaml:13:11: input "additions" is not defined in action "My action" defined at "./.github/actions/my-action". available inputs are "addition", "message", "name" [action]
- |
-13 | additions: foo, bar
- | ^~~~~~~~~~
-```
-
-When a local action is run in `uses:` of `step:`, actionlint reads `action.yml` file in the local action directory and
-validates inputs at `with:` in the workflow are correct. Missing required inputs and unexpected inputs can be detected.
-
-
-## Popular action inputs validation at `with:`
-
-Example input:
-
-```yaml
-on: push
-
-jobs:
- test:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/cache@v3
- with:
- keys: |
- ${{ hashFiles('**/*.lock') }}
- ${{ hashFiles('**/*.cache') }}
- path: ./packages
- - run: make
-```
-
-Output:
-
-```
-test.yaml:7:15: missing input "key" which is required by action "actions/cache@v3". all required inputs are "key", "path" [action]
- |
-7 | - uses: actions/cache@v3
- | ^~~~~~~~~~~~~~~~
-test.yaml:9:11: input "keys" is not defined in action "actions/cache@v3". available inputs are "key", "path", "restore-keys", "upload-chunk-size" [action]
- |
-9 | keys: |
- | ^~~~~
-```
-
-[Playground](https://rhysd.github.io/actionlint#eJyFj0EKwjAQRfc9xV8I1UJbcJmVK+8xDYOpqUlwEkVq725apYgbV8PMe/Dne6cQkpiiOPtOVAFEljhP4Jqc1D4LqUsupnqgmS1IIgd5W0CNJCwKpGPvnbSatOHDbf/BwL2PRq0bYPmR9efXBdiMIwyJOfYDy7asqrZqBq9tucM0/TWXyF81UI5F0wbSlk4s67u5mMKFLL8A+h9EEw==)
-
-actionlint checks inputs of many popular actions such as `actions/checkout@v3`. It checks
-
-- some input is required by the action but it is not set at `with:`
-- input set at `with:` is not defined in the action (this commonly occurs by a typo)
-
-this is done by checking `with:` section items with a small database collected at building `actionlint` binary. actionlint
-can check popular actions without fetching any `action.yml` of the actions from the remote so that it can run efficiently.
-
-Note that it only supports the case of specifying major versions like `actions/checkout@v3`. Fixing version of action like
-`actions/checkout@v3.0.2` and using the HEAD of action like `actions/checkout@main` are not supported for now.
-
-So far, actionlint supports more than 100 popular actions The data set is embedded at [`popular_actions.go`](../popular_actions.go)
-and were automatically collected by [a script][generate-popular-actions]. If you want more checks for other actions, please
-make a request [as an issue][issue-form].
-
-
-## Outdated popular actions detection at `with:`
-
-Example input:
+## Configuration
-```yaml
-on: push
-
-jobs:
- test:
- runs-on: ubuntu-latest
- steps:
- # ERROR: actions/checkout@v2 is using the outdated runner 'node12'
- - uses: actions/checkout@v2
-```
-
-Output:
-
-```
-test.yaml:8:15: the runner of "actions/checkout@v2" action is too old to run on GitHub Actions. update the action's version to fix this issue [action]
- |
-8 | - uses: actions/checkout@v2
- | ^~~~~~~~~~~~~~~~~~~
-```
-
-[Playground](https://rhysd.github.io/actionlint#eJwlyjkOwCAMRNGeU8wFUKSUVLkKICSyyEYZO+fPVv3ifZWE4ewhbFqYAmCN9hY4XRj1Gby4mMcjv/YRrQ3+FxDhbEzI1VYVTrW3uqvbcs03dIgdzQ==)
-
-In addition to the checks for inputs of actions described in [the previous section](#check-popular-action-inputs), actionlint
-reports an error when a popular action is 'outdated'. An action is outdated when the runner used by the action is no longer
-supported by GitHub Actions runtime. For example, `node12` is no longer available so any actions can use `node12` runner.
-
-Note that this check doesn't report that the action version is up-to-date. For example, even if you use `actions/checkout@v3` and
-newer version `actions/checkout@v4` is available, actionlint reports no error as long as `actions/checkout@v3` is not outdated.
-If you want to keep actions used by your workflows up-to-date, consider to use [Dependabot][dependabot-doc].
-
-
-## Shell name validation at `shell:`
-
-Example input:
-
-```yaml
-on: push
-jobs:
- linux:
- runs-on: ubuntu-latest
- steps:
- - run: echo 'hello'
- # ERROR: Unavailable shell
- shell: dash
- - run: echo 'hello'
- # ERROR: 'powershell' is only available on Windows
- shell: powershell
- mac:
- runs-on: macos-latest
- defaults:
- run:
- # ERROR: default config is also checked. fish is not supported
- shell: fish
- steps:
- - run: echo 'hello'
- # OK: Custom shell
- shell: 'perl {0}'
- windows:
- runs-on: windows-latest
- steps:
- - run: echo 'hello'
- # ERROR: 'sh' is only available on Windows
- shell: sh
- - run: echo 'hello'
- # OK: 'powershell' is only available on Windows
- shell: powershell
-```
-
-Output:
-
-```
-test.yaml:8:16: shell name "dash" is invalid. available names are "bash", "pwsh", "python", "sh" [shell-name]
- |
-8 | shell: dash
- | ^~~~
-test.yaml:11:16: shell name "powershell" is invalid on macOS or Linux. available names are "bash", "pwsh", "python", "sh" [shell-name]
- |
-11 | shell: powershell
- | ^~~~~~~~~~
-test.yaml:14:16: shell name "powershell" is invalid on macOS or Linux. available names are "bash", "pwsh", "python", "sh" [shell-name]
- |
-14 | shell: powershell
- | ^~~~~~~~~~
-test.yaml:20:16: shell name "fish" is invalid. available names are "bash", "pwsh", "python", "sh" [shell-name]
- |
-20 | shell: fish
- | ^~~~
-test.yaml:30:16: shell name "sh" is invalid on Windows. available names are "bash", "pwsh", "python", "cmd", "powershell" [shell-name]
- |
-30 | shell: sh
- | ^~
-```
-
-[Playground](https://rhysd.github.io/actionlint#eJylkLsKwzAMRfd8hbZMhs7+GydWcIpqGcsihdJ/r52GUjz1sUm6R48rjhaSShjOPIkdAGiNem0BQNYohiugk8aihlxBKbskBZM8KQDTSAs4B4YxIBGPh1LBllvwrq74mE68Yd7jH3subu4s1ArLuwOPi1MqLxNtQT9zWY+rv7U7JswEt9O9KdsaPW/SHXRU/3mqhAdbk36k)
-
-Available shells for runners are defined in [the documentation][shell-doc]. actionlint checks shell names at `shell:`
-configuration are properly using the available shells.
-
-
-## Job ID and step ID uniqueness
-
-Example input:
-
-```yaml
-on: push
-jobs:
- test:
- runs-on: ubuntu-latest
- steps:
- - run: echo 'hello'
- id: step_id
- - run: echo 'bye'
- # ERROR: Duplicate step ID
- id: STEP_ID
- # ERROR: Duplicate job ID
- TEST:
- runs-on: ubuntu-latest
- steps:
- - run: echo 'hello'
- # OK. Step ID uniqueness is job-local
- id: step_id
-```
-
-Output:
-
-```
-test.yaml:10:13: step ID "STEP_ID" duplicates. previously defined at line:7,col:13. step ID must be unique within a job. note that step ID is case insensitive [id]
- |
-10 | id: STEP_ID
- | ^~~~~~~
-test.yaml:12:3: key "TEST" is duplicated in "jobs" section. previously defined at line:3,col:3. note that key names are case insensitive [syntax-check]
- |
-12 | TEST:
- | ^~~~~
-```
-
-[Playground](https://rhysd.github.io/actionlint#eJzLz7NSKCgtzuDKyk8qtuJSUChJLS4B0QoKRaV5xbr5QPnSpNK8klLdnESQHFiquCS1oBiiSkFBF6TSSiE1OSNfQT0jNScnXx0qo6CQmWIFVhyfmYJNdVJlKqra4BDXgHhPF6BYiGtwCE3cAQCKgUNq)
-
-Job IDs and step IDs in each jobs must be unique. IDs are compared in case insensitive. actionlint checks all job IDs
-and step IDs, and reports errors when some IDs duplicate.
-
-
-## Hardcoded credentials
-
-Example input:
-
-```yaml
-on: push
-jobs:
- test:
- runs-on: ubuntu-latest
- container:
- image: 'example.com/owner/image'
- credentials:
- username: user
- # ERROR: Hardcoded password
- password: pass
- services:
- redis:
- image: redis
- credentials:
- username: user
- # ERROR: Hardcoded password
- password: pass
- steps:
- - run: echo 'hello'
-```
-
-Output:
-
-```
-test.yaml:10:19: "password" section in "container" section should be specified via secrets. do not put password value directly [credentials]
- |
-10 | password: pass
- | ^~~~
-test.yaml:17:21: "password" section in "redis" service should be specified via secrets. do not put password value directly [credentials]
- |
-17 | password: pass
- | ^~~~
-```
-
-[Playground](https://rhysd.github.io/actionlint#eJx1kLEOwyAMRPd8hTemNDt/4xCroQKMMDT9/AJNUYd0wrp357PgoCEW2acHr6IngEyS2wuQSpCZKy9rCbnMDhvryHDIaAOljxPAeryTBkUv9NHRzbBf+KiGpRN12kyijUK26OSbBChCKaCv8TYNOaLIwWnTfepyxU9raGTrNvuz6Dyiq0O8rPxbel2bKY7w3P5FA5mdQe3kHKs3Uktdww==)
-
-[Credentials for container][credentials-doc] can be put in `container:` configuration. Password should be put in secrets
-and the value should be expanded with `${{ }}` syntax at `password:`. actionlint checks hardcoded credentials, and reports
-them as an error.
-
-
-## Environment variable names
-
-Example input:
-
-```yaml
-on: push
-jobs:
- test:
- runs-on: ubuntu-latest
- env:
- FOO=BAR: foo
- FOO BAR: foo
- steps:
- - run: echo 'hello'
-```
-
-Output:
-
-```
-test.yaml:6:7: environment variable name "FOO=BAR" is invalid. '&', '=' and spaces should not be contained [env-var]
- |
-6 | FOO=BAR: foo
- | ^~~~~~~~
-test.yaml:7:7: environment variable name "FOO BAR" is invalid. '&', '=' and spaces should not be contained [env-var]
- |
-7 | FOO BAR: foo
- | ^~~
-```
-
-[Playground](https://rhysd.github.io/actionlint#eJzLz7NSKCgtzuDKyk8qtuJSUChJLS4B0QoKRaV5xbr5QPnSpNK8klLdnESQHFgqNa8MokZBwc3f39bJMchKIS0/HyGkgCJUXJJaUAzToAsy2EohNTkjX0E9IzUnJ18dAPhYJMc=)
-
-`=` must not be included in environment variable names. And `&` and spaces should not be included in them. In almost all
-cases they are mistakes and they may cause some issues on using them in shell since they have special meaning in shell syntax.
-
-actionlint checks environment variable names are correct in `env:` configuration.
-
-
-## Permissions
-
-Example input:
-
-```yaml
-on: push
-
-# ERROR: Available values for whole permissions are "write-all", "read-all" or "none"
-permissions: write
-
-jobs:
- test:
- runs-on: ubuntu-latest
- permissions:
- # ERROR: "checks" is correct scope name
- check: write
- # ERROR: Available values are "read", "write" or "none"
- issues: readable
- steps:
- - run: echo hello
-```
-
-Output:
-
-```
-test.yaml:4:14: "write" is invalid for permission for all the scopes. available values are "read-all" and "write-all" [permissions]
- |
-4 | permissions: write
- | ^~~~~
-test.yaml:11:7: unknown permission scope "check". all available permission scopes are "actions", "attestations", "checks", "contents", "deployments", "discussions", "id-token", "issues", "packages", "pages", "pull-requests", "repository-projects", "security-events", "statuses" [permissions]
- |
-11 | check: write
- | ^~~~~~
-test.yaml:13:15: "readable" is invalid for permission of scope "issues". available values are "read", "write" or "none" [permissions]
- |
-13 | issues: readable
- | ^~~~~~~~
-```
-
-[Playground](https://rhysd.github.io/actionlint#eJxNjd0NwyAMhN89xS3AAmwDxBK0FCOMlfUDiVr16aTv/qR5dNNM1Hl8imqRph7nKJOJXhLVEzBZ51ZgWFMnq2TR2jRXw/Zu63/gBkDKnN7ftQethPF6GByOEOuDdXL/ldw+8eCUBZlrlQvntjLp)
-
-Permissions of `GITHUB_TOKEN` token can be configured at workflow-level or job-level by [`permissions:` section][perm-config-doc].
-Each permission scopes have its access levels. The default levels are described in [the document][permissions-doc].
-
-actionlint checks permission scopes and access levels in a workflow are correct.
-
-
-## Reusable workflows
-
-[Reusable workflows][reusable-workflow-doc] is a feature to call a workflow from another workflow.
-
-actionlint does several checks for both workflow calls (caller) and reusable workflows (callee):
-
-- syntax of workflow calls and reusable workflows
-- type checks for inputs (respecting `type:` field of each input) in both workflow calls and reusable workflows
-- type checks for `inputs`, `outputs` and `secrets` context objects in reusable workflows
-- optional/required/undefined inputs and secrets at `uses:` in workflow calls
-- type checks for `outputs` objects used by downstream jobs of workflow calls
-
-These checks are described in this section.
-
-### Check input definitions of `workflow_call` event in reusable workflow
-
-Example input:
-
-```yaml
-on:
- workflow_call:
- inputs:
- scheme:
- description: Scheme of URL
- # OK: Type is string
- default: https
- type: string
- host:
- default: example.com
- type: string
- port:
- description: Port of URL
- # ERROR: Type is number but default value is string
- default: ':1234'
- type: number
- query:
- description: Query of URL
- # ERROR: Type must be one of number, string, boolean
- type: object
- path:
- description: Path of URL
- required: true
- # ERROR: Default value is never used since this input is required
- default: ''
- type: string
-jobs:
- do:
- runs-on: ubuntu-latest
- steps:
- - run: echo "${{ inputs.scheme }}://${{ inputs.host }}:${{ inputs.port }}${{ inputs.path }}"
-```
-
-Output:
-
-```
-test.yaml:15:18: input of workflow_call event "port" is typed as number but its default value ":1234" cannot be parsed as a float number: strconv.ParseFloat: parsing ":1234": invalid syntax [events]
- |
-15 | default: ':1234'
- | ^~~~~~~
-test.yaml:20:15: invalid value "object" for input type of workflow_call event. it must be one of "boolean", "number", or "string" [syntax-check]
- |
-20 | type: object
- | ^~~~~~
-test.yaml:25:18: input "path" of workflow_call event has the default value "", but it is also required. if an input is marked as required, its default value will never be used [events]
- |
-25 | default: ''
- | ^~
-```
-
-[Playground](https://rhysd.github.io/actionlint#eJx9kctuwyAQRff9ilFUKSsn6mPFN3TRh7quMB4XUswQGJRGkf+9JjiR5dbdwZnhzmUuOXEDcKDw1Vo6fChpbQYAxvnEsZwBotLY4eUG0GBUwXg25AS8nYtALby/Pk1aWpksC9DMPl4xHz0KiByM+xyhpsji9zv8lp23uFHU/ffaU+AFY89DadHWWtzdPzyuZ9IudTWGEe4ThuOC9kuuzcWLBtU7VHyxJ1kv2RtKc4WA+2QCNgI4JPzD9dzwuIsd1eewGirDQnKxykNSnRynykrGWDxFRn8Ntsqdw66VJljdnk5j7psSOPS92G4nOEeV4QTl/Q9oSvK/+n71A7U5rsA=)
-
-Unlike inputs of action, inputs of a workflow must specify their types. actionlint validates input types and checks the default
-values are correctly typed. For more details, see [the official document][create-reusable-workflow-doc].
-
-### Check workflow call syntax
-
-Example input:
-
-```yaml
-on: push
-jobs:
- job1:
- uses: owner/repo/path/to/workflow.yml@v1
- # ERROR: 'runs-on' is not available on calling reusable workflow
- runs-on: ubuntu-latest
- job2:
- # ERROR: Local file path with ref is not available
- uses: ./.github/workflows/ci.yml@main
- job3:
- # ERROR: 'with' is only available on calling reusable workflow
- with:
- foo: bar
- runs-on: ubuntu-latest
- steps:
- - run: echo hello
- job4:
- # ERROR: This workflow does not exist
- uses: ./.github/workflows/not-existing.yml
-```
-
-Output:
-
-```
-test.yaml:6:5: when a reusable workflow is called with "uses", "runs-on" is not available. only following keys are allowed: "name", "uses", "with", "secrets", "needs", "if", and "permissions" in job "job1" [syntax-check]
- |
-6 | runs-on: ubuntu-latest
- | ^~~~~~~~
-test.yaml:9:11: reusable workflow call "./.github/workflows/ci.yml@main" at "uses" is not following the format "owner/repo/path/to/workflow.yml@ref" nor "./path/to/workflow.yml". see https://docs.github.com/en/actions/learn-github-actions/reusing-workflows for more details [workflow-call]
- |
-9 | uses: ./.github/workflows/ci.yml@main
- | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-test.yaml:12:5: "with" is only available for a reusable workflow call with "uses" but "uses" is not found in job "job3" [syntax-check]
- |
-12 | with:
- | ^~~~~
-test.yaml:19:11: could not read reusable workflow file for "./.github/workflows/not-existing.yml": open /path/to/.github/workflows/not-existing.yml: no such file or directory [workflow-call]
- |
-19 | uses: ./.github/workflows/not-existing.yml
- | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-```
-
-[Playground](https://rhysd.github.io/actionlint#eJyFjkESwiAMRfeeIhdomaorVl4FOmlBKWFIEL29bRkdV7r6Wfz38ilqSIXd4UqW9QFgzWFLgMLIGqhGzCpjIpWMOCWkKuXbFKj2zyVc7sNeziVyR6us2BKldMEIsjTf8dvXq3724or9aFiNflctxsdGnBpR12K7ACYiDdbk398AWDDxG+q2pgYcHYHDEKjpz/8GRZIOH57Fx3mb9gJtJVzI)
-
-When calling an external workflow, [only specific keys are available][reusable-workflow-call-keys] at job configuration.
-For example, `secrets:` is not available when running steps in a normal job. And `runs-on:` is not available when calling
-a reusable workflow since the called workflow determines which OS is used. actionlint checks such keys are used correctly
-to call a reusable workflow or to run steps in a normal job.
-
-And the workflow syntax at `uses:` must follow the format `owner/repo/path/to/workflow.yml@ref` as described in
-[the official document][create-reusable-workflow-doc]. actionlint checks if the value follows the format.
-
-actionlint also validates the called workflow file is actually existing when it is a local workflow (starting with `./`).
-actionlint reports an error when it does not exist.
-
-### Check types of `inputs.*` and `secrets.*` in reusable workflow
-
-Example input:
-
-```yaml
-on:
- workflow_call:
- inputs:
- url:
- description: 'your URL'
- type: string
- lucky_number:
- description: 'your lucky number'
- type: number
- secrets:
- credential:
- description: 'your credential'
-
-jobs:
- test:
- runs-on: ubuntu-20.04
- steps:
- - name: Send data
- # ERROR: uri is typo of url
- run: curl ${{ inputs.uri }} -d ${{ inputs.lucky_number }}
- env:
- # ERROR: credentials is typo of credential
- TOKEN: ${{ secrets.credentials }}
-```
-
-Output:
-
-```
-test.yaml:20:23: property "uri" is not defined in object type {url: string; lucky_number: number} [expression]
- |
-20 | run: curl ${{ inputs.uri }} -d ${{ inputs.lucky_number }}
- | ^~~~~~~~~~
-test.yaml:23:22: property "credentials" is not defined in object type {credential: string} [expression]
- |
-23 | TOKEN: ${{ secrets.credentials }}
- | ^~~~~~~~~~~~~~~~~~~
-```
-
-[Playground](https://rhysd.github.io/actionlint#eJx9UD1PwzAQ3fMr3oCUKVGFmLwzgUAqMFeOfSDT9BzZZ6qo6n/HjUNSMXS7e/c+7s6zqoCjD/vP3h93Rvf9BQAcD0liqYEU+r8SsBRNcIM4zwr16FPAx/a5XuYyDqQQJTj+msE+mf2443ToKNw0mogoxP+OBZ3ASCbQul5uLLE4fXvLlVZX1bfvJr1QlKIKiWNzYacusaTmftNuHkqc0LCENWB9yOu8EVtYLXqJzAYKJv8Kd6fT/ME2BYfzGY29Bq//kaeLA/HPegHw/vr0+KIm4Xxxu94Qs/AXqVaEog==)
-
-Inputs of reusable workflow calls are set to `inputs.*` properties following the definitions at `on.workflow_call.inputs`.
-And in a job of a reusable workflow, `secrets.*` are passed from caller of the workflow so it is set following the definitions at
-`on.workflow_call.secrets`. See [the official document][create-reusable-workflow-doc] for more details.
-
-actionlint contextually defines types of `inputs` and `secrets` contexts looking at `workflow_call` event. Keys of `inputs` only
-allow keys at `on.workflow_call.inputs` and their values are typed based on `on.workflow_call.inputs..type`. Type of
-`secrets` is also strictly typed following `on.workflow_call.secrets`.
-
-[From May 3, 2022][inherit-secrets-announce], GitHub Actions allows inheriting secrets by calling reusable workflows. The caller
-declares to inherit all secrets.
-
-```yaml
-jobs:
- pass-secrets-to-workflow:
- uses: ./.github/workflows/called-workflow.yml
- secrets: inherit
-```
-
-This means that actionlint cannot know whether the workflow inherits secrets or not when checking a reusable workflow.
-To solve this issue, actionlint assumes that
-
-- when `secrets:` is omitted in a reusable workflow, the workflow inherits secrets from a caller
-- when `secrets:` exists in a reusable workflow, the workflow inherits no other secret
-
-Following the assumptions,
-
-```yaml
-on:
- workflow_call:
-
-jobs:
- pass-secret-to-action:
- runs-on: ubuntu-latest
- steps:
- # OK: This reports no error. FOO is assumed to be inherited from caller
- - run: echo ${{ secrets.FOO }}
-```
-
-this workflow causes no error. And
-
-```yaml
-on:
- workflow_call:
- secrets:
-
-jobs:
- pass-secret-to-action:
- runs-on: ubuntu-latest
- steps:
- # ERROR: Secret FOO is not defined
- - run: echo ${{ secrets.FOO }}
-```
-
-this workflow causes 'no such secret' error at `secrets.FOO`.
+- **Allowlist**: `.github/actionlint-allowlist.txt` - patterns to ignore
+- **Workflow**: `health-42-actionlint.yml` - CI integration
-### Check outputs in reusable workflow
-
-Example input:
-
-```yaml
-on:
- workflow_call:
- outputs:
- image-version:
- description: "Docker image version"
- # ERROR: 'imagetag' does not exist (typo of 'image_tag')
- value: ${{ jobs.gen-image-version.outputs.imagetag }}
-jobs:
- gen-image-version:
- runs-on: ubuntu-latest
- outputs:
- image_tag: "${{ steps.get_tag.outputs.tag }}"
- steps:
- - run: ./output_image_tag.sh
- id: get_tag
-```
-
-Output:
-
-```
-test.yaml:6:20: property "imagetag" is not defined in object type {image_tag: string} [expression]
- |
-6 | value: ${{ jobs.gen-image-version.outputs.imagetag }}
- | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-```
-
-[Playground](https://rhysd.github.io/actionlint#eJx1j0EOgyAQRfc9xcR0C92z7j0M6tRSKRgYdGG8ex1Rkqbpkp/Hf3+8UxeA2YfhYf1ct9paDgB8ojFRzA8A89Y9iglDNN6dIUCHsQ1mJA6huvt2wJBZONiqsJO2CRVclwVevomyRye+auXhlHtKuod1vTDKvh86jwjJRcHy1CRHSVhNGOnvBfXWug3lDZFw5BHEWVFnax69E+d3wSoF8pbJutTJ+Cwnmk7B0fgB2ORuYw==)
-
-Outputs of a reusable workflow can be defined at `on.workflow_call.outputs` as described in [the document][reusable-workflow-outputs].
-The `jobs` context is available to define an output value to refer the outputs of jobs in the workflow. actionlint checks
-the context is used correctly.
-
-### Check inputs and secrets in workflow call
-
-Example reusable workflow:
-
-```yaml
-# .github/workflows/reusable.yaml
-on:
- workflow_call:
- inputs:
- name:
- type: string
- required: true
- id:
- type: number
- message:
- type: string
- secrets:
- password:
- required: true
-
-jobs:
- test:
- runs-on: ubuntu-latest
- steps:
- - run: echo '${{ outputs.required_input }}'
-```
-
-Example input:
-
-```yaml
-on: push
-
-jobs:
- # Check required/undefined inputs and secrets
- missing-required:
- uses: ./.github/workflows/reusable.yaml
- with:
- # ERROR: Undefined input
- user: rhysd
- # ERROR: Required input "name" is missing
- secrets:
- # ERROR: Undefined secret
- credentials: my-token
- # ERROR: Required secret "password" is missing
-
- # Check types of inputs defined in reusable workflow
- type-checks:
- uses: ./.github/workflows/reusable.yaml
- with:
- name: rhysd
- # ERROR: Cannot assign bool value to number input
- id: true
- # ERROR: Cannot assign null to string input. If you want to pass string "null", use ${{ 'null' }}
- message: null
- secrets:
- password: p@ssw0rd
-```
-
-Output:
-
-```
-test.yaml:6:11: input "name" is required by "./.github/workflows/reusable.yaml" reusable workflow [workflow-call]
- |
-6 | uses: ./.github/workflows/reusable.yaml
- | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-test.yaml:6:11: secret "password" is required by "./.github/workflows/reusable.yaml" reusable workflow [workflow-call]
- |
-6 | uses: ./.github/workflows/reusable.yaml
- | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-test.yaml:9:7: input "user" is not defined in "./.github/workflows/reusable.yaml" reusable workflow. defined inputs are "id", "message", "name" [workflow-call]
- |
-9 | user: rhysd
- | ^~~~~
-test.yaml:13:7: secret "credentials" is not defined in "./.github/workflows/reusable.yaml" reusable workflow. defined secret is "password" [workflow-call]
- |
-13 | credentials: my-token
- | ^~~~~~~~~~~~
-test.yaml:22:11: input "id" is typed as number by reusable workflow "./.github/workflows/reusable.yaml". bool value cannot be assigned [expression]
- |
-22 | id: true
- | ^~~~
-test.yaml:24:16: input "message" is typed as string by reusable workflow "./.github/workflows/reusable.yaml". null value cannot be assigned [expression]
- |
-24 | message: null
- | ^~~~
-```
-
-Reusable workflows can define required/optional inputs and secrets. When they are missing or some undefined input is used in a
-workflow call, actionlint reports an error.
-
-And reusable workflows must define types of their inputs by `type:` field. Workflow calls pass constants (`input: 42`) or
-expressions (`inputs: ${{ ... }}`) to the inputs or secrets. actionlint checks types of values passed to inputs in workflow call.
-When a type of input doesn't match to its definition, actionlint reports an error.
-
-Note that this check only works with local reusable workflow (it starts with `./`).
-
-### Check outputs of workflow call in downstream jobs
-
-Example reusable workflow:
-
-```yaml
-# .github/workflows/get-build-info.yaml
-on:
- workflow_call:
- outputs:
- version:
- value: ${{ outputs.version }}
- description: version of software
-
-jobs:
- test:
- runs-on: ubuntu-latest
- outputs:
- version: ${{ steps.get_version.outputs.version }}
- steps:
- - run: ...
- id: get_version
-```
-
-Example input:
-
-```yaml
-on: push
-
-jobs:
- get_build_info:
- uses: ./.github/workflows/get-build-info.yaml
- downstream:
- needs: [get_build_info]
- runs-on: ubuntu-latest
- steps:
- # OK. `version` is defined in the reusable workflow
- - run: echo '${{ needs.get_build_info.outputs.version }}'
- # ERROR: `tag` is not defined in the reusable workflow
- - run: echo '${{ needs.get_build_info.outputs.tag }}'
-```
-
-Output:
-
-```
-test.yaml:13:24: property "tag" is not defined in object type {version: string} [expression]
- |
-13 | - run: echo '${{ needs.get_build_info.outputs.tag }}'
- | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-```
-
-Outputs of workflow call are set to the job's outputs object. They can be accessed by downstream jobs specified with `needs:`.
-What outputs are set is defined in the reusable workflow. actionlint types outputs objects from workflow calls and check the
-object types in downstream jobs.
-
-In the above example, `get-build-info.yaml` has one output `version`. actionlint types the outputs object of workflow call job
-as `{version: string}`. In the downstream job, actionlint can report an error at undefined key `tag` in the object.
-
-Note that this check only works with local reusable workflow (starting with `./`).
-
-
-## ID naming convention
-
-Example input:
-
-```yaml
-on: push
-
-jobs:
- # ERROR: '.' cannot be contained in ID
- foo-v1.2.3:
- runs-on: ubuntu-latest
- steps:
- - run: echo 'job ID with version'
- # ERROR: ID cannot contain spaces
- id: echo for test
- # ERROR: ID cannot start with '-'
- -hello-world-:
- runs-on: ubuntu-latest
- steps:
- - run: echo 'oops'
- # ERROR: ID cannot start with numbers
- 2d-game:
- runs-on: ubuntu-latest
- steps:
- - run: echo 'oops'
-```
-
-Output:
-
-```
-test.yaml:5:3: invalid job ID "foo-v1.2.3". job ID must start with a letter or _ and contain only alphanumeric characters, -, or _ [id]
- |
-5 | foo-v1.2.3:
- | ^~~~~~~~~~~
-test.yaml:10:13: invalid step ID "echo for test". step ID must start with a letter or _ and contain only alphanumeric characters, -, or _ [id]
- |
-10 | id: echo for test
- | ^~~~
-test.yaml:12:3: invalid job ID "-hello-world-". job ID must start with a letter or _ and contain only alphanumeric characters, -, or _ [id]
- |
-12 | -hello-world-:
- | ^~~~~~~~~~~~~~
-test.yaml:17:3: invalid job ID "2d-game". job ID must start with a letter or _ and contain only alphanumeric characters, -, or _ [id]
- |
-17 | 2d-game:
- | ^~~~~~~~
-```
-
-[Playground](https://rhysd.github.io/actionlint#eJylzTEOwyAMheGdU7wtkyM13Zi79BhJIYWKYoQhuX5Cyw0yWv70P44aqYpT6sOLaAWszLTdxmm8twvINQrxyepSY6kU5mKl/F5SbJK/AqhJDftyjOGM4fnA7ovDZrN4jkN3gDedrZzRY+RsCEw752DowjBzkrY0GXrPX3u1dACDIlSH)
-
-IDs must start with a letter or `_` and contain only alphanumeric characters, `-` or `_`. actionlint checks the naming
-convention, and reports invalid IDs as errors.
-
-
-## Contexts and special functions availability
-
-Example input:
-
-```yaml
-on: push
-
-env:
- NAME: rhysd
-
-jobs:
- test:
- strategy:
- matrix:
- directory:
- # OK: 'github' context is available here
- - ${{ github.workflow }}
- # ERROR: 'runner' context is not available here
- - ${{ runner.temp }}
- runs-on: ubuntu-latest
- env:
- # ERROR: 'env' context is not available here
- NAME: ${{ env.NAME }}
- steps:
- - env:
- # OK: 'env' context is available here
- NAME: ${{ env.NAME }}
- # ERROR: 'success()' function is not available here
- run: echo 'Success? ${{ success() }}'
- # OK: 'success()' function is available here
- if: success()
-```
-
-Output:
-
-```
-test.yaml:14:17: context "runner" is not allowed here. available contexts are "github", "inputs", "needs", "vars". see https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability for more details [expression]
- |
-14 | - ${{ runner.temp }}
- | ^~~~~~~~~~~
-test.yaml:18:17: context "env" is not allowed here. available contexts are "github", "inputs", "matrix", "needs", "secrets", "strategy", "vars". see https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability for more details [expression]
- |
-18 | NAME: ${{ env.NAME }}
- | ^~~~~~~~
-test.yaml:24:33: calling function "success" is not allowed here. "success" is only available in "jobs..if", "jobs..steps.if". see https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability for more details [expression]
- |
-24 | run: echo 'Success? ${{ success() }}'
- | ^~~~~~~~~
-```
-
-[Playground](https://rhysd.github.io/actionlint#eJx9jkEOgjAURPc9xSxM1AUcoBvjwqVuPAGUr6DQkv5flRDvLhVRExO7aWbmTTvOarSBS6XIXrQCduvtRsOXHRdKnVzO0RRiiTfA4jOhYzcqoMnEV7dJAUXlyYjz3ccCEsz6HsdKypCnV+fPh9pdcb//ID5YSz4VatopHixO3LAy5MFKSOosjnlGr8XxjKvjE4OZRjX1WajlCUu+O/97r781yJQO830whphXT5ZHsVgO8PxNVwf9SR5WsV7P)
-
-Some contexts are only available in some places. For example, `env` context is not available at `jobs..env` but it is
-available at `jobs..steps.env`.
-
-Similarly, some status functions are special since they limit where they can be called. For example, `success()`, `failure()`,
-`always()`, and `cancelled()` are only available at `if:` section. At the time of writing this document, the following functions
-are special.
-
-- `hashFiles()`
-- `always()`
-- `success()`
-- `failure()`
-- `cancelled()`
-
-[The official contexts document][availability-doc] describes which contexts and special functions are available at which workflow
-keys.
-
-actionlint checks if these contexts and special functions are used correctly. It reports an error when it finds that some context
-or special function is not available in your workflow.
-
-
-## Check deprecated workflow commands
-
-Example input:
-
-```yaml
-on: push
-
-jobs:
- test:
- runs-on: ubuntu-latest
- steps:
- # ERROR: 'set-output' workflow command was deprecated
- - run: echo '::set-output name=foo::bar'
- # OK: Use this instead
- - run: echo "foo=bar" >> "$GITHUB_OUTPUT"
- # OK: 'debug' command is not deprecated
- - run: echo "::debug::Set the Octocat variable"
-```
-
-Output:
-
-```
-test.yaml:8:14: workflow command "set-output" was deprecated. use `echo "{name}={value}" >> $GITHUB_OUTPUT` instead: https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions [deprecated-commands]
- |
-8 | - run: echo '::set-output name=foo::bar'
- | ^~~~
-```
-
-[Playground](https://rhysd.github.io/actionlint#eJxtyjEOgkAQRuGeU/zZmFDtBSaBwkatMBFqs4ujaHCHsDOeX9CW6hXvk0SYLA9F8ZKYqQCUs64FZkvZywIsWlLzY1jfb2XlKf8V4FdJ4H4QlESZ1YvpZIoU3lzdRYhimMsN7pZZLc+hruF2h1N77PbXpmvPXeu2PNGNoz2ILqzQgdH0Kn1QfML8DHFk9wXMjT7o)
-
-GitHub deprecated the following workflow commands.
-
-- [`set-output`][deprecate-set-output-save-state]
-- [`save-state`][deprecate-set-output-save-state]
-- [`set-env`][deprecate-set-env-add-path]
-- [`add-path`][deprecate-set-env-add-path]
-
-actionlint detects these commands are used in `run:` and reports them as errors suggesting alternatives. See
-[the official document][workflow-commands-doc] for the comprehensive list of workflow commands to know the usage.
-
-
-## Conditions always evaluated to true at `if:`
-
-Example input:
-
-```yaml
-on: push
-
-jobs:
- test:
- runs-on: ubuntu-latest
- steps:
- - run: echo 'Commit is pushed'
- # OK
- if: ${{ github.event_name == 'push' }}
- - run: echo 'Commit is pushed'
- # OK
- if: |
- github.event_name == 'push'
- - run: echo 'Commit is pushed'
- # ERROR: It is always evaluated to true
- if: |
- ${{ github.event_name == 'push' }}
- - run: echo 'Commit is pushed'
- # ERROR: It is always evaluated to true
- if: "${{ github.event_name == 'push' }} "
- - run: echo 'Commit is pushed to main'
- # OK
- if: github.event_name == 'push' && github.ref_name == 'main'
- - run: echo 'Commit is pushed to main'
- # ERROR: It is always evaluated to true
- if: ${{ github.event_name == 'push' }} && ${{ github.ref_name == 'main' }}
-```
-
-Output:
-
-```
-test.yaml:16:13: if: condition "${{ github.event_name == 'push' }}\n" is always evaluated to true because extra characters are around ${{ }} [if-cond]
- |
-16 | if: |
- | ^
-test.yaml:20:13: if: condition "${{ github.event_name == 'push' }} " is always evaluated to true because extra characters are around ${{ }} [if-cond]
- |
-20 | if: "${{ github.event_name == 'push' }} "
- | ^~~~
-test.yaml:26:13: if: condition "${{ github.event_name == 'push' }} && ${{ github.ref_name == 'main' }}" is always evaluated to true because extra characters are around ${{ }} [if-cond]
- |
-26 | if: ${{ github.event_name == 'push' }} && ${{ github.ref_name == 'main' }}
- | ^~~
-```
-
-[Playground](https://rhysd.github.io/actionlint#eJy1j00OgjAQhfec4oUYusIDNGHlQQzoIDW2JXTqBrm7FP8wJogaV5PJ+/K9GWskau+qKNrbwskIYHIcJtB441LbA77whn16yEM2RI6pdhcKSAMpQZvKQqys1oqh3KClrbhCgColFm2LneLKF0s6kuG1yTUhyyACLdB1nztP9w1T7t/E/zg8fi9FPEcLttC5Ms/6KXOS3OKGykc4lnzROOOfvnhEvZb3zBmiAMLK)
-
-Evaluation of `${{ }}` at `if:` condition is tricky. When the expression in `${{ }}` is evaluated to boolean value and there is
-no extra characters around the `${{ }}`, the condition is evaluated to the boolean value. Otherwise the condition is treated as
-string hence it is **always** evaluated to `true`.
-
-It means that multi-line string must not be used at `if:` condition (`if: |`) because the condition is always evaluated to true.
-Multi-line string inserts newline character at end of each line.
-
-```yaml
-if: |
- ${{ false }}
-```
-
-is equivalent to
+## Common Issues
+### Expression syntax errors
```yaml
-if: "${{ false }}\n"
+# Wrong
+if: ${{ github.event.pull_request.merged == true }}
+# Right (no quotes needed in if:)
+if: github.event.pull_request.merged == true
```
-Unlike using `${{ }}`, putting an expression directly ignores white spaces around it. It's the reason why
-
+### Missing permissions
```yaml
-if: |
- false
+permissions:
+ contents: read
+ pull-requests: write
```
-works as intended.
-
-actionlint checks all `if:` conditions in workflow and reports error when some condition is always evaluated to true due to extra
-characters around `${{ }}`.
-
-
-## Action metadata syntax validation
-
-Example workflow input:
-
+### Deprecated commands
+Replace `set-output` with `$GITHUB_OUTPUT`:
```yaml
-on: push
-
-jobs:
- test:
- runs-on: ubuntu-latest
- steps:
- # actionlint checks an action when it is actually used in a workflow
- - uses: ./.github/actions/my-invalid-action
+# Deprecated
+echo "::set-output name=value::$VALUE"
+# Current
+echo "value=$VALUE" >> "$GITHUB_OUTPUT"
```
-Example action metadata:
-
-```yaml
-# .github/actions/my-invalid-action/action.yml
-
-name: 'My action'
-author: '...'
-# ERROR: 'description' section is required
+## Running Locally
-branding:
- # ERROR: Invalid icon name
- icon: dog
- # ERROR: Unsupported icon color
- color: black
+```bash
+# Install actionlint
+brew install actionlint # macOS
+# or
+go install github.com/rhysd/actionlint/cmd/actionlint@latest
-runs:
- # ERROR: Node.js runtime version is too old
- using: 'node14'
- # ERROR: The source file being run by this action does not exist
- main: 'this-file-does-not-exist.js'
- # ERROR: 'env' configuration is only allowed for Docker actions
- env:
- SOME_VAR: SOME_VALUE
-```
-
-Output:
-
-```
-action_metadata_syntax_validation.yaml:8:15: description is required in metadata of "My action" action at "path/to/.github/actions/my-invalid-action/action.yml" [action]
- |
-8 | - uses: ./.github/actions/my-invalid-action
- | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-action_metadata_syntax_validation.yaml:8:15: incorrect icon name "dog" at branding.icon in metadata of "My action" action at "path/to/.github/actions/my-invalid-action/action.yml". see the official document to know the exhaustive list of supported icons: https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#brandingicon [action]
- |
-8 | - uses: ./.github/actions/my-invalid-action
- | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-action_metadata_syntax_validation.yaml:8:15: incorrect color "black" at branding.icon in metadata of "My action" action at "path/to/.github/actions/my-invalid-action/action.yml". see the official document to know the exhaustive list of supported colors: https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#brandingcolor [action]
- |
-8 | - uses: ./.github/actions/my-invalid-action
- | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-action_metadata_syntax_validation.yaml:8:15: invalid runner name "node14" at runs.using in "My action" action defined at "path/to/.github/actions/my-invalid-action". valid runners are "composite", "docker", "node16", and "node20". see https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#runs [action]
- |
-8 | - uses: ./.github/actions/my-invalid-action
- | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-action_metadata_syntax_validation.yaml:8:15: file "this-file-does-not-exist.js" does not exist in "path/to/.github/actions/my-invalid-action". it is specified at "main" key in "runs" section in "My action" action [action]
- |
-8 | - uses: ./.github/actions/my-invalid-action
- | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-action_metadata_syntax_validation.yaml:8:15: "env" is not allowed in "runs" section because "My action" is a JavaScript action. the action is defined at "path/to/.github/actions/my-invalid-action" [action]
- |
-8 | - uses: ./.github/actions/my-invalid-action
- | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+# Run on all workflows
+actionlint .github/workflows/*.yml
```
-
-All actions require a metadata file `action.yml` or `aciton.yaml`. The syntax is defined in [the official document][action-metadata-doc].
-
-actionlint checks metadata files used in workflows and reports errors when they are not following the syntax.
-
-- `name:`, `description:`, `runs:` sections are required
-- Runner name at `using:` is one of `composite`, `docker`, `node16`, `node20`
-- Keys under `runs:` section are correct. Required/Valid keys are different depending on the type of action; Docker action or
- Composite action or JavaScript action (e.g. `image:` is required for Docker action).
-- Files specified in some keys under `runs` are existing. For example, JavaScript action defines a script file path for
- entrypoint at `main:`.
-- Icon name at `icon:` in `branding:` section is correct. Supported icon names are listed in
- [the official document][branding-icons-doc].
-- Icon color at `color:` in `branding:` section is correct. Supported icon colors are white, yellow, blue, green, orange, red,
- purple, or gray-dark.
-
-actionlint checks action metadata files which are used by workflows. Currently it is not supported to specify `action.yml`
-directly via command line arguments.
-
-Note that `steps` in Composite action's metadata is not checked at this point. It will be supported in the future.
-
----
-
-[Installation](install.md) | [Usage](usage.md) | [Configuration](config.md) | [Go API](api.md) | [References](reference.md)
-
-[yamllint]: https://github.com/adrienverge/yamllint
-[issue-form]: https://github.com/rhysd/actionlint/issues/new
-[syntax-doc]: https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions
-[filter-pattern-doc]: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#filter-pattern-cheat-sheet
-[shellcheck]: https://github.com/koalaman/shellcheck
-[shellcheck-install]: https://github.com/koalaman/shellcheck#installing
-[SC1091]: https://github.com/koalaman/shellcheck/wiki/SC1091
-[SC2050]: https://github.com/koalaman/shellcheck/wiki/SC2050
-[SC2194]: https://github.com/koalaman/shellcheck/wiki/SC2194
-[SC2154]: https://github.com/koalaman/shellcheck/wiki/SC2154
-[SC2157]: https://github.com/koalaman/shellcheck/wiki/SC2157
-[shellcheck-env-var]: https://github.com/koalaman/shellcheck/wiki/Integration#environment-variables
-[pyflakes]: https://github.com/PyCQA/pyflakes
-[expr-doc]: https://docs.github.com/en/actions/learn-github-actions/expressions
-[contexts-doc]: https://docs.github.com/en/actions/learn-github-actions/contexts
-[funcs-doc]: https://docs.github.com/en/actions/learn-github-actions/expressions#functions
-[needs-doc]: https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#jobsjob_idneeds
-[needs-context-doc]: https://docs.github.com/en/actions/learn-github-actions/contexts#needs-context
-[shell-doc]: https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#using-a-specific-shell
-[matrix-doc]: https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#jobsjob_idstrategymatrix
-[webhook-doc]: https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#webhook-events
-[schedule-event-doc]: https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#scheduled-events
-[cron-syntax]: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/crontab.html#tag_20_25_07
-[gh-hosted-runner]: https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners
-[self-hosted-runner]: https://docs.github.com/en/actions/hosting-your-own-runners/about-self-hosted-runners
-[action-uses-doc]: https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#jobsjob_idstepsuses
-[dependabot-doc]: https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot
-[credentials-doc]: https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#jobsjob_idcontainercredentials
-[actions-cache]: https://github.com/actions/cache
-[permissions-doc]: https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token
-[perm-config-doc]: https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#permissions
-[generate-webhook-events]: https://github.com/rhysd/actionlint/tree/main/scripts/generate-webhook-events
-[generate-popular-actions]: https://github.com/rhysd/actionlint/tree/main/scripts/generate-popular-actions
-[issue-25]: https://github.com/rhysd/actionlint/issues/25
-[issue-40]: https://github.com/rhysd/actionlint/issues/40
-[security-doc]: https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions
-[reusable-workflow-doc]: https://docs.github.com/en/actions/learn-github-actions/reusing-workflows
-[create-reusable-workflow-doc]: https://docs.github.com/en/actions/learn-github-actions/reusing-workflows#creating-a-reusable-workflow
-[reusable-workflow-call-keys]: https://docs.github.com/en/actions/learn-github-actions/reusing-workflows#supported-keywords-for-jobs-that-call-a-reusable-workflow
-[object-filter-syntax]: https://docs.github.com/en/actions/learn-github-actions/expressions#object-filters
-[github-script]: https://github.com/actions/github-script
-[workflow-dispatch-event]: https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#workflow_dispatch
-[workflow-dispatch-input-type-announce]: https://github.blog/changelog/2021-11-10-github-actions-input-types-for-manual-workflows/
-[reusable-workflow-outputs]: https://docs.github.com/en/actions/using-workflows/reusing-workflows#using-outputs-from-a-reusable-workflow
-[inherit-secrets-announce]: https://github.blog/changelog/2022-05-03-github-actions-simplify-using-secrets-with-reusable-workflows/
-[specific-paths-doc]: https://docs.github.com/en/actions/using-workflows/triggering-a-workflow#using-filters-to-target-specific-paths-for-pull-request-or-push-events
-[availability-doc]: https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability
-[deprecate-set-output-save-state]: https://github.blog/changelog/2022-10-11-github-actions-deprecating-save-state-and-set-output-commands/
-[deprecate-set-env-add-path]: https://github.blog/changelog/2020-10-01-github-actions-deprecating-set-env-and-add-path-commands/
-[workflow-commands-doc]: https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions
-[action-metadata-doc]: https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions
-[branding-icons-doc]: https://github.com/github/docs/blob/main/content/actions/creating-actions/metadata-syntax-for-github-actions.md#exhaustive-list-of-all-currently-supported-icons
-[operators-doc]: https://docs.github.com/en/actions/learn-github-actions/expressions#operators
diff --git a/docs/ci/WORKFLOWS.md b/docs/ci/WORKFLOWS.md
index 32db46fe02..6b31623312 100644
--- a/docs/ci/WORKFLOWS.md
+++ b/docs/ci/WORKFLOWS.md
@@ -98,6 +98,7 @@ The agent workflows coordinate Codex and chat orchestration across topics:
* [`agents-74-pr-body-writer.yml`](../../.github/workflows/agents-74-pr-body-writer.yml) synchronizes PR body sections from source issues and builds status summaries.
* [`agents-pr-meta.yml`](../../.github/workflows/agents-pr-meta.yml) manages PR metadata and automated status updates. (Deprecated; see `agents-pr-meta-v2.yml`.)
* [`agents-pr-meta-v2.yml`](../../.github/workflows/agents-pr-meta-v2.yml) is the active replacement for `agents-pr-meta.yml` with identical functionality.
+* [`agents-pr-meta-v3.yml`](../../.github/workflows/agents-pr-meta-v3.yml) is a minimal test workflow to verify GitHub workflow registration.
* [`agents-75-keepalive-on-gate.yml`](../../.github/workflows/agents-75-keepalive-on-gate.yml) implements the keepalive-on-gate consolidation and gate-aware keepalive behavior.
* [`reusable-16-agents.yml`](../../.github/workflows/reusable-16-agents.yml) includes the keepalive sweep, which the orchestrator toggles via the `keepalive_enabled` flag and repository-level `keepalive:paused` label.
* [`agents-63-issue-intake.yml`](../../.github/workflows/agents-63-issue-intake.yml) is the canonical front door. It now listens for `agent:codex` labels directly and routes both label triggers and ChatGPT sync requests through the shared normalization pipeline.
diff --git a/docs/ci/WORKFLOW_SYSTEM.md b/docs/ci/WORKFLOW_SYSTEM.md
index b30c2670a0..49a911c28a 100644
--- a/docs/ci/WORKFLOW_SYSTEM.md
+++ b/docs/ci/WORKFLOW_SYSTEM.md
@@ -552,6 +552,8 @@ Keep this table handy when you are triaging automation: it confirms which workfl
favor of `agents-pr-meta-v2.yml` due to GitHub workflow registration issue.)
- **Agents PR meta manager v2** – `.github/workflows/agents-pr-meta-v2.yml` is
the active replacement for `agents-pr-meta.yml` with identical functionality.
+- **Agents PR meta manager v3** – `.github/workflows/agents-pr-meta-v3.yml` is
+ a minimal test workflow to verify GitHub workflow registration.
- **Keepalive sweep (orchestrator only).** The Agents 70 Orchestrator provides
the single, consolidated keepalive path. The orchestrator passes the
`enable_keepalive` flag into `reusable-16-agents.yml`, which executes the
diff --git a/docs/config.md b/docs/config.md
index 2d5775f0f6..0da3570992 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -1,52 +1,105 @@
-Configuration
-=============
+# Configuration Reference
-This document describes how to configure [actionlint](..) behavior.
+This document describes the configuration options for the Trend Model Project.
-Note that configuration file is optional. The author tries to keep configuration file as minimal as possible not to
-bother users to configure behavior of actionlint. Running actionlint without configuration file would work fine in most
-cases.
+## Configuration Files
-## Configuration file
+### File Locations
-Configuration file `actionlint.yaml` or `actionlint.yml` can be put in `.github` directory.
+| File | Purpose |
+|------|---------|
+| `config/defaults.yml` | Default values (fallback) |
+| `config/demo.yml` | Demo/test configuration |
+| `config/presets/*.yml` | Risk profile presets |
+| `config/universe/*.yml` | Universe definitions |
-Note: If you're using [Super-Linter][], the file should be placed in a different directory. Please check the project's document.
+### Loading Priority
-You don't need to write the first configuration file by your hand. `actionlint` command can generate a default configuration
-with `-init-config` flag.
+1. Command-line argument: `-c path/to/config.yml`
+2. Environment variable: `TREND_CFG=path/to/config.yml`
+3. Default: `config/defaults.yml`
-```sh
-actionlint -init-config
-vim .github/actionlint.yaml
+## Configuration Sections
+
+### Data Section
+
+```yaml
+data:
+ csv_path: demo/demo_returns.csv
+ date_column: Date
+ columns: null # null = all columns except date
+ missing_policy: drop # drop, ffill, zero
+ missing_fill_limit: 3 # max consecutive fills
+```
+
+### Portfolio Section
+
+```yaml
+portfolio:
+ top_n: 8 # Number of assets to select
+ lookback: 12 # Months for trend calculation
+ rebalance_frequency: M # M=monthly, Q=quarterly
+ weighting: equal # equal, score_prop, vol_adjusted
+
+ # Selection options
+ selection:
+ mode: rank # all, random, manual, rank
+ score_by: Sharpe # Sharpe, AnnualReturn, blended
+```
+
+### Risk Section
+
+```yaml
+risk:
+ vol_target: 0.10 # Target volatility (10%)
+ max_position: 0.25 # Max 25% per position
+ min_position: 0.02 # Min 2% per position
```
-Currently only one item can be configured.
+### Output Section
```yaml
-self-hosted-runner:
- # Labels of self-hosted runner in array of string
- labels:
- - linux.2xlarge
- - windows-latest-xl
- - linux-multi-gpu
-# Configuration variables in array of strings defined in your repository or organization
-config-variables:
- - DEFAULT_RUNNER
- - JOB_NAME
- - ENVIRONMENT_STAGE
+output:
+ format: excel # csv, json, excel
+ path: outputs/analysis # Output directory/prefix
+ include_raw_metrics: true # Include detailed metrics
+```
+
+### Walk-Forward Section
+
+```yaml
+walk_forward:
+ train: 36 # In-sample months
+ test: 12 # Out-of-sample months
+ step: 6 # Step size
+```
+
+## Preset Configurations
+
+Pre-built risk profiles in `config/presets/`:
+
+| Preset | Description |
+|--------|-------------|
+| `conservative.yml` | Lower risk, stable returns |
+| `balanced.yml` | Moderate risk/return |
+| `aggressive.yml` | Higher risk, growth focus |
+| `cash_constrained.yml` | Limited cash allocation |
+
+### Using Presets
+
+```bash
+PYTHONPATH="./src" python -m trend_analysis.run_analysis -c config/presets/balanced.yml
```
-- `self-hosted-runner`: Configuration for your self-hosted runner environment.
- - `labels`: Label names added to your self-hosted runners as list of pattern. Glob syntax supported by [`path.Match`][pat]
- is available.
-- `config-variables`: [Configuration variables][vars]. When an array is set, actionlint will check `vars` properties strictly.
- An empty array means no variable is allowed. The default value `null` disables the check.
+## Environment Variables
----
+| Variable | Purpose |
+|----------|---------|
+| `TREND_CFG` | Default config file path |
+| `PYTHONPATH` | Add `./src` for module imports |
-[Checks](checks.md) | [Installation](install.md) | [Usage](usage.md) | [Go API](api.md) | [References](reference.md)
+## See Also
-[Super-Linter]: https://github.com/super-linter/super-linter
-[pat]: https://pkg.go.dev/path#Match
-[vars]: https://docs.github.com/en/actions/learn-github-actions/variables
+- [ConfigMap.md](ConfigMap.md) - Complete config file inventory
+- [PresetStrategies.md](PresetStrategies.md) - Preset details
+- [UserGuide.md](UserGuide.md) - User documentation
diff --git a/assets/screenshots/template-section.png b/docs/directory-index/README.md
similarity index 100%
rename from assets/screenshots/template-section.png
rename to docs/directory-index/README.md
diff --git a/docs/directory-index/ROOT.md b/docs/directory-index/ROOT.md
new file mode 100644
index 0000000000..3d59c874d8
--- /dev/null
+++ b/docs/directory-index/ROOT.md
@@ -0,0 +1,99 @@
+# 📂 Repository Root Directory Index
+
+> **Last updated:** November 2025
+> **Purpose:** Quick reference for all root-level files and their roles
+
+---
+
+## 🏗️ Build & Configuration
+
+| File | Purpose |
+|------|---------|
+| `pyproject.toml` | Python package configuration, dependencies, and build settings |
+| `pytest.ini` | Pytest configuration and markers |
+| `requirements.lock` | Pinned dependency versions for reproducible installs |
+| `Makefile` | Build automation targets |
+| `MANIFEST.in` | Package manifest for distribution |
+| `cliff.toml` | Git-cliff changelog generator configuration |
+
+## 🐳 Container & DevOps
+
+| File | Purpose |
+|------|---------|
+| `Dockerfile` | Container image definition |
+| `docker-compose.yml` | Multi-container orchestration |
+| `.dockerignore` | Files excluded from Docker builds |
+| `.hadolint.yaml` | Dockerfile linting rules |
+
+## 🔧 Development Tools
+
+| File | Purpose |
+|------|---------|
+| `.coveragerc` | Default coverage configuration |
+| `.coveragerc.core` | Core module coverage settings |
+| `.coveragerc.full` | Full coverage configuration |
+| `.coveragerc.workflows` | Workflow-specific coverage settings |
+| `.flake8` | Flake8 linting configuration |
+| `.pre-commit-config.yaml` | Pre-commit hook definitions |
+| `.gitignore` | Git ignore patterns |
+| `.gitattributes` | Git file attributes |
+
+## 📖 Documentation
+
+| File | Purpose |
+|------|---------|
+| `README.md` | Primary project documentation |
+| `README_APP.md` | Streamlit application guide |
+| `README_DATA.md` | Data provenance and usage guide |
+| `CHANGELOG.md` | Version history and release notes |
+| `CONTRIBUTING.md` | Contribution guidelines |
+| `CODE_OF_CONDUCT.md` | Community standards |
+| `SECURITY.md` | Security policy and disclosure |
+| `DEPENDENCY_QUICKSTART.md` | Quick dependency setup guide |
+| `DOCKER_QUICKSTART.md` | Docker usage guide |
+| `LICENSE` | MIT License |
+
+## 🤖 AI Agent Instructions
+
+| File | Purpose |
+|------|---------|
+| `Agents.md` | Codex agent instructions and workflow guidance |
+| `Issues.txt` | Issue templates for keepalive automation |
+
+## 📊 CI-Generated Status Files
+
+| File | Purpose |
+|------|---------|
+| `coverage-summary.md` | Coverage trend snapshot (CI-generated) |
+| `gate-summary.md` | PR gate status (CI-generated) |
+| `keepalive_status.md` | Keepalive status index |
+
+---
+
+## 📁 Directory Structure
+
+| Folder | Purpose | Index |
+|--------|---------|-------|
+| `agents/` | Active Codex instruction files (1 active, rest archived) | [View](agents.md) |
+| `analysis/` | Analysis modules and results helpers | [View](analysis.md) |
+| `archives/` | Historical artifacts and retired code (incl. 415+ agent files) | [View](archives.md) |
+| `assets/` | Static assets (images, templates) | [View](assets.md) |
+| `config/` | YAML configuration files | [View](config.md) |
+| `data/` | Reference datasets and raw data | [View](data.md) |
+| `demo/` | Demo outputs and generated samples | [View](demo.md) |
+| `docs/` | Project documentation | [View](docs.md) |
+| `examples/` | Example scripts and notebooks | [View](examples.md) |
+| `man/` | Manual pages | [View](man.md) |
+| `notebooks/` | Jupyter notebooks | [View](notebooks.md) |
+| `perf/` | Performance benchmarks and reports | [View](perf.md) |
+| `reports/` | Generated reports | [View](reports.md) |
+| `retired/` | Deprecated modules awaiting removal | [View](retired.md) |
+| `scripts/` | Utility and automation scripts | [View](scripts.md) |
+| `src/` | Main source code | [View](src.md) |
+| `streamlit_app/` | Streamlit web application | [View](streamlit_app.md) |
+| `tests/` | Test suite | [View](tests.md) |
+| `tools/` | Development and build tools | [View](tools.md) |
+
+---
+
+*See individual folder indexes for detailed contents.*
diff --git a/docs/directory-index/config.md b/docs/directory-index/config.md
new file mode 100644
index 0000000000..cd16390466
--- /dev/null
+++ b/docs/directory-index/config.md
@@ -0,0 +1,48 @@
+# 📂 `config/` — Configuration Directory
+
+> **Purpose:** YAML configuration files for analysis runs
+> **Last updated:** November 2025
+
+---
+
+## 📄 Configuration Files
+
+| File | Description |
+|------|-------------|
+| `defaults.yml` | Default configuration values |
+| `demo.yml` | Demo run configuration |
+| `portfolio_test.yml` | Portfolio testing configuration |
+| `trend_universe_2004.yml` | Trend universe 2004 configuration |
+| `trend_concentrated_2004.yml` | Concentrated trend strategy |
+| `long_backtest.yml` | Long backtest configuration |
+| `robust_demo.yml` | Robustness testing configuration |
+| `walk_forward.yml` | Walk-forward analysis settings |
+| `trend.toml` | TOML-format trend settings |
+| `coverage-baseline.json` | Coverage baseline for CI |
+
+## 📁 Subdirectories
+
+### `universe/`
+Universe definition files:
+- `core.yml` — Core universe definition
+- `core_plus_benchmarks.yml` — Core with benchmark indices
+- `managed_futures_min.yml` — Minimal managed futures universe
+
+### `presets/`
+Pre-configured analysis presets for common use cases.
+
+---
+
+## 🔧 Usage
+
+```bash
+# Run with specific config
+python -m trend_analysis.run_analysis -c config/demo.yml
+
+# Use environment variable
+TREND_CFG=config/defaults.yml python -m trend_analysis.run_analysis
+```
+
+---
+
+*See `docs/configuration.md` for full configuration reference.*
diff --git a/docs/directory-index/data.md b/docs/directory-index/data.md
new file mode 100644
index 0000000000..91d458700b
--- /dev/null
+++ b/docs/directory-index/data.md
@@ -0,0 +1,55 @@
+# 📂 `data/` — Reference Datasets
+
+> **Purpose:** Demo datasets and reference data for tests and examples
+> **Last updated:** November 2025
+
+---
+
+## 📊 Primary Datasets
+
+| File | Size | Description |
+|------|------|-------------|
+| `Trend Universe Data.csv` | 136 KB | Monthly total returns for Trend universe funds |
+| `Trend Universe Membership.csv` | 4 KB | Fund membership effective dates |
+| `hedge_fund_returns_with_indexes.csv` | 140 KB | Hedge fund returns with benchmark indices |
+
+## 📁 Subdirectories
+
+### `raw/`
+Raw input data organized by type:
+
+#### `raw/managers/`
+- `sample_manager.csv` — Minimal manager fixture for tests
+
+#### `raw/indices/`
+- `sample_index.csv` — Sample benchmark index data
+
+---
+
+## 📋 Schema Reference
+
+### Trend Universe Data
+- **Columns:** `Date` + fund/index return series (decimal percentages)
+- **Frequency:** Monthly
+- **Pair with:** Membership ledger for effective windows
+
+### Trend Universe Membership
+- **Columns:** `fund`, `effective_date`, `end_date`
+- **Purpose:** Maps each fund to its active date range
+
+### Hedge Fund Returns
+- **Columns:** `Date`, `Risk-Free Rate`, fund returns
+- **Frequency:** Monthly
+- **Use:** Long backtests and rolling-hold configs
+
+---
+
+## ⚠️ Important Notes
+
+1. **Synthetic Data:** All datasets are synthetic or derived from public benchmarks
+2. **Demo Only:** Not suitable for production trading decisions
+3. **Provenance:** See `README_DATA.md` for full details
+
+---
+
+*See `docs/data/Trend_Universe_Data.md` for stewardship notes.*
diff --git a/docs/directory-index/docs.md b/docs/directory-index/docs.md
new file mode 100644
index 0000000000..70c4a75637
--- /dev/null
+++ b/docs/directory-index/docs.md
@@ -0,0 +1,43 @@
+# 📂 `docs/` — Documentation
+
+> **Purpose:** Project documentation and guides
+> **Last updated:** November 2025
+
+---
+
+## 📁 Key Subdirectories
+
+| Directory | Description |
+|-----------|-------------|
+| `archive/` | Archived documentation |
+| `data/` | Data documentation and schemas |
+| `directory-index/` | This directory index system |
+| `keepalive/` | Keepalive workflow documentation |
+| `phase-1/` | Phase 1 implementation docs |
+| `phase-2/` | Phase 2 implementation docs |
+| `workflows/` | GitHub workflow documentation |
+
+---
+
+## 📄 Key Documents
+
+| Document | Description |
+|----------|-------------|
+| `INDEX.md` | Main documentation index |
+| `architecture.md` | System architecture overview |
+| `configuration.md` | Configuration reference |
+| `walk_forward.md` | Walk-forward analysis guide |
+
+---
+
+## 🔗 Quick Links
+
+- **Getting Started:** See `README.md` in root
+- **Configuration:** `docs/configuration.md`
+- **Architecture:** `docs/architecture.md`
+- **Workflows:** `docs/workflows/`
+- **Data Schemas:** `docs/data/`
+
+---
+
+*For historical documentation, see `archives/docs/`.*
diff --git a/docs/directory-index/scripts.md b/docs/directory-index/scripts.md
new file mode 100644
index 0000000000..608f4a6c6a
--- /dev/null
+++ b/docs/directory-index/scripts.md
@@ -0,0 +1,86 @@
+# 📂 `scripts/` — Utility Scripts
+
+> **Purpose:** Automation, development, and CI/CD scripts
+> **Last updated:** November 2025
+
+---
+
+## 🚀 Quick Start Scripts
+
+| Script | Description |
+|--------|-------------|
+| `setup_env.sh` | Bootstrap virtual environment (60-180s) |
+| `run_tests.sh` | Run full test suite with coverage |
+| `run_streamlit.sh` | Launch Streamlit web application |
+| `generate_demo.py` | Generate demo dataset |
+
+## 🔍 Validation Scripts
+
+| Script | Description |
+|--------|-------------|
+| `dev_check.sh` | Fast development validation (2-5s) |
+| `validate_fast.sh` | Adaptive validation (5-30s) |
+| `check_branch.sh` | Comprehensive pre-merge validation (30-120s) |
+| `quality_gate.sh` | Quality gate enforcement |
+| `quick_check.sh` | Rapid syntax/import check |
+
+## 🔧 CI/CD Scripts
+
+| Script | Description |
+|--------|-------------|
+| `ci_cosmetic_repair.py` | Auto-fix cosmetic issues |
+| `ci_coverage_delta.py` | Calculate coverage changes |
+| `ci_history.py` | CI run history tracking |
+| `ci_metrics.py` | CI metrics collection |
+| `workflow_lint.sh` | Lint GitHub workflow files |
+| `workflow_smoke_tests.py` | Workflow smoke tests |
+
+## 📊 Analysis & Reporting
+
+| Script | Description |
+|--------|-------------|
+| `run_multi_demo.py` | Multi-period demo runner |
+| `walk_forward.py` | Walk-forward analysis |
+| `benchmark_performance.py` | Performance benchmarking |
+| `compare_perf.py` | Performance comparison |
+| `generate_residual_report.py` | Residual analysis reports |
+
+## 🤖 Automation
+
+| Script | Description |
+|--------|-------------|
+| `codex_git_bootstrap.sh` | Codex agent git setup |
+| `keepalive-runner.js` | Keepalive workflow runner |
+| `open_pr_from_issue.sh` | Create PR from issue |
+| `git_hooks.sh` | Install git hooks |
+
+## 🛠️ Development Tools
+
+| Script | Description |
+|--------|-------------|
+| `fix_common_issues.sh` | Auto-fix common problems |
+| `mypy_autofix.py` | Auto-fix type errors |
+| `prune_allowlist.py` | Prune lint allowlists |
+| `sync_tool_versions.py` | Sync tool versions |
+
+---
+
+## 📋 Common Workflows
+
+### Development Cycle
+```bash
+./scripts/dev_check.sh --changed --fix # Quick validation
+./scripts/validate_fast.sh --fix # Before commit
+./scripts/check_branch.sh --fast --fix # Before merge
+```
+
+### Demo Pipeline
+```bash
+./scripts/setup_env.sh
+python scripts/generate_demo.py
+python scripts/run_multi_demo.py
+```
+
+---
+
+*See `.github/copilot-instructions.md` for timing expectations.*
diff --git a/docs/directory-index/src.md b/docs/directory-index/src.md
new file mode 100644
index 0000000000..1108c331c9
--- /dev/null
+++ b/docs/directory-index/src.md
@@ -0,0 +1,48 @@
+# 📂 `src/` — Source Code Directory
+
+> **Purpose:** Main application source code
+> **Last updated:** November 2025
+
+---
+
+## 📦 Packages
+
+| Package | Description |
+|---------|-------------|
+| `trend_analysis/` | Core trend analysis engine and pipeline |
+| `trend_model/` | Trend model implementation |
+| `trend_portfolio_app/` | Portfolio application components |
+| `backtest/` | Backtesting framework |
+| `data/` | Data loading and validation |
+| `health_summarize/` | Health check summarization |
+| `trend/` | Trend signal generation |
+| `utils/` | Shared utilities |
+
+## 📄 Root Files
+
+| File | Purpose |
+|------|---------|
+| `__init__.py` | Package initialization |
+| `cli.py` | Command-line interface entry point |
+
+---
+
+## 🔗 Key Subpackages
+
+### `trend_analysis/`
+The primary analysis package containing:
+- Pipeline orchestration
+- Metrics computation
+- Configuration management
+- Multi-period analysis engine
+- Export functionality
+
+### `trend_portfolio_app/`
+Streamlit web application components for interactive portfolio analysis.
+
+### `backtest/`
+Walk-forward and backtesting utilities for strategy validation.
+
+---
+
+*See `docs/architecture.md` for detailed module relationships.*
diff --git a/docs/directory-index/tests.md b/docs/directory-index/tests.md
new file mode 100644
index 0000000000..1dc451e5ad
--- /dev/null
+++ b/docs/directory-index/tests.md
@@ -0,0 +1,88 @@
+# 📂 `tests/` — Test Suite
+
+> **Purpose:** Comprehensive unit and integration tests
+> **Last updated:** November 2025
+> **Test count:** 400+ tests | **Coverage target:** 70%+
+
+---
+
+## 📁 Structure
+
+| Directory | Description |
+|-----------|-------------|
+| `app/` | Application-level tests |
+| `backtesting/` | Backtest engine tests |
+| `data/` | Data loading/validation tests |
+| `fixtures/` | Test fixtures and sample data |
+| `github_scripts/` | GitHub workflow script tests |
+| `golden/` | Golden master comparison files |
+| `proxy/` | Proxy server tests |
+| `scripts/` | Script tests |
+| `smoke/` | Smoke tests for quick validation |
+| `soft_coverage/` | Soft coverage tracking |
+| `tools/` | Tool tests |
+| `trend_analysis/` | Core analysis tests |
+| `unit/` | Pure unit tests |
+
+---
+
+## 🧪 Test Categories
+
+### Core Analysis
+- `test_pipeline*.py` — Pipeline orchestration
+- `test_metrics*.py` — Financial metrics
+- `test_config*.py` — Configuration loading
+- `test_export*.py` — Export functionality
+
+### Multi-Period Engine
+- `test_multi_period_engine*.py` — Rolling analysis engine
+- `test_multi_period_export.py` — Period export tests
+- `test_multi_period_selection.py` — Manager selection
+
+### Data & Validation
+- `test_data*.py` — Data loading
+- `test_validators*.py` — Input validation
+- `test_market_data*.py` — Market data handling
+
+### UI & Application
+- `test_streamlit*.py` — Streamlit app tests
+- `test_gui*.py` — GUI component tests
+- `test_cli*.py` — CLI tests
+
+### Workflows & CI
+- `test_workflow*.py` — Workflow tests
+- `test_autofix*.py` — Autofix pipeline tests
+- `test_keepalive*.py` — Keepalive tests
+
+---
+
+## 🚀 Running Tests
+
+```bash
+# Full suite with coverage
+./scripts/run_tests.sh
+
+# Quick run
+pytest -q
+
+# Specific category
+pytest tests/test_pipeline*.py
+
+# With coverage report
+pytest --cov=trend_analysis --cov-branch
+```
+
+---
+
+## 📋 Key Files
+
+| File | Purpose |
+|------|---------|
+| `conftest.py` | Pytest fixtures and configuration |
+| `quarantine.yml` | Quarantined flaky tests |
+| `sitecustomize.py` | Test environment customization |
+| `score_frame_golden.csv` | Golden master for score frame |
+
+---
+
+*See `pytest.ini` for test configuration.*
diff --git a/docs/fastapi-migration.md b/docs/fastapi-migration.md
index 13cb6d2011..f026a84e55 100644
--- a/docs/fastapi-migration.md
+++ b/docs/fastapi-migration.md
@@ -6,10 +6,14 @@ This document explains the migration from deprecated `@app.on_event()` decorator
### 1. Updated Dependencies
-Added FastAPI and Uvicorn to `requirements.txt`:
-```
-fastapi>=0.104.0
-uvicorn[standard]
+Added FastAPI and Uvicorn to `pyproject.toml`:
+```toml
+[project]
+dependencies = [
+ # ... other deps ...
+ "fastapi>=0.104.0",
+ "uvicorn[standard]",
+]
```
### 2. Replaced Basic HTTP Server with FastAPI
diff --git a/docs/install.md b/docs/install.md
index 22dd9922d8..d071363f21 100644
--- a/docs/install.md
+++ b/docs/install.md
@@ -1,179 +1,82 @@
-Installation
-============
+# Installation Guide
-This document describes how to install [actionlint](../docs).
+This guide covers how to install and set up the Trend Model Project.
-## Windows
+## Quick Start
-### [Chocolatey](https://chocolatey.org/)
+The fastest way to get started:
-[`actionlint` package][chocolatey] is available in the community repo:
-
-```powershell
-choco install actionlint
-```
-
-### [Scoop](https://scoop.sh/)
-
-[`actionlint` package][scoop] is available in the main bucket:
-
-```powershell
-scoop install actionlint
-```
-
-### [Winget](https://learn.microsoft.com/en-us/windows/package-manager/)
-
-[`actionlint` package][winget] is available in the winget-pkgs repo:
-
-```powershell
-winget install actionlint
-```
-
-## Linux
-
-### [Arch Linux](https://archlinux.org/)
-
-[`actionlint` package][archlinux] is available in the official repo:
-
-```sh
-pacman -S actionlint
-```
-
-Alternatively actionlint is also available on [AUR][aur]. The packages can be installed via [`paru`][paru] command.
-
-- [actionlint-bin](https://aur.archlinux.org/packages/actionlint-bin)
-- [actionlint-git](https://aur.archlinux.org/packages/actionlint-git)
-
-### [Nix](https://nixos.wiki/)
-
-[`actionlint` package][nixpkgs] is available in the Nix ecosystem:
-
-On NixOS:
-
-```sh
-nix-env -iA nixos.actionlint
-```
-
-On Non NixOS:
-
-```sh
-nix-env -iA nixpkgs.actionlint
-```
-
-## macOS
-
-### [Homebrew][homebrew]
-
-[`actionlint`][formula] formula is provided by Homebrew officially.
+```bash
+# Clone the repository
+git clone https://github.com/stranske/Trend_Model_Project.git
+cd Trend_Model_Project
-```sh
-brew install actionlint
-```
+# Set up the environment
+./scripts/setup_env.sh
-Alternatively rhysd/actionlint repository also provides its own Homebrew package, which is automatically updated on new release.
-If you prefer it, tap the repository before running `brew install`.
+# Generate demo data
+python scripts/generate_demo.py
-```sh
-brew tap "rhysd/actionlint" "https://github.com/rhysd/actionlint"
-brew install actionlint
+# Run the analysis
+PYTHONPATH="./src" python -m trend_analysis.run_analysis -c config/demo.yml
```
-## Prebuilt binaries
-
-Download an archive file from [the releases page][releases] for your platform, unarchive it and put the executable file to a
-directory in `$PATH`.
-
-Prebuilt binaries are built at each release by CI for the following OS and arch:
+## Prerequisites
-- macOS (x86_64, arm64)
-- Linux (i386, x86_64, arm32, arm64)
-- Windows (i386, x86_64, arm64)
-- FreeBSD (i386, x86_64)
+- **Python 3.11+** (required)
+- **Git** (for cloning)
+- **Node.js v20+** (optional, for workflow tests)
+- **uv** (optional, for fast dependency management)
-Note: The following targets are not tested since GitHub Actions doesn't support them:
+## Installation Methods
-- Linux i386, arm32, arm64
-- Windows i386, arm64
-- FreeBSD i386, x86_64
+### 1. Development Installation (Recommended)
-
-## Download script
+```bash
+# Create and activate virtual environment
+python -m venv .venv
+source .venv/bin/activate # On Windows: .venv\Scripts\activate
-To install `actionlint` executable with one command, [the download script](../scripts/download-actionlint.bash) is available.
-It downloads the latest version of actionlint (`actionlint.exe` on Windows and `actionlint` on other OSes) to the current
-directory automatically. This is a recommended way if you install actionlint in some shell script.
+# Install dependencies
+pip install uv
+uv pip sync requirements.lock
-```sh
-bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash)
+# Install package in editable mode
+pip install --no-deps -e '.[dev]'
```
-When you need to install specific version of actionlint, please give the version to the 1st command line argument. The following
-example installs v1.6.17.
+### 2. Using setup script
-```sh
-bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash) 1.6.17
+```bash
+./scripts/setup_env.sh
+source .venv/bin/activate
```
-This script downloads `actionlint` (or `actionlint.exe` on Windows) binary to the current working directory. When you need to put
-the downloaded binary to some other directory, please give the directory path to the 2nd command line argument. The following
-example installs the latest version to `/usr/bin`.
+### 3. Docker (No local setup needed)
-```sh
-bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash) latest /usr/bin
+```bash
+docker run -p 8501:8501 ghcr.io/stranske/trend-model:latest
```
-For the usage of actionlint on GitHub Actions, see [the usage document](usage.md#on-github-actions).
-
-## Docker image
-
-See [the usage document](./usage.md#docker) to know how to install and use an official actionlint Docker image.
+Visit http://localhost:8501 after the container starts.
-## asdf
+## Verification
-You can install actionlint with the [asdf version manager][asdf] using the [asdf-actionlint][asdf-plugin] plugin, which
-automates the process of installing (and switching between) various versions of GitHub release binaries. With asdf already
-installed, run these commands to install actionlint:
+After installation, verify everything works:
```bash
-# Add actionlint plugin
-asdf plugin add actionlint
+# Run tests
+./scripts/run_tests.sh
-# Show all installable versions
-asdf list-all actionlint
+# Check CLI
+PYTHONPATH="./src" python -m trend_analysis.run_analysis --help
-# Install specific version
-asdf install actionlint latest
-
-# Set a version globally (on your ~/.tool-versions file)
-asdf global actionlint latest
+# Launch Streamlit app
+./scripts/run_streamlit.sh
```
-## Build from source
-
-Recent [Go][] toolchain is necessary to build actionlint from source. Use Go 1.16 or later.
-
-```sh
-# Install the latest stable version
-go install github.com/rhysd/actionlint/cmd/actionlint@latest
-
-# Install the head of main branch
-go install github.com/rhysd/actionlint/cmd/actionlint
-```
+## Next Steps
----
-
-[Checks](checks.md) | [Usage](usage.md) | [Configuration](config.md) | [Go API](api.md) | [References](reference.md)
-
-[formula]: https://formulae.brew.sh/formula/actionlint
-[homebrew]: https://brew.sh/
-[releases]: https://github.com/rhysd/actionlint/releases
-[Go]: https://golang.org/
-[asdf]: https://asdf-vm.com/
-[asdf-plugin]: https://github.com/crazy-matt/asdf-actionlint
-[chocolatey]: https://community.chocolatey.org/packages/actionlint
-[scoop]: https://scoop.sh/#/apps?q=actionlint&s=0&d=1&o=true
-[winget]: https://github.com/microsoft/winget-pkgs/tree/master/manifests/r/rhysd/actionlint
-[archlinux]: https://archlinux.org/packages/extra/x86_64/actionlint/
-[aur]: https://aur.archlinux.org/
-[paru]: https://github.com/Morganamilo/paru
-[nixpkgs]: https://github.com/NixOS/nixpkgs/blob/master/pkgs/development/tools/analysis/actionlint/default.nix
+- See [quickstart.md](quickstart.md) for a 10-minute tutorial
+- See [UserGuide.md](UserGuide.md) for comprehensive documentation
+- See [CLI.md](CLI.md) for command-line interface reference
diff --git a/docs/performance_benchmark.md b/docs/performance_benchmark.md
index 8fc9fe0bc2..6d5dd6b7c5 100644
--- a/docs/performance_benchmark.md
+++ b/docs/performance_benchmark.md
@@ -18,10 +18,8 @@ runtime increases by more than `PERF_REGRESSION_PCT` (default 15%). Equal or
smaller increases pass (non-strict comparator).
## Baseline
-Stored at `archives/generated/2025/perf/perf_baseline.json` (generated with
-rows=1200, cols=35, runs=4). The legacy path `perf/perf_baseline.json` remains
-available via a symlink for CI and helper script compatibility. To regenerate
-after intentional optimisation:
+Stored at `perf/perf_baseline.json`. To generate a baseline after intentional
+optimisation:
```bash
python scripts/benchmark_performance.py \
diff --git a/docs/phase-2/Agents.md b/docs/phase-2/Agents.md
index bbacc1e866..7121e51c67 100644
--- a/docs/phase-2/Agents.md
+++ b/docs/phase-2/Agents.md
@@ -232,44 +232,28 @@ exactly as v1.0 did. Breaking that throws `ExportError`.
| ------------------------------------ | -------------------------------------------------------------------------- | --------------------------------------------------------- |
| **Data ingest & cleaning** | `trend_analysis/data.py` (alias exported as `trend_analysis.data`) | `data_utils.py`, helper code in notebooks or `scripts/` |
| **Portfolio logic & metrics** | `trend_analysis/metrics.py` (vectorised) | loops inside `run_analysis.py`, ad‑hoc calcs in notebooks |
-| **Export / I/O** | `trend_analysis/export.py` | the root‑level `exports.py`, snippets inside notebooks |
+| **Export / I/O** | `trend_analysis/export/` package | the root‑level `exports.py`, snippets inside notebooks |
| **Domain kernels (fast primitives)** | `trend_analysis/core/` package | stand‑alone modules under the top‑level `core/` directory |
| **Pipeline orchestration** | `trend_analysis/pipeline.py` (pure) | any duplicated control flow elsewhere |
-| **CLI entry‑point** | `run_analysis.py` **only** (thin wrapper around `trend_analysis.cli:main`) | bespoke `scripts/*.py` entry points |
-| **Config** | `config/defaults.yml` loaded through `trend_analysis.config.load()` | hard‑coded constants, magic numbers in notebooks |
-| **Tests** | `tests/` (pytest; 100 % branch‑aware coverage gate) | — |
+| **CLI entry‑point** | `trend_analysis/run_analysis.py` via `cli:main` | bespoke `scripts/*.py` entry points |
+| **Config** | `trend_analysis/config/` package with `models.py` | hard‑coded constants, magic numbers in notebooks |
+| **Tests** | `tests/` (pytest with coverage gate) | — |
+
One concern → one module.
Replacements must delete or comment‑out whatever they obsolete in the same PR.
-Immediate Refactor Tasks
-Flatten duplications
-
-Rename data_utils.py → trend_analysis/data.py, adjust imports, delete the original.
-
-Migrate the contents of the top‑level exports.py into trend_analysis/export.py; keep only a re‑export stub for one minor release.
-
-Turn the stray core/ directory into an importable sub‑package:
-core/indicator.py → trend_analysis/core/indicator.py, etc.
-
-Single pipeline
-
-Implement trend_analysis/pipeline.py exposing a pure function
-run(config: Config) -> pd.DataFrame.
-
-run_analysis.py should parse CLI args, build a Config, pass it to pipeline.run, then handle pretty printing / file output only.
-
-Config resolution
+### Refactor Status ✅
-# trend_analysis/config.py
-from pydantic import BaseModel
-class Config(BaseModel):
- defaults: str = Path(__file__).with_name("..").joinpath("config/defaults.yml")
- # ...other validated fields...
-def load(path: str | None = None) -> Config: ...
+The following refactors from the original Phase-2 spec are **complete**:
-Env‑var override: TREND_CFG=/path/to/override.yml run_analysis ...
+- ✅ `data.py` – canonical data ingest module
+- ✅ `export/` – package with formatters and multi-period helpers
+- ✅ `core/` – importable sub-package with rank selection and metric cache
+- ✅ `pipeline.py` – pure orchestration function
+- ✅ `config/` – Pydantic models with YAML loading
+- ✅ Env-var override: `TREND_CFG=/path/to/override.yml`
-Dependency hygiene
+### Dependency Hygiene
Heavy imports (numpy, pandas, scipy) at top of each module are fine.
@@ -277,13 +261,13 @@ Keep formatter/test tool versions in lock-step by running `python -m scripts.syn
No circular imports. pipeline.py orchestrates; nothing imports it.
-Tests
+### Tests
NOTE: Test fixtures must be text-serialised (CSV/JSON); no binary formats in PRs.
-Require 100 % branch coverage on trend_analysis/* via pytest‑cov in CI.
+Coverage gate enforced via pytest-cov in CI.
-Conventions & Guard‑rails
+### Conventions & Guard‑rails
Vectorise first.
Falling back to for‑loops requires a comment justifying why vectorisation is impossible or harmful.
diff --git a/docs/reference.md b/docs/reference.md
index 9aa8e26b82..8b280e5a52 100644
--- a/docs/reference.md
+++ b/docs/reference.md
@@ -1,24 +1,48 @@
-References
-==========
-
-This document describes links to resources.
-
-- Repository: https://github.com/rhysd/actionlint
-- Playground: https://rhysd.github.io/actionlint/
-- GitHub Actions official documentations
- - Workflow syntax: https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions
- - Expression syntax: https://docs.github.com/en/actions/learn-github-actions/expressions
- - Built-in functions: https://docs.github.com/en/actions/learn-github-actions/expressions#functions
- - Webhook events: https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#webhook-events
- - Self-hosted runner: https://docs.github.com/en/actions/hosting-your-own-runners/about-self-hosted-runners
- - Security: https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions
-- CRON syntax: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/crontab.html#tag_20_25_07
-- shellcheck: https://github.com/koalaman/shellcheck
-- pyflakes: https://github.com/PyCQA/pyflakes
-- Japanese blog posts
- - GitHub Actions のワークフローをチェックする actionlint をつくった: https://rhysd.hatenablog.com/entry/2021/07/11/214313
- - actionlint v1.4 → v1.6 で実装した新機能の紹介: https://rhysd.hatenablog.com/entry/2021/08/11/221044
-
----
-
-[Checks](checks.md) | [Installation](install.md) | [Usage](usage.md) | [Configuration](config.md) | [Go API](api.md)
+# Reference Documentation
+
+## External Resources
+
+### Project Links
+- **Repository**: https://github.com/stranske/Trend_Model_Project
+- **Documentation**: See `docs/` folder
+
+### Dependencies Documentation
+- **Streamlit**: https://docs.streamlit.io/
+- **FastAPI**: https://fastapi.tiangolo.com/
+- **Pandas**: https://pandas.pydata.org/docs/
+- **Pydantic**: https://docs.pydantic.dev/
+
+### CI/CD Tools
+- **GitHub Actions**: https://docs.github.com/en/actions
+- **actionlint**: https://github.com/rhysd/actionlint
+- **pytest**: https://docs.pytest.org/
+
+## Internal Documentation Index
+
+### Getting Started
+- [quickstart.md](quickstart.md) - 10-minute tutorial
+- [install.md](install.md) - Installation guide
+- [UserGuide.md](UserGuide.md) - Comprehensive user guide
+
+### Configuration
+- [config.md](config.md) - Configuration options
+- [ConfigMap.md](ConfigMap.md) - Configuration file map
+- [PresetStrategies.md](PresetStrategies.md) - Risk profile presets
+
+### CLI & API
+- [CLI.md](CLI.md) - Command-line interface
+- [api.md](api.md) - Python and REST API reference
+
+### Analysis Features
+- [backtesting_harness.md](backtesting_harness.md) - Backtesting framework
+- [walkforward-analysis.md](walkforward-analysis.md) - Walk-forward analysis
+- [walkforward-parameter-grid.md](walkforward-parameter-grid.md) - Parameter optimization
+
+### Development
+- [WORKFLOW_GUIDE.md](WORKFLOW_GUIDE.md) - CI/CD workflows
+- [fast-validation-ecosystem.md](fast-validation-ecosystem.md) - Validation tools
+- [DEPENDENCY_ENFORCEMENT.md](DEPENDENCY_ENFORCEMENT.md) - Dependency management
+
+### Repository Structure
+- [INDEX.md](INDEX.md) - Complete documentation index
+- [repository_housekeeping.md](repository_housekeeping.md) - Maintenance guide
diff --git a/docs/usage.md b/docs/usage.md
index 6e769fd6cc..2746164bb6 100644
--- a/docs/usage.md
+++ b/docs/usage.md
@@ -1,486 +1,113 @@
-Usage
-=====
+# Usage Guide
-This document describes how to use [actionlint](..).
+Quick reference for common Trend Model Project operations.
-## `actionlint` command
+## Analysis
-With no argument, actionlint finds all workflow files in the current repository and checks them.
+### Command Line
-```sh
-actionlint
-```
-
-When paths to YAML workflow files are given as arguments, actionlint checks them.
-
-```sh
-actionlint path/to/workflow1.yaml path/to/workflow2.yaml
-```
-
-When `-` argument is given, actionlint reads inputs from stdin and checks it as workflow source.
-
-```sh
-cat path/to/workflow.yaml | actionlint -
-```
-
-To know all flags and options, see an output of `actionlint -h` or [the online command manual][cmd-manual].
-
-### Ignore some errors
-
-To ignore some errors, `-ignore` option offers to filter errors by messages using regular expression. The option is repeatable.
-The regular expression syntax is the same as [RE2][re2].
-
-```sh
-actionlint -ignore 'label ".+" is unknown' -ignore '".+" is potentially untrusted'
-```
-
-`-shellcheck` and `-pyflakes` specifies file paths of executables. Setting empty string to them disables `shellcheck` and
-`pyflakes` rules. As a bonus, disabling them makes actionlint much faster Since these external linter integrations spawn many
-processes.
-
-```sh
-actionlint -shellcheck= -pyflakes=
-```
-
-
-### Format error messages
-
-`-format` option can flexibly format error messages with [Go template syntax][go-template].
-
-Before explaining the formatting details, let's see some examples.
-
-#### Example: Serialized into JSON
-
-```sh
-actionlint -format '{{json .}}'
-```
-
-Output:
-
-```
-[{"message":"unexpected key \"branch\" for ...
-```
-
-#### Example: Markdown
-
-````sh
-actionlint -format '{{range $err := .}}### Error at line {{$err.Line}}, col {{$err.Column}} of `{{$err.Filepath}}`\n\n{{$err.Message}}\n\n```\n{{$err.Snippet}}\n```\n\n{{end}}'
-````
-
-Output:
-
-````markdown
-### Error at line 21, col 20 of `test.yaml`
-
-property "platform" is not defined in object type {os: string}
-
-```
- key: ${{ matrix.platform }}-node-${{ hashFiles('**/package-lock.json') }}
- ^~~~~~~~~~~~~~~
-```
-````
-
-#### Example: Serialized in [JSON Lines][jsonl]
-
-```sh
-actionlint -format '{{range $err := .}}{{json $err}}{{end}}'
-```
-
-Output:
-
-```
-{"message":"unexpected key \"branch\" for ...
-{"message":"character '\\' is invalid for branch ...
-{"message":"label \"linux-latest\" is unknown. ...
-```
-
-#### Example: [Error annotation][ga-annotate-error] on GitHub Actions
-
-````sh
-actionlint -format '{{range $err := .}}::error file={{$err.Filepath}},line={{$err.Line}},col={{$err.Column}}::{{$err.Message}}%0A```%0A{{replace $err.Snippet "\\n" "%0A"}}%0A```\n{{end}}' -ignore 'SC2016:'
-````
-
-Output:
-
-
-
-To include newlines in the annotation body, it prints `%0A`. (ref [actions/toolkit#193](https://github.com/actions/toolkit/issues/193)).
-And it suppresses `SC2016` shellcheck rule error since it complains about the template argument.
-
-Basically it is more recommended to use [Problem Matchers](#problem-matchers) or reviewdog as explained in
-['Tools integration' section](#tools-integ) below.
-
-#### Example: [SARIF format][sarif]
-
-[The Static Analysis Results Interchange Format (SARIF)][sarif] is a standardized format for the results of static analysis tools.
-
-Since this practical format is much more complex than the above examples, the template is not written here. Please read
-[the template file in test data](../testdata/format/sarif_template.txt).
-
-Outputs are also too large to be written here. Please read [the output example in test data](../testdata/format/test.sarif).
-
-#### Formatting syntax
-
-In [Go template syntax][go-template], `.` within `{{ }}` means the target object. Here, the target object is a sequence of error
-objects.
-
-The sequence can be traversed with `range` action, which is like `for ... = range ... {}` in Go.
-
-```
-{{range $err := .}} this part iterates error objects with the iteration variable $err {{end}}
-```
-
-The error object has the following fields.
-
-| Field | Description | Example |
-|----------------------|-------------------------------------------------------|------------------------------------------------------------------|
-| `{{$err.Message}}` | Body of error message | `property "platform" is not defined in object type {os: string}` |
-| `{{$err.Snippet}}` | Code snippet to indicate error position | ` node_version: 16.x\n ^~~~~~~~~~~~~` |
-| `{{$err.Kind}}` | Name of rule the error belongs to | `expression` |
-| `{{$err.Filepath}}` | Canonical relative file path of the error position | `.github/workflows/ci.yaml` |
-| `{{$err.Line}}` | Line number of the error position (1-based) | `9` |
-| `{{$err.Column}}` | Column number of the error's start position (1-based) | `11` |
-| `{{$err.EndColumn}}` | Column number of the error's end position (1-based) | `23` |
-
-Functions called in `{{ }}` placeholder are template actions. There are many actions defined by Go standard library. In addition,
-there are a few custom actions defined by actionlint. Most useful action would be `json` as we already used it in the above JSON
-example. List of all custom actions are as follows:
-
-| Action | Description | Example usage |
-|------------------|----------------------------------------------------------------------------------|-------------------------------------------|
-| `json x` | Serialize `x` as JSON string followed by newline character | `{{json $err}}` |
-| `replace x y z` | Replace string `y` with `z` in `x` | `{{replace $err.Filepath "\\" "/"}}` |
-| `toPascalCase x` | Convert `x` into PascalCase (e.g. 'foo-bar' to 'FooBar') | `{{toPascalCase $err.Kind}}` |
-| `allKinds` | Return an array of kind objects. The kind object is explained in the below table | `{{range $ = allKinds}}{{$.Name}}{{end}}` |
-| `getVersion` | Return the version of actionlint as string | `{{getVersion}}` |
-
-The kind object returned from `allKinds` action has the following fields.
-
-| Field | Description | Example |
-|-------------------------|-------------------------------|---------------------------------------------|
-| `{{$kind.Name}}` | Name of the kind | `syntax-check` |
-| `{{$kind.Description}}` | Short description of the kind | `Checks for GitHub Actions workflow syntax` |
-
-For example, the following simple iteration body
-
-```
-line is {{$err.Line}}, col is {{$err.Column}}, message is {{$err.Message | printf "%q"}}
-```
-
-will produce output like below.
-
-```
-line is 21, col is 20, message is "property \"platform\" is not defined in object type {os: string}"
-```
-
-In `{{ }}` placeholder, input can be piped and action can be used to transform texts. In above example, the message is piped with
-`|` and transformed with `printf "%q"`.
-
-Note that special characters escaped with back slash like `\n` in the format string are automatically unespcaed.
-
-### Exit status
-
-`actionlint` command exits with one of the following exit statuses.
-
-| Status | Description |
-|--------|---------------------------------------------------------|
-| `0` | The command ran successfully and no problem was found |
-| `1` | The command ran successfully and some problem was found |
-| `2` | The command failed due to invalid command line option |
-| `3` | The command failed due to some fatal error |
-
-
-## Use actionlint on GitHub Actions
-
-Preparing `actionlint` executable with the download script is recommended. See [the instruction](install.md#download-script) for
-more details. It sets an absolute file path of downloaded executable to `executable` output in order to use the executable in the
-following steps easily.
-
-Here is an example of simple workflow to run actionlint on GitHub Actions. Please ensure `shell: bash` since the default
-shell for Windows runners is `pwsh`.
-
-```yaml
-name: Lint GitHub Actions workflows
-on: [push, pull_request]
-
-jobs:
- actionlint:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
- - name: Download actionlint
- id: get_actionlint
- run: bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash)
- shell: bash
- - name: Check workflow files
- run: ${{ steps.get_actionlint.outputs.executable }} -color
- shell: bash
-```
+```bash
+# Basic analysis with config file
+PYTHONPATH="./src" python -m trend_analysis.run_analysis -c config/demo.yml
-Or simply download the executable and run it in one step:
+# Using environment variable
+TREND_CFG=config/demo.yml PYTHONPATH="./src" python -m trend_analysis.run_analysis
-```yaml
-- name: Check workflow files
- run: |
- bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash)
- ./actionlint -color
- shell: bash
+# With default config
+PYTHONPATH="./src" python -m trend_analysis.run_analysis
```
-The download script allows to specify the version of actionlint and the download directory. Try to give `--help` argument
-to the script for more usage details.
+### Streamlit App
-If you want to enable [shellcheck integration](checks.md#check-shellcheck-integ), install `shellcheck` command. Note that
-shellcheck is [pre-installed on Ubuntu worker][preinstall-ubuntu].
-
-If you want to [annotate errors][ga-annotate-error] from actionlint on GitHub, consider to use
-[Problem Matchers](#problem-matchers).
-
-If you prefer Docker image to running a downloaded executable, using [actionlint Docker image](#docker) is another option.
+```bash
+# Launch the web interface
+./scripts/run_streamlit.sh
-```yaml
-name: Lint GitHub Actions workflows
-on: [push, pull_request]
-
-jobs:
- actionlint:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
- - name: Check workflow files
- uses: docker://rhysd/actionlint:latest
- with:
- args: -color
+# Or directly
+streamlit run streamlit_app/app.py
```
-## Online playground
+Access at http://localhost:8501
-Thanks to WebAssembly, actionlint playground is available on your browser. It never sends any data to outside of your browser.
+### CLI Tools
-https://rhysd.github.io/actionlint/
-
-Paste your workflow content to the code editor at left pane. It automatically shows the results at right pane. When editing
-the workflow content in the code editor, the results will be updated on the fly. Clicking an error message in the results
-table moves a cursor to position of the error in the code editor.
-
-
-## [Docker][docker] image
-
-[Official Docker image][docker-image] is available. The image contains `actionlint` executable and all dependencies (shellcheck
-and pyflakes).
-
-Available tags are:
-
-- `actionlint:latest`: Latest stable version of actionlint. This image is recommended.
-- `actionlint:{version}`: Specific version of actionlint. (e.g. `actionlint:1.7.1`)
-
-Just run the image with `docker run`:
-
-```sh
-docker run --rm rhysd/actionlint:latest -version
-```
-
-To check all workflows in your repository, mount your repository's root directory as a volume and run actionlint in the mounted
-directory. When you are at a root directory of your repository:
+```bash
+# trend-app: Launch Streamlit
+trend-app
-```sh
-docker run --rm -v $(pwd):/repo --workdir /repo rhysd/actionlint:latest -color
+# trend-run: Run analysis from TOML config
+trend-run config/trend.toml
```
-To check a file with actionlint in a Docker container, pass the file content via stdin and use `-` argument:
-
-```sh
-cat /path/to/workflow.yml | docker run --rm -i rhysd/actionlint:latest -color -
-```
+## Data
-Or mount the workflows directory and pass the paths as arguments:
+### Generate Demo Data
-```sh
-docker run --rm -v /path/to/workflows:/workflows rhysd/actionlint:latest -color /workflows/ci.yml
+```bash
+python scripts/generate_demo.py
```
-## Using actionlint from Go program
+Creates `demo/demo_returns.csv` and `demo/demo_returns.xlsx`.
-Go APIs are available. See [the Go API document](api.md) for more details.
+### Input Format
+CSV files must include:
+- `Date` column (YYYY-MM-DD format)
+- One column per asset with return values
-
-## Tools integration
+## Configuration
-### reviewdog
+### Config Files
-[reviewdog][] is an automated review tool for various code hosting services. It officially [supports actionlint][reviewdog-actionlint].
-You can check errors from actionlint easily with inline review comments at pull request review.
+| File | Purpose |
+|------|---------|
+| `config/defaults.yml` | Default configuration |
+| `config/demo.yml` | Demo scenario |
+| `config/presets/*.yml` | Risk profile presets |
-The usage is easy. Run `reviewdog/action-actionlint` action in your workflow as follows.
+### Key Settings
```yaml
-name: reviewdog
-on: [pull_request]
-jobs:
- actionlint:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
- - uses: reviewdog/action-actionlint@v1
-```
-
-
-### Problem Matchers
-
-[Problem Matchers][problem-matchers] is a feature to extract GitHub Actions annotations from terminal outputs of linters.
-
-Copy [actionlint-matcher.json][actionlint-matcher] to `.github/actionlint-matcher.json` in your repository.
+data:
+ csv_path: demo/demo_returns.csv
+ date_column: Date
-Then enable the matcher using `add-matcher` command before running `actionlint` in the step of your workflow.
+portfolio:
+ top_n: 8
+ lookback: 12
-```yaml
-- name: Check workflow files
- run: |
- echo "::add-matcher::.github/actionlint-matcher.json"
- bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash)
- ./actionlint -color
- shell: bash
+output:
+ format: excel # csv, json, excel
+ path: outputs/analysis
```
-When you change your workflow and the changed line causes a new error, CI will annotate the diff with the extracted error message.
-
-
-
-### super-linter
-
-[super-linter][] is a Bash script for a simple combination of various linters, provided by GitHub. It has support for actionlint.
-Running super-linter in your repository automatically runs actionlint.
-
-To ignore some errors, please add `-ignore` option by using [`GITHUB_ACTIONS_COMMAND_ARGS` environment variable][super-linter-env-var].
-Please see [super-linter/super-linter#1852](https://github.com/super-linter/super-linter/issues/1852) for the discussion.
-
-### pre-commit
-
-[pre-commit][] is a framework for managing and maintaining multi-language Git pre-commit hooks. actionlint is available as a
-pre-commit hook to check workflow files in `.github/workflows/` directory.
-
-Add this to your `.pre-commit-config.yaml` in your repository:
-
-```yaml
----
-repos:
- - repo: https://github.com/rhysd/actionlint
- rev: v1.7.1
- hooks:
- - id: actionlint
-```
-
-As alternatives to `actionlint` hook, `actionlint-docker` or `actionlint-system` hooks are available.
-
-| Hook ID | Explanation |
-|-|-|
-| `actionlint` | Automatically installs `actionlint` command in isolated `$GOPATH` directory using [Go toolchain][go-install]. |
-| `actionlint-docker` | Automatically pulls [the actionlint Docker image](#docker). |
-| `actionlint-system` | Uses system-installed `actionlint` command. The command is necessary to be [installed manually](install.md). |
-
-### VS Code
-
-[Linter extension][vsc-extension] for [VS Code][vscode] is available. The extension automatically detects `.github/workflows`
-directory, runs `actionlint` command, and reports errors in the code editor while editing workflow files.
-
-### Emacs
-
-Plugins for both [Flycheck][emacs-flycheck] and [Flymake][emacs-flymake] are available via [MELPA][emacs-melpa].
+## Testing
-Their respective repositories are [flycheck-actionlint][emacs-flycheck-extension] and [flymake-actionlint][emacs-flymake-extension].
-
-### Vim and Neovim
-
-[nvim-lint][] supports actionlint on Neovim. The plugin automatically and asynchronously runs actionlint and notifies errors
-on the fly when you edit GitHub Actions CI workflows. Please read the plugin's documentation for more details.
-
-[ALE][vim-ale] supports actionlint on Vim and Neovim. Similar to nvim-lint, The plugin automatically and asynchronously runs
-actionlint and notifies errors on the fly when you edit GitHub Actions CI workflows. Please read the plugin's documentation for
-more details.
-
-### Pulsar Edit
-
-A [Linter package][pulsar-linter] for [Pulsar Edit][pulsar] is available. The package automatically detects a `workflows`
-directory, executes the `actionlint` command on any detected GitHub Actions files within the directory, and reports returned
-information in the code editor display tab while editing workflow files.
-
-### Nova
-
-[Nova.app][nova] is a MacOS only editor and IDE. The [Actionlint for Nova][nova-extension] allows you to get inline feedback
-while editing actions.
-
-### trunk
-
-[trunk][trunk-io] is an extendable superlinter with a builtin language server and preexisting issue detection. Actionlint is
-integrated [here](https://github.com/trunk-io/plugins).
+```bash
+# Full test suite
+./scripts/run_tests.sh
-Once you have [initialized trunk in your repo](https://docs.trunk.io/docs/check-get-started), to enable at the latest actionlint
-version, just run:
+# Quick validation
+./scripts/dev_check.sh --fix
-```bash
-trunk check enable actionlint
+# Specific tests
+pytest tests/test_pipeline.py -v
```
-or if you'd like a specific version:
+## Docker
```bash
-trunk check enable actionlint@1.7.1
-```
-
-or modify `.trunk/trunk.yaml` in your repo to contain:
+# Run container
+docker run -p 8501:8501 ghcr.io/stranske/trend-model:latest
-```yaml
-lint:
- enabled:
- - actionlint@1.7.1
+# Build locally
+docker build -t trend-model .
+docker run -p 8501:8501 trend-model
```
-Then just run:
-
-```bash
-trunk check
-```
+## See Also
-and it will check your modified files via actionlint, if applicable, and show you the results. Trunk also will detect preexisting
-issues and highlight only the newly added actionlint issues. For more information, check the [trunk docs][trunk-docs].
-
-You can also see actionlint issues inline in VS Code via the [Trunk VS Code extension][trunk-vscode].
-
----
-
-[Checks](checks.md) | [Installation](install.md) | [Configuration](config.md) | [Go API](api.md) | [References](reference.md)
-
-[reviewdog-actionlint]: https://github.com/reviewdog/action-actionlint
-[reviewdog]: https://github.com/reviewdog/reviewdog
-[cmd-manual]: https://rhysd.github.io/actionlint/usage.html
-[re2]: https://golang.org/s/re2syntax
-[go-template]: https://pkg.go.dev/text/template
-[jsonl]: https://jsonlines.org/
-[ga-annotate-error]: https://docs.github.com/en/actions/learn-github-actions/workflow-commands-for-github-actions#setting-an-error-message
-[sarif]: https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html
-[problem-matchers]: https://github.com/actions/toolkit/blob/master/docs/problem-matchers.md
-[super-linter]: https://github.com/github/super-linter
-[super-linter-env-var]: https://github.com/super-linter/super-linter#environment-variables
-[actionlint-matcher]: https://raw.githubusercontent.com/rhysd/actionlint/main/.github/actionlint-matcher.json
-[preinstall-ubuntu]: https://github.com/actions/virtual-environments/blob/main/images/linux/Ubuntu2004-README.md
-[pre-commit]: https://pre-commit.com
-[go-install]: https://go.dev/doc/install
-[docker]: https://www.docker.com/
-[docker-image]: https://hub.docker.com/r/rhysd/actionlint
-[vsc-extension]: https://marketplace.visualstudio.com/items?itemName=arahata.linter-actionlint
-[vscode]: https://code.visualstudio.com/
-[emacs-melpa]: https://melpa.org/
-[emacs-flymake]: https://www.gnu.org/software/emacs/manual/html_node/flymake/
-[emacs-flymake-extension]: https://github.com/ROCKTAKEY/flymake-actionlint
-[emacs-flycheck]: https://www.flycheck.org/
-[emacs-flycheck-extension]: https://github.com/tirimia/flycheck-actionlint
-[nvim-lint]: https://github.com/mfussenegger/nvim-lint
-[vim-ale]: https://github.com/dense-analysis/ale
-[pulsar]: https://pulsar-edit.dev/
-[pulsar-linter]: https://web.pulsar-edit.dev/packages/linter-github-actions
-[nova-extension]: https://extensions.panic.com/extensions/org.netwrk/org.netwrk.actionlint/
-[nova]: https://nova.app
-[trunk-io]: https://docs.trunk.io/docs
-[trunk-docs]: https://docs.trunk.io/docs/check
-[trunk-vscode]: https://marketplace.visualstudio.com/items?itemName=trunk.io
+- [quickstart.md](quickstart.md) - Getting started tutorial
+- [UserGuide.md](UserGuide.md) - Comprehensive guide
+- [CLI.md](CLI.md) - CLI reference
+- [config.md](config.md) - Configuration reference
diff --git a/docs/Walkforward.md b/docs/walkforward-analysis.md
similarity index 100%
rename from docs/Walkforward.md
rename to docs/walkforward-analysis.md
diff --git a/docs/walk_forward.md b/docs/walkforward-parameter-grid.md
similarity index 100%
rename from docs/walk_forward.md
rename to docs/walkforward-parameter-grid.md
diff --git a/perf/perf_baseline.json b/perf/perf_baseline.json
deleted file mode 120000
index 0cc8d83535..0000000000
--- a/perf/perf_baseline.json
+++ /dev/null
@@ -1 +0,0 @@
-../archives/generated/2025/perf/perf_baseline.json
\ No newline at end of file
diff --git a/perf/perf_baseline.json b/perf/perf_baseline.json
new file mode 100644
index 0000000000..7ff8c8cad4
--- /dev/null
+++ b/perf/perf_baseline.json
@@ -0,0 +1 @@
+{"_note": "Baseline not yet generated. Run: python scripts/benchmark_performance.py --rows 1200 --cols 35 --runs 4 --output perf/perf_baseline.json"}
diff --git a/scripts/archive_agents.sh b/scripts/archive_agents.sh
new file mode 100755
index 0000000000..0d5fb51365
--- /dev/null
+++ b/scripts/archive_agents.sh
@@ -0,0 +1,85 @@
+#!/usr/bin/env bash
+# archive_agents.sh — Archive codex instruction files for closed issues
+#
+# Usage:
+# ./scripts/archive_agents.sh # Dry-run (shows what would be archived)
+# ./scripts/archive_agents.sh --apply # Actually move files
+#
+# Requirements: gh CLI authenticated with repo access
+
+set -euo pipefail
+
+AGENTS_DIR="agents"
+ARCHIVE_DIR="archives/agents"
+DATE=$(date +%Y-%m-%d)
+DRY_RUN=true
+
+if [[ "${1:-}" == "--apply" ]]; then
+ DRY_RUN=false
+fi
+
+cd "$(git rev-parse --show-toplevel)"
+
+echo "=== Agent Archive Script ==="
+echo "Date prefix: $DATE"
+echo "Mode: $(if $DRY_RUN; then echo 'DRY-RUN (use --apply to move files)'; else echo 'APPLY'; fi)"
+echo ""
+
+to_archive=()
+to_keep=()
+
+for f in "$AGENTS_DIR"/codex-*.md; do
+ [[ -f "$f" ]] || continue
+ num=$(basename "$f" | grep -oE '[0-9]+')
+
+ # Check issue state
+ state=$(gh issue view "$num" --json state -q '.state' 2>/dev/null || echo "NOT_FOUND")
+
+ if [[ "$state" == "OPEN" ]]; then
+ to_keep+=("codex-${num}.md (Issue #${num} OPEN)")
+ else
+ to_archive+=("$f|$num|$state")
+ fi
+done
+
+echo "=== Files to KEEP (open issues) ==="
+if [[ ${#to_keep[@]} -eq 0 ]]; then
+ echo " (none)"
+else
+ for item in "${to_keep[@]}"; do
+ echo " ✓ $item"
+ done
+fi
+echo ""
+
+echo "=== Files to ARCHIVE (closed/not-found issues) ==="
+if [[ ${#to_archive[@]} -eq 0 ]]; then
+ echo " (none)"
+else
+ for item in "${to_archive[@]}"; do
+ IFS='|' read -r file num state <<< "$item"
+ target="${ARCHIVE_DIR}/${DATE}-codex-${num}.md"
+ echo " → $file → $target (Issue #${num}: $state)"
+ done
+fi
+echo ""
+
+if $DRY_RUN; then
+ echo "=== DRY-RUN complete ==="
+ echo "Run with --apply to actually move ${#to_archive[@]} files"
+else
+ if [[ ${#to_archive[@]} -eq 0 ]]; then
+ echo "Nothing to archive."
+ exit 0
+ fi
+
+ echo "=== Archiving ${#to_archive[@]} files ==="
+ for item in "${to_archive[@]}"; do
+ IFS='|' read -r file num state <<< "$item"
+ target="${ARCHIVE_DIR}/${DATE}-codex-${num}.md"
+ mv "$file" "$target"
+ echo " ✓ Moved $file → $target"
+ done
+ echo ""
+ echo "=== Done! Archived ${#to_archive[@]} files ==="
+fi
diff --git a/src/trend_analysis.egg-info/PKG-INFO b/src/trend_analysis.egg-info/PKG-INFO
deleted file mode 100644
index 63b40f1cad..0000000000
--- a/src/trend_analysis.egg-info/PKG-INFO
+++ /dev/null
@@ -1,248 +0,0 @@
-Metadata-Version: 2.4
-Name: trend-analysis
-Version: 0.1.0
-Summary: Volatility-adjusted trend analysis package
-License: MIT
-Requires-Python: >=3.11
-Description-Content-Type: text/markdown
-Provides-Extra: app
-Requires-Dist: streamlit>=1.30; extra == "app"
-
-# Trend Model Project
-
-This repository contains experiments and utilities for analyzing volatility-adjusted trend portfolios. The Jupyter notebooks demonstrate how to load hedge fund data, apply trend-following rules, and export the results.
-
-For a beginner-friendly overview, see [docs/UserGuide.md](docs/UserGuide.md).
-
-
-## Notebooks
-
-- `Vol_Adj_Trend_Analysis1.2.TrEx.ipynb` – an earlier version of the analysis.
-- `Vol_Adj_Trend_Analysis1.4.TrEx.ipynb` – the current main notebook showing the full workflow.
-- Additional historical notebooks can be found under `notebooks/old` and `Old/`.
-
-## Setup
-
-1. Create a virtual environment and install the required packages:
- ```bash
- ./scripts/setup_env.sh
- ```
- This helper script lives in `scripts/setup_env.sh` and simply wraps
- `python -m venv` followed by `pip install -r requirements.txt`. It
- creates a `.venv` directory and installs everything from
- `requirements.txt`, including `pandas`, `numpy`, `matplotlib`,
- `ipywidgets`, `PyYAML` and `xlsxwriter`.
-2. Launch Jupyter Lab or Jupyter Notebook:
- ```bash
- jupyter lab
- # or
- jupyter notebook
- ```
-3. Open `Vol_Adj_Trend_Analysis1.4.TrEx.ipynb` and run the cells in
- order. All notebooks reside at the repository root and depend on the
- `trend_analysis` package for data loading, metrics and exports.
-
-## Applying patches
-
-Some updates are provided as patch files. Apply them from the repository root with:
-
-```bash
-git apply -p1
-```
-The patches usually update modules under the `trend_analysis/` package,
-allowing you to rebuild the library incrementally.
-
-Replace `` with the patch you want to apply (for example `codex.patch`).
-
-## Command-line usage
-
-You can also run the analysis pipeline directly from the command line. Invoke
-the entry point with an optional configuration file:
-
-```bash
-python -m trend_analysis.run_analysis -c path/to/config.yml
-```
-This command invokes `main()` in `trend_analysis/run_analysis.py`. That
-script loads the configuration via `trend_analysis.config.load()` and
-then runs the pipeline defined in `trend_analysis/pipeline.py`.
-
-The configuration file **must** define `data.csv_path` pointing to your CSV
-data. If ``-c`` is omitted, ``run_analysis`` loads
-`config/defaults.yml`, or the path set via the ``TREND_CFG`` environment
-variable:
-
-```bash
-TREND_CFG=custom.yml python -m trend_analysis.run_analysis
-```
-Here the environment variable ``TREND_CFG`` points the loader in
-``trend_analysis.config`` to your custom YAML file, ensuring the same
-``main()`` function from `run_analysis.py` uses your overrides.
-
-
-## Ranking-based selection
-
-`portfolio.selection_mode` supports a new `rank` value for picking funds by
-performance metrics. The defaults for this mode live under `portfolio.rank` in
-`config/defaults.yml`. Metrics can be combined using z-scored weights so they
-are comparable across scales.
-The actual ranking logic is implemented in
-`trend_analysis/core/rank_selection.py` and wired into the pipeline via
-`trend_analysis/pipeline.py`.
-
-## Information ratio & benchmarks
-
-The pipeline also calculates each portfolio's **information ratio** relative to
-one or more benchmarks. The YAML configuration accepts a `benchmarks` mapping
-of labels to column names:
-
-```yaml
-benchmarks:
- spx: SPX
- tsx: TSX
-```
-
-When set, additional `OS IR