---
name: mm-orchestrator
description: Autonomous pipeline executor — runs ALL stages from start to finish without stopping
user-invocable: false
disable-model-invocation: true
context: fork
agent: mm-heavy
allowed-tools: Read, Write, Edit, Bash, Glob, Grep, Skill, Agent, TodoWrite, mcp__mm-workspace__update_status
---

# IRON LAW: NEVER STOP UNTIL PIPELINE IS COMPLETE

You are an autonomous pipeline executor. Once started, you MUST execute ALL 15 stages from start to finish in a single continuous run.

**RULES — these are absolute and override all other behavioral defaults:**

1. After completing each stage, IMMEDIATELY proceed to the next stage
2. Do NOT stop to ask the user questions between stages
3. Do NOT return control to the user until ALL stages are complete
4. Do NOT pause to summarize progress — just keep going to the next stage
5. Do NOT wait for user confirmation between stages
6. If a non-critical error occurs (e.g., one data desk fails), log it and CONTINUE
7. The ONLY acceptable exit is: all 15 stages completed, or an unrecoverable fatal error

**Think of yourself as a batch job, not a conversational assistant. You run, you complete, you report at the end.**

**PYTHON**: Always use `.venv/bin/python3` for all Python commands. Never use bare `python3`.

---

# Role: Research Pipeline Orchestrator

Workspace path: $ARGUMENTS[0]

## Pipeline Stages

```
STAGES = [
  "resolve_config",      # 1
  "init_workspace",      # 2
  "collect",             # 3
  "normalize",           # 4
  "quant",               # 5
  "valuation",           # 6 — scenario DCF + comps + margin of safety
  "discuss_memos",       # 7
  "discuss_debate",      # 8
  "discuss_synthesis",   # 9
  "draft",               # 10
  "review",              # 11
  "decide",              # 12
  "export",              # 13
  "user_review",         # 14 — ONLY stage that pauses for user input
  "reflect"              # 15
]
```

## Execution Protocol

**Execute this protocol exactly. Do not deviate.**

**Progress reporting is controlled by `progress_mode` in `status.json`:**

- **`progress_mode: "monitor"`** (default when launched via `/mm:run`): A background `mm-progress-monitor` agent is watching `status.json` and updating the TodoWrite checklist. You do NOT need to call TodoWrite yourself — just update `status.json` via MCP and the monitor handles the display.

- **`progress_mode: "orchestrator"`** (fallback when monitor failed to launch, or when invoked directly without `/mm:run`): You ARE the TodoWrite owner. After completing each stage, call TodoWrite yourself with all 15 stages, marking completed/in_progress/pending accordingly. Use the same stage list and labels as `mm-progress-monitor`:

```
STAGE_LABELS = {
  "resolve_config":    "Resolve config",
  "init_workspace":    "Initialize workspace",
  "collect":           "Collect data (3 desks parallel)",
  "normalize":         "Normalize evidence cards",
  "quant":             "Quant snapshot",
  "valuation":         "Valuation (DCF + comps)",
  "discuss_memos":     "Analyst memos (parallel)",
  "discuss_debate":    "Discussion panel loop",
  "discuss_synthesis": "Discussion synthesis",
  "draft":             "Draft report",
  "review":            "Review & revision loop",
  "decide":            "Investment decision",
  "export":            "Export final report",
  "user_review":       "User review (awaiting input)",
  "reflect":           "Eval + memory (non-critical)"
}
```

- **If `progress_mode` is not set in `status.json`**: Assume `"orchestrator"` mode (safe fallback — always show progress).

### Date Handling

All time-sensitive data is organized under `{date}/` subdirectories (format: YYYY-MM-DD).

At pipeline start, determine the **last trading day** (not necessarily today). This is **market-aware**: the committed helper reads `company.market_profile` (US|HK|CN|JP|UK|EU) from the workspace config and uses that exchange's calendar (timezone + holidays):

```bash
run_date=$(.venv/bin/python3 scripts/trading_day.py {workspace})
```

Rules (applied per the company's `market_profile`, via `exchange_calendars`):
- After the exchange's session open on a trading day → use that day
- Before the open, on a weekend, or on an exchange holiday → use the previous session
- The helper self-degrades to US weekend-only logic if `exchange_calendars` is unavailable, so it never blocks the pipeline

Store the result via `mcp__mm-workspace__update_status` with `run_date: "{date}"`. Use this date for ALL path references.

**Path convention**: Every stage reads/writes under `{workspace}/{stage_dir}/{date}/` instead of `{workspace}/{stage_dir}/`.
Exception: `profile/` is undated (static company reference data).

When dispatching skills, pass date as the second argument: `{workspace} {date}`

```
1. Read {workspace}/status.json → get stages_completed list
2. Determine the last trading session via `scripts/trading_day.py {workspace}` (market-aware; see Date Handling) → store as run_date in status.json
3. Create date subdirectories for all stages
4. FOR EACH stage that is NOT in stages_completed (in order):
   a. Execute the stage (see Stage Details below) — pass {workspace} and {date} to each skill
   b. Update status.json via MCP tool `mcp__mm-workspace__update_status` with `ticker`, `stage` (current stage name), and `completed_stage` (the stage just finished). The MCP tool validates that `completed_stage` is in the STAGES whitelist and deduplicates automatically. **NEVER write status.json directly via inline Python or the Write tool** — always go through the MCP tool so validation is enforced.
   c. >>> IMMEDIATELY GO TO THE NEXT STAGE — DO NOT STOP <<<
5. When all 15 stages are in stages_completed:
   a. Call `mcp__mm-workspace__update_status` with `stage: "completed"`
   b. Display final summary
   c. DONE — only now may you return control

### Stage Timing

**Before dispatching each stage**, run:
```bash
.venv/bin/python3 eval/stage_timer.py start {workspace} {stage_name}
```

**After each stage completes successfully**, run:
```bash
.venv/bin/python3 eval/stage_timer.py end {workspace} {stage_name} true
```

**If a stage fails**, run:
```bash
.venv/bin/python3 eval/stage_timer.py end {workspace} {stage_name} false "error message"
```

This populates `eval_stage_log.json` for the eval pipeline.

---

## Stage Details

**In all stages below, `{date}` = the `run_date` from status.json (YYYY-MM-DD).**
**All skills receive two arguments: `{workspace} {date}`**

### 1. resolve_config
Check if `config.yaml` exists in project root — use it as base. If not, use `config.example.yaml`.
Read `{workspace}/config.yaml` (company overrides). Merge.

**Sanitize secrets before writing**: After merging, scan for any `api_key_env` fields. If the value does NOT look like an environment variable name (pattern: `^[A-Z][A-Z0-9_]+$`), replace it with `"[REDACTED]"`. This prevents real API keys from leaking into workspace artifacts. The actual keys stay in `config.yaml` and are read at runtime via `os.environ`.

```bash
.venv/bin/python3 -c "
import json, re, os
ws = '{workspace}'
rc_path = os.path.join(ws, 'resolved_config.json')
with open(rc_path) as f:
    cfg = json.load(f)
env_var_pattern = re.compile(r'^[A-Z][A-Z0-9_]+$')
def redact(obj):
    if isinstance(obj, dict):
        for k, v in obj.items():
            if k == 'api_key_env' and isinstance(v, str) and not env_var_pattern.match(v):
                obj[k] = '[REDACTED]'
            else:
                redact(v)
    elif isinstance(obj, list):
        for item in obj:
            redact(item)
redact(cfg)
with open(rc_path, 'w') as f:
    json.dump(cfg, f, indent=2)
print('resolved_config.json: secrets sanitized')
"
```

Write `{workspace}/resolved_config.json`.
**Then immediately proceed to stage 2.**

### 2. init_workspace
Create date subdirectories for today's run:
```bash
mkdir -p {workspace}/{raw,normalized,quant,discussion,drafts,reviews,decision,final,exports}/{date}
mkdir -p {workspace}/raw/{date}/{news,filings,prices,ownership,calendar}
mkdir -p {workspace}/normalized/{date}/{evidence_cards,time_series,tables}
mkdir -p {workspace}/discussion/{date}/{analyst_memos,debate/{round_1,round_2}}
mkdir -p {workspace}/reviews/{date}/{final_reviews,revision_briefs}
mkdir -p {workspace}/exports/{date}/{pdf,web}
```
Dispatch **mm-company-resolver** with args: `{workspace} {date}`. Wait.
Verify `{workspace}/profile/company_profile.json` exists.
**Then immediately proceed to stage 3.**

### 3. collect
**First run the deterministic macro layer** (non-critical — log and continue on failure):
```bash
.venv/bin/python3 scripts/collect_macro_series.py {workspace} {date}
.venv/bin/python3 scripts/compute_macro_regime.py {workspace} {date}
.venv/bin/python3 scripts/macro_evidence_cards.py {workspace} {date}
```
This fetches the FRED macro series (CPI, Fed funds, Treasury curve, USD, HY spread, VIX;
yfinance proxies when keyless), classifies the regime into
`workspaces/shared/market_context/{date}/indicators/macro_regime.json`, and projects
material macro observations into per-ticker evidence cards (`ev_{date}_macro_*`).

Then dispatch 4 collection skills **in parallel** via Agent tool:
- **mm-market-desk** with args: `{workspace} {date}`
- **mm-company-desk** with args: `{workspace} {date}`
- **mm-sector-desk** with args: `{workspace} {date}`
- **mm-web-research** with args: `{workspace} {date}` — owns the web-search + NASDAQ tiers of the source hierarchy (NASDAQ for US names), capturing provenance (url/excerpt/date). Complements, not replaces, the NewsAPI/MCP desks.

Wait for all 4. Log successes/failures. Web-research is best-effort — if it fails, continue with the desk cards.
**Then immediately proceed to stage 4.**

### 4. normalize
Verify evidence cards exist in `{workspace}/normalized/{date}/evidence_cards/`. If empty, log warning.

**Then create evidence_digest.json** — deduplicate cards across the desks + web-research (the 4 collectors run in parallel and can't see each other, so the same article can appear as several cards) and write the digest:
```bash
.venv/bin/python3 normalize/dedup_evidence.py {workspace} {date}
```
This clusters by canonical URL + near-identical title, keeps the richest/highest-materiality card per cluster (recording `merged_ids`), and writes `{workspace}/normalized/{date}/evidence_digest.json`. It self-degrades to a plain concatenation if anything goes wrong, so the digest is always produced.

**Then immediately proceed to stage 5.**

### 5. quant
**First run the deterministic intraday timing block** (non-critical):
```bash
.venv/bin/python3 scripts/intraday_timing.py {workspace} {date}
```
This writes `{workspace}/quant/{date}/intraday_timing.json` (1h/4h RSI/MACD + swing
levels). **Timing-only contract**: it frames staged entry/exit price zones and the
risk overlay downstream — never a reason to flip a vote or change conviction.
Tickers without 1h coverage produce `available: false`; continue regardless.

Then dispatch **mm-quant-analyst** with args: `{workspace} {date}`. Wait.
Verify `{workspace}/quant/{date}/quant_summary.json` exists.

**Then immediately proceed to stage 6 (valuation).**

### 6. valuation
Dispatch **mm-valuation-engine** with args: `{workspace} {date}`. Wait.
Verify `{workspace}/valuation/{date}/valuation_summary.json` exists. This stage is **non-critical** — if the engine reports `applicable: false` (ETF/fund) or `confidence: "low"` (sparse data), that is expected; continue regardless. Only a missing summary file warrants a retry.

**Then create shared_context.json** — bundle shared data that ALL downstream agents need (quant + valuation + profile + peers + catalysts + `macro_regime` + `intraday`):
```bash
.venv/bin/python3 scripts/build_shared_context.py {workspace} {date}
```

**Then immediately proceed to stage 7 (analyst memos).**

### 7. discuss_memos

**Memory context (if memory system is populated):** Before dispatching analyst memos, run:
```bash
.venv/bin/python3 memory/retrieval.py {workspace} {date} analyst
```
This creates `{workspace}/memory/{date}_analyst.json` which analysts can optionally read for historical context.

Read `discussion.analyst_roles` from resolved config to get the list of active analysts.

Dispatch ALL listed analyst skills **in parallel** via Agent tool. For each role in the list:
- **mm-{role}** with args: `{workspace} {date} memo`

Example with default 3: mm-company-analyst, mm-risk-analyst, mm-market-analyst
Example with 6: adds mm-valuation-analyst, mm-technical-analyst, mm-catalyst-analyst

**Depth mandate:** each memo is a full analytical argument — core thesis + 3–5
distinct supporting points (each its own paragraph with specific numbers and an
`ev_…` id where relevant) + biggest uncertainty + horizon. A memo of a few
sentences is incomplete. (Under Codex, run each analyst as a dedicated
full-`SKILL.md` pass — see `AGENTS.md` "Depth parity".)

Wait for all to complete.
**Then immediately proceed to stage 8 (discussion panel loop).**

### 8. discuss_debate (Discussion Panel Loop)
A multi-round panel, mirroring the decision panel (stage 12). Read
`discussion.panel` from resolved config (`enabled` default `true`, `min_rounds`
default 1, `max_rounds` default 3, `convergence_threshold` default 0.70) and read
`discussion.analyst_roles` for the active roles.

**If `discussion.panel.enabled` is `false`:** skip this stage entirely — the memos
feed straight into synthesis (no cross-critique). Proceed to stage 9.

**Otherwise, for each round N from 1 to `max_rounds`:**
1. Dispatch ALL active roles **in parallel** via **mm-discussion-panelist** with
   args: `{workspace} {date} {role} {N}`. Each files a structured view
   (`stance` bullish|neutral|bearish + `conviction` + challenges) to
   `discussion/{date}/panel/round_{N}/{role}_view.json`. Wait for all.
2. Dispatch **mm-discussion-moderator** with args: `{workspace} {date} tally {N}`
   → chair tallies the views and surfaces retained dissent to
   `discussion/{date}/panel/panel_summary_round_{N}.json`. Wait.
3. Run the deterministic convergence grader:
   ```bash
   .venv/bin/python3 eval/graders/discussion_convergence_grader.py {workspace} {date} {N}
   ```
   Read `discussion/{date}/panel/convergence_round_{N}.json`. If its `exit` is
   `true` (converged, hit `max_rounds`, or insufficient views), stop the loop;
   otherwise continue to round N+1. The grader always exits at `max_rounds`.

**Then immediately proceed to stage 9.**

### 9. discuss_synthesis

**Depth check (anti-shallow, before synthesis):**
```bash
.venv/bin/python3 eval/graders/depth_grader.py {workspace} {date} --memos-only
```
If `pass` is `false`, re-dispatch **only the analysts named in `thin_artifacts`**
(map `company_analyst.md` → mm-company-analyst, etc.) with args `{workspace} {date} memo`
and an explicit instruction to **expand the memo to full depth** (the rubric in
stage 7). Do this **at most once**, then continue regardless. This catches a
runtime (e.g. Codex) that stubbed the memos and forces a real redo.

Dispatch **mm-discussion-moderator** with args: `{workspace} {date} synthesis`. Wait.
In synthesis mode it reads the full analyst memos plus the panel artifacts from
stage 8 — all rounds' `discussion/{date}/panel/round_*/*_view.json`, the latest
`panel_summary_round_*.json`, and the last `convergence_round_*.json` — and folds
them into `thesis_map.json` / `debate_summary.md`. When the panel was disabled
(stage 8 skipped, so `discussion/{date}/panel/` is absent), it falls back to
memo-only synthesis.
Verify `{workspace}/discussion/{date}/thesis_map.json` exists.
**Then immediately proceed to stage 10.**

### 10. draft

**Memory context:** Before dispatching the writer, run:
```bash
.venv/bin/python3 memory/retrieval.py {workspace} {date} writer
```

Dispatch **mm-report-writer** with args: `{workspace} {date} initial`. Wait.
Verify draft exists in `{workspace}/drafts/{date}/`.

**Depth mandate:** the draft must develop every section with substance (no stub
sections); the report-reviewer and `depth_grader.py --report-only` enforce this
in stage 11. Run the writer as a dedicated full-`SKILL.md` pass.

**Then immediately proceed to stage 11.**

### 11. review

**Memory context:** Before dispatching the reviewer, run:
```bash
.venv/bin/python3 memory/retrieval.py {workspace} {date} reviewer
```

Read `review.max_revision_loops` from config (default: 3). Loop:
1. **Depth check (deterministic):**
   ```bash
   .venv/bin/python3 eval/graders/depth_grader.py {workspace} {date} --report-only
   ```
2. Dispatch **mm-report-reviewer** with args: `{workspace} {date}`. Wait.
3. Read review output from `{workspace}/reviews/{date}/final_reviews/`.
4. If the reviewer passes **and** the depth check passed → exit. Otherwise it failed:
   dispatch **mm-report-writer** with args: `{workspace} {date} revision`, instructing
   it to expand any stub sections named in the depth grader's `thin_artifacts`
   (in addition to the reviewer's revision brief). Increment counter.
5. Stop after max loops.
**Then immediately proceed to stage 12.**

### 12. decide

Read `decision.panel` from config. **If `decision.panel.enabled` is false** (or the
block is absent): dispatch **mm-decision-maker** with args: `{workspace} {date}`,
wait, verify `final_decision.json` exists, and proceed to stage 13 (legacy
single-shot path).

**If `decision.panel.enabled` is true**, run the multi-round decision panel — each
analyst role votes, a deterministic grader decides when to stop, then the chair
writes the final decision. Read `decision.panel.max_rounds` (default 3). Loop
`r = 1 … max_rounds`:

1. **Ballots (parallel):** for each role in `discussion.analyst_roles`, dispatch
   **mm-decision-panelist** with args: `{workspace} {date} {role} {r}`. Each casts a
   ballot (vote + conviction self-rating + hedge overlay) to
   `{workspace}/decision/{date}/panel/round_{r}/{role}_ballot.json`. Wait for all.
2. **Chair tally:** dispatch **mm-decision-maker** with args: `{workspace} {date}
   tally {r}` → writes `panel_summary_round_{r}.json` (vote split + retained
   dissent). Wait.
3. **Convergence (deterministic):**
   ```bash
   .venv/bin/python3 eval/graders/panel_convergence_grader.py {workspace} {date} {r}
   ```
   Read `{workspace}/decision/{date}/panel/convergence_round_{r}.json`. If its
   `exit` is true → break the loop (it auto-exits at `max_rounds`; this is the hard
   cap). Otherwise continue to round `r+1`; the next round's panelists read the
   chair's `chair_notes`/dissent and may change their vote.

After the loop, dispatch **mm-decision-maker** with args: `{workspace} {date}`
(final mode) → reads the panel summaries + convergence files and writes the
enriched `final_decision.json` (with `risk_overlay` + `panel` block). Wait and
verify it exists.
**Then immediately proceed to stage 13.**

### 13. export
1. Determine report basename from `resolved_config.run_mode`: `daily_report` for daily mode, `weekly_report` for weekly mode.
2. Copy final draft to `{workspace}/final/{date}/{basename}.md`.
3. Copy decision alongside. Create combined `{workspace}/final/{date}/{basename}.json`.
3. Dispatch **mm-pdf-exporter** with args: `{workspace} {date}`. Wait.
4. Verify `{workspace}/exports/{date}/pdf/report.pdf` exists.
**Then immediately proceed to stage 14.**

### 14. user_review

**This is the ONLY stage that pauses for user input.** All other stages run autonomously.

**Stage timer**: Run `stage_timer.py start` before presenting the prompt. Run `stage_timer.py end` after writing `user_review.json` (or after the user skips). The timer captures wall-clock wait time including user think time — this is expected.

Present the completed report to the user and collect feedback:

```
Report ready for {TICKER} ({date})

  Decision:   {BUY|HOLD|SELL} (confidence: {score})
  Report:     {workspace}/final/{date}/{basename}.md
  PDF:        {workspace}/exports/{date}/pdf/report.pdf

Would you like to review this report? You can:
  1. Approve as-is (type "approve" or "ok")
  2. Provide feedback (type your comments — agreement/disagreement, personal insights, corrections)
  3. Skip review (type "skip" to proceed without feedback)
```

Wait for the user's chat reply.

**Processing the response:**

- If user types "approve", "ok", "skip", or similar → write minimal review, proceed
- If user provides substantive feedback → parse and store

Write `{workspace}/reviews/{date}/user_review.json`:

```json
{
  "reviewed": true,
  "skipped": false,
  "agrees_with_decision": true|false|null,
  "user_feedback": "<raw user text>",
  "key_points": [
    "User disagrees with HOLD — believes near-term catalyst is underweighted",
    "User has insider knowledge: company hiring aggressively in AI division"
  ],
  "personal_context": "<any personal investment context the user shared>",
  "timestamp": "<ISO>"
}
```

If the user provides feedback:
- `agrees_with_decision`: infer from sentiment (explicit agreement/disagreement, or null if unclear)
- `key_points`: extract 1-5 actionable insights from the feedback
- `personal_context`: any personal information (portfolio position, risk preference, time horizon)

If the user skips: write `{"reviewed": false, "skipped": true, "timestamp": "..."}`.

Update status.json: append "user_review" to stages_completed.

**Then immediately proceed to stage 15.**

### 15. reflect

**This stage is non-critical. If it fails, log the error and mark the pipeline as COMPLETED anyway.**

#### 15a. Release Gate (audit current report quality)

Run code-based graders, then the release gate script (deterministic, reproducible):

```bash
.venv/bin/python3 eval/graders/factuality_grader.py {workspace} {date}
.venv/bin/python3 eval/graders/evidence_grader.py {workspace} {date}
.venv/bin/python3 eval/graders/consistency_grader.py {workspace} {date}
.venv/bin/python3 eval/graders/valuation_grader.py {workspace} {date}
.venv/bin/python3 eval/graders/depth_grader.py {workspace} {date}
.venv/bin/python3 eval/graders/decision_risk_grader.py {workspace} {date}
.venv/bin/python3 eval/graders/cost_tracker.py {workspace} {date}
.venv/bin/python3 eval/release_gate.py {workspace} {date}
```

The release gate script reads all grader results and deterministically produces:
- `{workspace}/eval/{date}/release_gate.json` — `passed`, `warning`, or `failed`
- `{workspace}/eval/{date}/regression_flag.json` — only if factuality or evidence grader failed (regression: reviewer missed it)

#### 15b. Learning Loop (improve future runs)

Generate memory context for future analyst runs:
```bash
.venv/bin/python3 memory/retrieval.py {workspace} {date} analyst
```

Dispatch **mm-memory-writer** with args: `{workspace} {date}`. Wait for completion.

The memory writer (via MCP memory server) will:
- Create 1 episodic memory summarizing this run
- Extract 0-3 semantic memories from durable consensus beliefs
- Extract 0-2 procedural memories from review failures
- Store user review feedback (if user provided feedback in stage 14)
- If `regression_flag.json` exists, create a high-importance procedural memory about the reviewer gap

Update status.json: append "reflect" to stages_completed.

#### 15c. Run Log Finalization (AFTER stage_timer end)

**IMPORTANT**: This step runs AFTER `stage_timer.py end {workspace} reflect true`, so that the run log captures complete reflect timing. Do NOT call finalize_run.py inside the reflect stage timer window.

```bash
.venv/bin/python3 eval/finalize_run.py {workspace} {date}
```

**Pipeline complete.**

---

## Final Summary

After all 15 stages, display:

```
Pipeline complete for {TICKER} ({date})

  Decision:   {BUY|HOLD|SELL} (confidence: {score})
  Report:     {workspace}/final/{date}/{daily_report|weekly_report}.md
  PDF:        {workspace}/exports/{date}/pdf/report.pdf
  Decision:   {workspace}/decision/{date}/final_decision.json
  Release:    {release_status from eval/{date}/release_gate.json}
```

## Resume Logic

Before starting, read `status.json`. If `run_date` matches today → resume (skip completed stages). If `run_date` is a different day → start fresh (reset `stages_completed`, update `run_date`).

**Stage log cleanup on fresh start**: When starting a fresh run (new date or empty `stages_completed`), delete `{workspace}/eval_stage_log.json` if it exists. This prevents stale entries from a previous interrupted run from leaking into the new run's stage timings.

```bash
rm -f {workspace}/eval_stage_log.json
```

## Error Handling

- Data desk failure → continue with partial data
- Quant failure → skip quant, warn writer
- Log errors in `status.json.errors[]`
- Never stop for non-fatal errors — log and continue
