# v0.6.2-alpha — Runtime hygiene + paper-grade reproducibility hardening

**Released**: 2026-05-16
**Status**: Pre-release (alpha). `pip install aiswmm` continues to deliver v0.6.1; this version requires opt-in.
**PyPI version string**: `0.6.2a1` (PEP 440)

## How to install

```bash
# Pin this specific pre-release
pip install aiswmm==0.6.2a1

# Or allow any pre-release
pip install --pre aiswmm
```

The default `pip install aiswmm` will not pull this version — that is intentional. Pre-releases are for early adopters who can tolerate the alpha label; stable users on v0.6.1 remain undisturbed.

## What's new in one sentence

Six bug fixes and one portability hardening that together remove footguns from the user's day-to-day interactive runtime and make `aiswmm` substantively portable to a new watershed without code changes.

## Bug fixes (six)

### 1. Warm intro fires once per session (#108)

| Aspect | Before | After |
|---|---|---|
| Reproducer | pipe `hi\nhi\nhello\nwhat can you do` | same input |
| Canned `WARM_INTRO_TEMPLATE` fires | 4 times in 0.13s | 1 time on the first open-shaped prompt |
| LLM consulted | Never (all 4 were canned) | Subsequent open-shaped prompts fall through to the planner |

**Root cause**: `agentic_swmm/agent/runtime_loop.py:130` reset `turn = 0` after emitting the warm intro, so every subsequent `turn += 1` brought the counter back to `1` and `maybe_warm_intro` happily returned the same canned template.

**Fix**: Remove the reset. `maybe_warm_intro` already enforces `turn == 1` internally, so letting the counter advance produces the documented "once per session" behavior.

**Regression guard**: `tests/test_self_intro_on_open_prompt.py::WarmIntroFiresOncePerSessionRegression` — source-level check via `inspect.getsource` that the reset cannot sneak back into the emit block.

### 2. First plot no longer hangs ~90 seconds on cold start (#109/#110)

| Aspect | Before | After |
|---|---|---|
| User-reported first `plot_run` duration | 89 seconds (`agent_trace.jsonl` 19:07:42 → 19:09:11) | 5–15 seconds (post-preheat) |
| What was slow | matplotlib font cache rebuild + swmmtoolbox first import + Python bytecode cache creation | All of those run at MCP server boot, not on user-visible plot path |

**Fix**: `mcp/swmm-plot/server.js` fires a fire-and-forget Python subprocess at server startup that imports `matplotlib.pyplot`, materializes a `Figure`, and imports `swmmtoolbox`. The MCP `initialize` JSON-RPC handshake does not wait on it. If Python is missing, the preheat degrades to a stderr warning without crashing the server.

**Fallback UX**: `skills/swmm-plot/scripts/plot_rain_runoff_si.py` detects the absence of `matplotlib.get_cachedir()/fontlist-v*.json` and prints one stderr line so the user knows the first-call wait is one-shot.

### 3. Plot X-axis is readable across any simulated duration (#112)

| Fixture | Tick label count (before) | Tick label count (after) |
|---|---|---|
| 30-day Todcreek stretch | 316 (black blur) | 6 (e.g., `['29', 'Jun', '05', '09', '13', '17']`) |
| Tecnopolo 1994 full year | 4 369 (triggers `Locator.MAXTICKS (1000)` warning) | ≤ 12 |

**Fix**: 10-line surgical change in `plot_rain_runoff_si.py` replacing the hard-coded `HourLocator(interval=2)` + `DateFormatter('%m-%d\n%H:%M')` with `AutoDateLocator(maxticks=12)` + `ConciseDateFormatter`. The `--focus-day` branches that already handle sub-day windows are unchanged.

### 4. `swmm-end-to-end` and five sibling skills are discoverable again (#113)

| Aspect | Before | After |
|---|---|---|
| `select_skill swmm-end-to-end` | `FAILED: unknown skill: swmm-end-to-end` | OK |
| `aiswmm skill list --registry` count | 8 (`agent-internal`, `swmm-builder`, `swmm-climate`, `swmm-experiment-audit`, `swmm-modeling-memory`, `swmm-network`, `swmm-plot`, `swmm-runner`) | 14 (adds `swmm-calibration`, `swmm-end-to-end`, `swmm-gis`, `swmm-params`, `swmm-rag-memory`, `swmm-uncertainty`) |

**Root cause**: `SkillRouter._build_buckets()` seeded buckets only from the 8 skills with deterministic tool bindings. Pure-orchestration skills (only a `SKILL.md`, no MCP tool) silently disappeared.

**Fix**: Seed an empty bucket for every on-disk skill name returned by `discover_skills()` first; deterministic-binding logic still wins where it applies.

### 5. Compound intent "run X demo and plot" routes correctly (#111)

| Aspect | Before | After |
|---|---|---|
| `select_workflow_mode` on `"run Tod Creek demo and plot the figure"` | `mode=existing_run_plot` (plots a different prior run from global state) | `mode=prepared_demo` (runs Tod Creek as the user asked) |
| Layer responsible | Keyword fallback misordered `wants_plot` before `wants_demo` | Two-layer fix: keyword priority + LLM disambiguation deep module |

**Architectural addition**: New module `agentic_swmm/agent/intent_disambiguator.py`. Public interface `disambiguate(goal, signals, provider, timeout_sec=5.0) -> str | None`. Trigger condition is narrow — `wants_plot AND (wants_demo OR wants_run OR wants_calibration OR wants_uncertainty)`. On detection, a forced-enum LLM call picks one of the seven valid modes; on any failure (timeout, invalid output, exception) the function returns `None` and the keyword fallback (with priority fix) takes over. This preserves the deterministic SOP fast-path for the 95%+ unambiguous requests while resolving the small fraction of genuinely compound prompts that previously misclassified.

**Audit trail**: Every disambiguation call is recorded in `09_audit/llm_calls.jsonl` with `model_role="disambiguate_intent"` and emits an `intent_disambiguation` event into `agent_trace.jsonl` with the conflict signals, LLM-returned mode, duration, and whether the keyword fallback fired.

### 6. Doctor warns on stale editable installs + mcp.json drift; new `aiswmm setup --refresh-mcp` (#113/#114)

| New WARN row (when triggered) | Remediation it advertises |
|---|---|
| `editable install - editable install points to a worktree at <path>` | `Re-run 'pip install -e .' from the main checkout to sync fixes.` |
| `mcp.json: <server> - mcp.json routes <server> to a different checkout (<path>)` | `Re-run 'aiswmm setup --refresh-mcp' to align with the active install, or sync that checkout manually.` |

**New flag**: `aiswmm setup --refresh-mcp` regenerates only `~/.aiswmm/mcp.json` against the active editable install. `config.toml`, `skills.json`, `memory.json`, and `setup_state.json` stay untouched. Idempotent.

## Portability hardening (the headline)

### Zero hardcoded watershed names in routing/inference code (#118)

Prior to this release, three load-bearing call sites in the agent runtime decided "what watershed does the user mean" by substring-matching the strings `"tecnopolo"` and `"todcreek"`:

1. `agentic_swmm/agent/runtime_loop.py:217-219` — `_case_slug` inferred case ID from prompt
2. `agentic_swmm/agent/continuation_classifier.py:79-80` — `_NEW_RUN_KEYWORDS` contained literal watershed names
3. `skills/swmm-modeling-memory/scripts/summarize_memory.py:359-361` — `project_key` substring-matched watershed names

A user applying `aiswmm` to a new watershed (e.g., a flood study in the Mekong delta) would silently get incorrect case labeling. This breaks the paper-grade claim "portable to arbitrary watersheds".

**Fix**: All three sites now consult `agentic_swmm.case.case_registry.list_cases()` and substring-match each registered case_id, display_name, and optional `aliases` (via `case_meta.extra.aliases`). Migration to a new watershed requires only:

```bash
mkdir -p cases/<your-watershed>
# write cases/<your-watershed>/case_meta.yaml with display_name and (optionally) aliases
```

No code changes required.

**AST-based regression guard**: `tests/test_no_hardcoded_watershed_names.py` walks the Python AST of the agent runtime and skills layer, filters out docstrings and module-level constants in allow-listed files (`commands/demo.py` which intentionally pins demos, `agent/prompts.py` for the warm-intro template example text, `agent/welcome.py` for the welcome banner example), and asserts zero matches for the known watershed names. Any future hardcoded reference fails the test loudly.

### Pre-existing architectural enforcement (worth noting)

The agent runtime cannot execute arbitrary scripts via `run_allowed_command` — the [`_command_allowed` allowlist](../../agentic_swmm/agent/tool_registry.py) restricts execution to four binaries: `pytest`, `python -m pytest`/`python -m agentic_swmm.cli`, `node scripts/*.mjs`, and `swmm5`. The LLM-driven agent cannot bypass the skill/MCP layer by directly invoking Python scripts. Combined with the portability fix above, this means **two pillars of portability are now both in place**: the runtime cannot shortcut around skills/MCP, and the routing layer does not hardcode any case identity.

## Acceptance test results (zero OpenAI API cost)

Run on the main HEAD immediately preceding the release tag. All checks zero-OpenAI-cost (mocked or pure local).

| Check | Result | Notes |
|---|---|---|
| `pytest` on the 12 PR-relevant test files | **53/53 passed** in 2.7s, 0 failures | |
| `aiswmm doctor` | All green except expected drift WARNs | The drift WARNs are the new check in #114 firing correctly — they cleared after `aiswmm setup --refresh-mcp` |
| `aiswmm skill list --registry` includes `swmm-end-to-end` | ✅ | Recovered |
| `aiswmm mcp list --registry` server count | 11 | Full |
| Mocked LLM end-to-end (`AISWMM_OPENAI_MOCK_RESPONSE`) | ✅ Warm intro 1-fire, mock response delivered, retro chrome rendered | No OpenAI call made |
| `aiswmm setup --refresh-mcp` against temp config | ✅ 11 servers written, all paths under active repo root | Doctor reports zero mcp.json drift WARNs after |
| Compound-intent regression | ✅ `"run Tod Creek demo and plot the figue"` → `mode=prepared_demo` | Was `existing_run_plot` |
| Plot X-axis tick count (30-day fixture) | ✅ 6 tick labels | Was 316 |
| Startup latency `aiswmm doctor` | 0.11s cold / 0.06s cached | |
| Hardcoded watershed names in agent runtime + memory skill | **0 code-level references** | AST guard enforced. Remaining hits are docstrings/comments/intentionally-pinned demo registry, all allow-listed |

## PR + commit map

| Fix | Issue | PR | Squash SHA on `main` |
|---|---|---|---|
| Warm intro fires once per session | (no issue, diagnosed via `/diagnose`) | [#108](https://github.com/Zhonghao1995/agentic-swmm-workflow/pull/108) | `472b0da` |
| Plot cold-start preheat | [#109](https://github.com/Zhonghao1995/agentic-swmm-workflow/issues/109) | [#110](https://github.com/Zhonghao1995/agentic-swmm-workflow/pull/110) | `5f7ab4f` |
| Plot X-axis readable | [#112](https://github.com/Zhonghao1995/agentic-swmm-workflow/issues/112) | [#115](https://github.com/Zhonghao1995/agentic-swmm-workflow/pull/115) | `d13fcb9` |
| Doctor + skill + mcp.json hygiene | [#113](https://github.com/Zhonghao1995/agentic-swmm-workflow/issues/113), [#114](https://github.com/Zhonghao1995/agentic-swmm-workflow/issues/114) | [#116](https://github.com/Zhonghao1995/agentic-swmm-workflow/pull/116) | `be2dbe9` |
| Workflow router LLM disambiguation | [#111](https://github.com/Zhonghao1995/agentic-swmm-workflow/issues/111) | [#117](https://github.com/Zhonghao1995/agentic-swmm-workflow/pull/117) | `0996b89` |
| Portability: case-registry-driven case detection | [#118](https://github.com/Zhonghao1995/agentic-swmm-workflow/issues/118) | [#119](https://github.com/Zhonghao1995/agentic-swmm-workflow/pull/119) | `254439a` |

## Migration guide (from v0.6.1)

The runtime user surface is **backwards compatible**. No breaking changes to CLI flags, tool schemas, or config formats. If you upgrade:

```bash
pip install --upgrade aiswmm==0.6.2a1
aiswmm setup --refresh-mcp        # optional but recommended if you have multiple checkouts
aiswmm doctor                      # confirm no remaining WARNs
```

If you ran `pip install -e .` from a Claude Code worktree by accident at some point, doctor will now warn about it. Re-run `pip install -e .` from your main checkout to re-point the editable install. (Pre-#113 versions had no warning, so this is an alpha you'll see for the first time.)

## Known limitations and out-of-scope items

- **OpenAI provider does not expose a per-call wall-clock timeout**. The new `intent_disambiguator` wraps the LLM call in a thread-based 5s timeout helper; this is functional but not as elegant as a native provider-level timeout. Future provider refactor will lift this into the provider interface.
- **The architecture audit subagent (dispatched separately after this release) will produce issues for**: README-promise-vs-implementation gap, deeper portability invariants beyond the three fixed call sites, and the centralization of all keyword matching into one deep module (today's 50+ keyword checks are still scattered across 6 files). These are recorded as `ready-for-agent` issues in the tracker; they are not part of v0.6.2a1.
- **Pre-existing GIS / SWMM subprocess-MCP test failures** (~22 in `pytest tests/`) are baseline-stable — they fail on `main` both before and after today's PRs (verified via `git stash` round-trips by every TDD subagent). Tracked separately; not introduced by this release.
- **Node.js 20 deprecation warnings on CI** — GitHub Actions will force `Node.js 24` starting 2026-06-02. The CI workflows in this release still pass with the deprecation annotations.

## Reproducibility footprint

For paper-grade reproducibility of any v0.6.2-alpha experiment:

- Use the pinned model snapshot: `gpt-5.5-2026-04-23` (this snapshot was pinned in v0.6.1 via PR #105 and is unchanged here).
- Use the exact PyPI version: `aiswmm==0.6.2a1`.
- Use the exact git tag: `v0.6.2a1`.
- Use the exact Docker base ref: `AGENTIC_SWMM_REF=v0.6.2a1` (built from this tag).
- Cite the merge commits in the PR map above.

All evidence runs in `runs/2026-05-16/` from this release prep are auditable artifacts produced under this version.
