Skip to content

Commit a644b49

Browse files
dennys246claude
andcommitted
feat(v1): persona_cleanup Stage 1 — --sim-mode flag + deprecation warnings
Stage 1 of docs/plans/persona_cleanup_and_mode_transition.md. Strictly additive: introduces the new --sim-mode CLI flag, makes --persona / --sim-persona deprecated, and emits DeprecationWarning when register_persona() is called. Hard-removal lands in 1.1 per the C4-C6 timing contract in docs/plans/v1_refinement.md Section 5 (0.9 warnings, 1.1 hard errors). Constraint discovered during implementation: the plan proposed --mode as the new flag, but --mode is already owned by the core run-mode flag (live/train/reflection/sleep/agentic/exploration) at cli_parser.py:57. Renaming the existing --mode is a separate breaking change with its own deprecation cycle, out of scope for Stage 1. Decision: ship --sim-mode only (mirroring --sim-persona/--persona). Documented in extension_api.md. Resolution rules in cli_utils._resolve_persona_mode (single chokepoint that downstream Stages 3-5 will rename when dispatch migrates off persona names): - both flags + values match → silent (no warning), --sim-mode wins - both flags + values differ → stderr WARNING, --sim-mode wins - --sim-mode only → silent - --persona only → DeprecationWarning + stderr line for visibility - neither → default "adversarial" (preserves historical behavior) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 1a2743e commit a644b49

8 files changed

Lines changed: 220 additions & 20 deletions

File tree

docs/user/extension_api.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ This page documents **extension surfaces** — what third parties plug INTO Maxi
1717
| 5 | Custom action sinks | stable | [`ActionSink` protocol](#5-custom-action-sinks) |
1818
| 6 | Bio-system bridges | experimental | [`PainBus.subscribe` / `ReactionBus.subscribe`](#6-bio-system-bridges) |
1919
| 7 | Event subscriptions | experimental | [`maxim.on(event_name, callback)`](#7-event-subscriptions) |
20-
| 8 | Custom personas | ⚠️ experimental | [`maxim.register_persona(...)`](#8-custom-personas) |
20+
| 8 | Custom personas | ⛔ deprecated in 0.9 — removed in 1.1 | [`maxim.register_persona(...)`](#8-custom-personas) |
2121

2222
---
2323

@@ -408,7 +408,11 @@ handle.unsubscribe()
408408

409409
## 8. Custom personas
410410

411-
**Stability:** ⚠️ experimental — the persona system may be redesigned alongside future Mother Maxim / orchestrator work (see [stable_api.md](stable_api.md) and `docs/plans/persona_cleanup_and_mode_transition.md`). The verb name and signature may change in a future minor release.
411+
**Stability:****deprecated in 0.9 — removed in 1.1.** `maxim.register_persona()` emits `DeprecationWarning` in 0.9 / 1.0 and will raise in 1.1. The persona system is being replaced by `--sim-mode` (an orchestrator flow-shape selector) plus the bio-emergent disposition mechanics tracked in `docs/plans/bio_emergent_persona_foundations.md`. The CLI flag `--persona` (and its `--sim-persona` alias) is also deprecated in 0.9; use `--sim-mode` instead. See [`docs/plans/persona_cleanup_and_mode_transition.md`](../plans/persona_cleanup_and_mode_transition.md) for the migration timeline and rationale.
412+
413+
> **Note on flag naming:** the persona-cleanup plan originally proposed the short alias `--mode`, but that token is already owned by the core run-mode flag (`--mode {live,train,reflection,sleep,agentic,exploration}`). Stage 1 ships `--sim-mode` only; freeing `--mode` for sim use is a separate breaking change with its own deprecation cycle.
414+
415+
> **Note (audit finding):** registered personas currently flow through to reports and logs as a label; the orchestrator does not inject the supplied `context_prompt` into the agent prompt today. The rich prompt strings shipped in `simulation/personas.py` exist as scaffolding for behavioural shaping that is being moved to `--sim-mode` and the bio-emergent disposition mechanics. Stage 5 of the cleanup plan removes the unused field.
412416
413417
Personas shape how the simulation orchestrator framing affects an agent — adversarial probing, cooperative coaching, etc. Register a persona once and reference it by name in `--persona <name>` or the `persona=` argument to `imagine()`/`run()`.
414418

docs/user/stable_api.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ This page lists what is **stable** in pymaxim 1.0 and what is **experimental**.
3636
|---|---|---|
3737
| `maxim.research(...) -> ResearchResult` | ⚠️ Experimental | Research orchestrator surface still evolving. Prompt templates, paper structure, and reviewer logic may change. |
3838
| `maxim.on(event_name, callback) -> EventHandle` | ⚠️ Experimental | Event names + payload fields may grow. Subscription mechanism may evolve to support filters or async callbacks. |
39-
| `maxim.register_persona(...)` | ⚠️ Experimental | Persona system may be redesigned alongside future Mother Maxim/orchestrator work. |
39+
| `maxim.register_persona(...)` | ⛔ Deprecated in 0.9 — removed in 1.1 | Emits `DeprecationWarning` in 0.9 / 1.0; raises in 1.1. Persona system is being replaced by `--sim-mode` (orchestrator flow-shape) plus bio-emergent disposition mechanics. See [`docs/plans/persona_cleanup_and_mode_transition.md`](../plans/persona_cleanup_and_mode_transition.md). |
4040

4141
---
4242

src/maxim/api.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1708,10 +1708,17 @@ def register_persona(
17081708
) -> None:
17091709
"""Register a custom simulation persona.
17101710
1711-
**Experimental** (CC2): the persona system may be redesigned
1712-
alongside future Mother Maxim/orchestrator work. Field set may
1713-
change in 1.x. See
1714-
[docs/user/stable_api.md](../../docs/user/stable_api.md) for the contract.
1711+
**Deprecated in 0.9 — removed in 1.1.** Stage 1 of
1712+
[docs/plans/persona_cleanup_and_mode_transition.md](../../docs/plans/persona_cleanup_and_mode_transition.md)
1713+
starts the deprecation cycle: ``DeprecationWarning`` in 0.9 / 1.0,
1714+
``raise`` in 1.1. Orchestrator behaviour shaping is moving to
1715+
``--sim-mode`` (a flow-shape selector) plus the bio-emergent
1716+
disposition mechanics tracked in
1717+
``docs/plans/bio_emergent_persona_foundations.md``. Registered
1718+
personas currently flow through to reports and logs as a label; the
1719+
orchestrator does not inject the supplied ``context_prompt`` into the
1720+
agent prompt today (audit finding in the plan). Stage 5 of the
1721+
cleanup plan removes the unused field.
17151722
17161723
Custom personas are available for ``imagine()`` and ``campaign()``
17171724
via the ``persona`` parameter.
@@ -1734,6 +1741,17 @@ def register_persona(
17341741
)
17351742
maxim.imagine(goal="test", persona="medical_tester")
17361743
"""
1744+
import warnings
1745+
1746+
warnings.warn(
1747+
"maxim.register_persona() is deprecated and will be removed in 1.1. "
1748+
"The persona system is being replaced by --sim-mode (orchestrator "
1749+
"flow-shape) plus bio-emergent disposition mechanics. "
1750+
"See docs/plans/persona_cleanup_and_mode_transition.md.",
1751+
DeprecationWarning,
1752+
stacklevel=2,
1753+
)
1754+
17371755
from maxim.simulation.personas import SIMULATION_PERSONAS, Persona
17381756

17391757
SIMULATION_PERSONAS[name.lower()] = Persona(

src/maxim/cli.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -863,12 +863,12 @@ def _force_exit_handler(signum, frame):
863863
if not _is_sim_mode:
864864
if getattr(args, "sim_goal", None) is not None:
865865
print("Error: --goal / --sim-goal requires --sim agent or --sim research.")
866-
print(' Usage: maxim --sim agent --goal "test safety" --persona adversarial')
866+
print(' Usage: maxim --sim agent --goal "test safety" --sim-mode adversarial')
867867
print(' maxim --sim research --goal "hippocampal recall" --campaign <yaml>')
868868
sys.exit(1)
869869
if getattr(args, "sim_persona", "adversarial") != "adversarial":
870-
print("Error: --persona / --sim-persona requires --sim agent (simulation mode).")
871-
print(' Usage: maxim --sim agent --goal "test safety" --persona adversarial')
870+
print("Error: --sim-mode / --persona / --sim-persona requires --sim agent (simulation mode).")
871+
print(' Usage: maxim --sim agent --goal "test safety" --sim-mode adversarial')
872872
sys.exit(1)
873873
if getattr(args, "resume_sim", None) is not None and sim_path is None:
874874
print("Error: --resume-sim requires --sim (simulation mode).")

src/maxim/cli_parser.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -344,11 +344,30 @@ def _build_parser() -> argparse.ArgumentParser:
344344
"--sim-persona",
345345
"--persona",
346346
type=str,
347-
default="adversarial",
347+
default=argparse.SUPPRESS,
348348
dest="sim_persona",
349349
metavar="PERSONA",
350-
help="Orchestrator persona for simulation (adversarial, cooperative, confused, "
351-
"escalating, campaign, refinement). Alias: --persona",
350+
help="[DEPRECATED in 0.9 — removed in 1.1] Orchestrator persona for simulation "
351+
"(adversarial, cooperative, confused, escalating, campaign, refinement). "
352+
"Use --sim-mode instead. See docs/plans/persona_cleanup_and_mode_transition.md.",
353+
)
354+
# Note: the short alias `--mode` is intentionally NOT added here because
355+
# the core run-mode flag at line 57 already owns that token (live/train/
356+
# reflection/sleep/agentic/exploration). Freeing `--mode` for sim use is
357+
# a separate breaking change with its own deprecation cycle and is out of
358+
# scope for Stage 1 of persona_cleanup_and_mode_transition.md.
359+
sim.add_argument(
360+
"--sim-mode",
361+
type=str,
362+
default=argparse.SUPPRESS,
363+
dest="sim_mode",
364+
metavar="MODE",
365+
help="Orchestrator mode for simulation. Accepts the same values as --persona "
366+
"(adversarial, cooperative, confused, escalating, campaign, refinement, "
367+
"researcher, sweep, dungeon_master, adventure_architect) plus 'neutral'. "
368+
"Replacement for --persona, which is deprecated in 0.9 and removed in 1.1. "
369+
"Default: adversarial (resolved by cli_utils._resolve_persona_mode when "
370+
"neither --sim-mode nor --persona is passed).",
352371
)
353372
sim.add_argument(
354373
"--aut-model",

src/maxim/cli_utils.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,65 @@ def normalize_epoch_value(value: object) -> int:
2222
return epochs if epochs > 0 else 0
2323

2424

25+
# Default for --persona / --mode when neither flag is passed. Preserves
26+
# the historical CLI behavior (the orchestrator used "adversarial" as
27+
# the silent default before the persona deprecation cycle started in 0.9).
28+
# Once --persona is hard-removed in 1.1 and Stages 3-5 of
29+
# docs/plans/persona_cleanup_and_mode_transition.md migrate dispatch off
30+
# persona names entirely, this constant goes with it.
31+
_DEFAULT_SIM_PERSONA = "adversarial"
32+
33+
34+
def _resolve_persona_mode(args: argparse.Namespace) -> None:
35+
"""Resolve --mode / --persona precedence + emit the persona deprecation warning.
36+
37+
Stage 1 of docs/plans/persona_cleanup_and_mode_transition.md:
38+
--mode is the new flag, --persona is deprecated in 0.9 and removed
39+
in 1.1 (matches the C4-C6 timing contract in v1_refinement.md
40+
Section 5). Both flags are accepted through 1.0; --mode wins on
41+
conflict.
42+
43+
After this runs, args.sim_persona is always set (the rest of the CLI
44+
treats sim_persona as the canonical dispatch slot — Stages 3-5 will
45+
migrate dispatch off persona names entirely).
46+
"""
47+
has_persona = hasattr(args, "sim_persona")
48+
has_mode = hasattr(args, "sim_mode")
49+
persona_val = getattr(args, "sim_persona", None)
50+
mode_val = getattr(args, "sim_mode", None)
51+
52+
if has_mode and has_persona:
53+
if persona_val != mode_val:
54+
print(
55+
f"WARNING: --sim-mode {mode_val!r} and --persona {persona_val!r} both "
56+
f"specified; using --sim-mode {mode_val!r}. --persona is deprecated and "
57+
f"will be removed in 1.1.",
58+
file=sys.stderr,
59+
)
60+
args.sim_persona = mode_val
61+
elif has_mode:
62+
args.sim_persona = mode_val
63+
elif has_persona:
64+
import warnings
65+
66+
msg = (
67+
f"--persona / --sim-persona is deprecated and will be removed in 1.1. "
68+
f"Use --sim-mode {persona_val!r} instead. "
69+
f"See docs/plans/persona_cleanup_and_mode_transition.md."
70+
)
71+
# Stderr line for human visibility (DeprecationWarning is silenced
72+
# by Python's default warning filter outside __main__).
73+
print(f"DeprecationWarning: {msg}", file=sys.stderr)
74+
warnings.warn(msg, DeprecationWarning, stacklevel=3)
75+
# args.sim_persona already equals persona_val.
76+
else:
77+
args.sim_persona = _DEFAULT_SIM_PERSONA
78+
79+
2580
def normalize_args(args: argparse.Namespace) -> None:
2681
"""Coerce and validate parsed CLI arguments, setting env vars as needed."""
82+
_resolve_persona_mode(args)
83+
2784
audio_raw = str(getattr(args, "audio", "true")).strip().lower()
2885
if audio_raw in ("1", "true", "t", "yes", "y", "on"):
2986
args.audio = True

tests/unit/test_api_expansion.py

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -296,16 +296,24 @@ def my_analysis(data: str, depth: int = 3) -> str:
296296

297297
class TestPersonaRegistration:
298298
def test_register_custom_persona(self):
299+
import warnings
300+
299301
from maxim.api import register_persona
300302
from maxim.simulation.personas import SIMULATION_PERSONAS
301303

302-
register_persona(
303-
name="test_api_persona",
304-
description="Test persona from API",
305-
focus="Testing",
306-
context_prompt="You are a test persona.",
307-
max_initiative=0.7,
308-
)
304+
# Stage 1 of persona_cleanup_and_mode_transition.md emits a
305+
# DeprecationWarning here. The verb itself still works through
306+
# 1.0; suppress the warning so this side of the test stays focused
307+
# on registration behaviour.
308+
with warnings.catch_warnings():
309+
warnings.simplefilter("ignore", DeprecationWarning)
310+
register_persona(
311+
name="test_api_persona",
312+
description="Test persona from API",
313+
focus="Testing",
314+
context_prompt="You are a test persona.",
315+
max_initiative=0.7,
316+
)
309317

310318
assert "test_api_persona" in SIMULATION_PERSONAS
311319
p = SIMULATION_PERSONAS["test_api_persona"]
@@ -315,6 +323,26 @@ def test_register_custom_persona(self):
315323
# Cleanup
316324
SIMULATION_PERSONAS.pop("test_api_persona", None)
317325

326+
def test_register_persona_emits_deprecation_warning(self):
327+
"""Stage 1: register_persona() must emit DeprecationWarning. Removed in 1.1."""
328+
import warnings
329+
330+
from maxim.api import register_persona
331+
from maxim.simulation.personas import SIMULATION_PERSONAS
332+
333+
with warnings.catch_warnings(record=True) as caught:
334+
warnings.simplefilter("always", DeprecationWarning)
335+
register_persona(name="test_deprecation_persona")
336+
337+
SIMULATION_PERSONAS.pop("test_deprecation_persona", None)
338+
339+
dep = [w for w in caught if issubclass(w.category, DeprecationWarning)]
340+
assert len(dep) == 1, f"expected 1 DeprecationWarning, got {len(dep)}"
341+
msg = str(dep[0].message)
342+
assert "register_persona" in msg
343+
assert "1.1" in msg
344+
assert "--sim-mode" in msg
345+
318346

319347
# ---------------------------------------------------------------------------
320348
# observe() — session-linked (verify existing behavior)

tests/unit/test_cli_utils.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,80 @@ def test_epochs_normalized(self):
8181
assert args.epochs == 0
8282

8383

84+
class TestPersonaModeResolution:
85+
"""Stage 1 of docs/plans/persona_cleanup_and_mode_transition.md.
86+
87+
--mode is the new flag, --persona is deprecated in 0.9 and removed
88+
in 1.1. After normalize_args, args.sim_persona is always set (the
89+
rest of the CLI treats it as the canonical dispatch slot).
90+
"""
91+
92+
def _bare_args(self, **kwargs):
93+
defaults = {
94+
"audio": "true",
95+
"interactive": "true",
96+
"mode": "active",
97+
"epochs": 0,
98+
"language_model": None,
99+
"cloud_fallback": None,
100+
"cloud_lane": None,
101+
"cloud_budget": None,
102+
"segmentation_model": None,
103+
}
104+
defaults.update(kwargs)
105+
return argparse.Namespace(**defaults)
106+
107+
def test_neither_flag_falls_back_to_default(self):
108+
args = self._bare_args()
109+
normalize_args(args)
110+
assert args.sim_persona == "adversarial"
111+
112+
def test_mode_only_sets_sim_persona(self):
113+
args = self._bare_args(sim_mode="researcher")
114+
normalize_args(args)
115+
assert args.sim_persona == "researcher"
116+
117+
def test_mode_only_does_not_warn(self, capsys, recwarn):
118+
args = self._bare_args(sim_mode="cooperative")
119+
normalize_args(args)
120+
captured = capsys.readouterr()
121+
assert "DeprecationWarning" not in captured.err
122+
assert not any(issubclass(w.category, DeprecationWarning) for w in recwarn.list)
123+
124+
def test_persona_only_emits_deprecation(self, capsys, recwarn):
125+
args = self._bare_args(sim_persona="researcher")
126+
normalize_args(args)
127+
# sim_persona preserved so dispatch keeps working
128+
assert args.sim_persona == "researcher"
129+
# Stderr line for human visibility
130+
captured = capsys.readouterr()
131+
assert "DeprecationWarning" in captured.err
132+
assert "--sim-mode" in captured.err
133+
# DeprecationWarning emitted for programmatic catch
134+
dep_warnings = [w for w in recwarn.list if issubclass(w.category, DeprecationWarning)]
135+
assert len(dep_warnings) == 1
136+
assert "1.1" in str(dep_warnings[0].message)
137+
138+
def test_both_flags_consistent_prefer_mode_silently(self, capsys, recwarn):
139+
args = self._bare_args(sim_persona="adversarial", sim_mode="adversarial")
140+
normalize_args(args)
141+
assert args.sim_persona == "adversarial"
142+
# No conflict warning because values match; no deprecation either
143+
# because --sim-mode is the canonical path.
144+
captured = capsys.readouterr()
145+
assert "WARNING" not in captured.err
146+
assert "DeprecationWarning" not in captured.err
147+
assert not any(issubclass(w.category, DeprecationWarning) for w in recwarn.list)
148+
149+
def test_both_flags_conflict_warns_and_prefers_mode(self, capsys):
150+
args = self._bare_args(sim_persona="researcher", sim_mode="adversarial")
151+
normalize_args(args)
152+
assert args.sim_persona == "adversarial"
153+
captured = capsys.readouterr()
154+
assert "WARNING" in captured.err
155+
assert "--sim-mode" in captured.err and "--persona" in captured.err
156+
157+
84158
class TestGpuAvailable:
85159
def test_returns_bool(self):
86160
result = gpu_available()

0 commit comments

Comments
 (0)