personal memory agent
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(routines): accept dict cadences in create validator

Sprint 4 added dict cadences ({"type": "activity-anticipation", "offset_minutes": -30}) and wired the supervisor dispatcher to handle them, but the CLI validator _validate_routine_cadence was left behind and hard-rejected every non-string cadence. That broke `sol call routines create --template meeting-prep`, forcing manual routines.json edits.

Extend the validator to accept dict cadences with a known `type`, mirroring the dispatcher's int coercion for `offset_minutes`. Add reciprocal sync comments in both the validator and the dispatcher so future cadence types get updated in both places. New tests cover the meeting-prep template, a string override of a dict default, and the missing-type / bad-offset rejection paths.

Co-Authored-By: OpenAI Codex <codex@openai.com>

+117 -1
+84
tests/test_routines.py
··· 441 441 assert result.exit_code == 1 442 442 assert "template 'nonexistent' not found" in result.stderr 443 443 444 + def test_create_template_dict_cadence_persisted(self, journal_path): 445 + result = runner.invoke( 446 + call_app, 447 + ["routines", "create", "--template", "meeting-prep"], 448 + ) 449 + assert result.exit_code == 0 450 + config = get_config() 451 + assert len(config) == 1 452 + routine = next(iter(config.values())) 453 + assert routine["cadence"] == { 454 + "type": "activity-anticipation", 455 + "offset_minutes": -30, 456 + } 457 + 458 + def test_create_template_dict_cadence_overridden_by_string(self, journal_path): 459 + result = runner.invoke( 460 + call_app, 461 + [ 462 + "routines", 463 + "create", 464 + "--template", 465 + "meeting-prep", 466 + "--cadence", 467 + "0 9 * * *", 468 + ], 469 + ) 470 + assert result.exit_code == 0 471 + config = get_config() 472 + assert len(config) == 1 473 + routine = next(iter(config.values())) 474 + assert routine["cadence"] == "0 9 * * *" 475 + 444 476 def test_create_invalid_template_cadence_type(self, journal_path, monkeypatch): 445 477 import think.tools.routines as routines_cli 446 478 ··· 467 499 ) 468 500 assert result.exit_code == 1 469 501 assert "unsupported cadence type" in result.stderr 502 + 503 + def test_create_template_dict_cadence_missing_type(self, journal_path, monkeypatch): 504 + import think.tools.routines as routines_cli 505 + 506 + def _fake_template(name: str): 507 + return ( 508 + { 509 + "name": name, 510 + "description": "missing cadence type", 511 + "default_cadence": {"offset_minutes": -30}, 512 + "default_timezone": "UTC", 513 + "default_facets": [], 514 + }, 515 + "Instruction body", 516 + ) 517 + 518 + monkeypatch.setattr(routines_cli, "_load_template", _fake_template) 519 + result = runner.invoke( 520 + call_app, 521 + ["routines", "create", "--template", "bad-template"], 522 + ) 523 + assert result.exit_code == 1 524 + assert "type" in result.stderr 525 + assert "missing" in result.stderr 526 + 527 + def test_create_template_dict_cadence_bad_offset_minutes( 528 + self, journal_path, monkeypatch 529 + ): 530 + import think.tools.routines as routines_cli 531 + 532 + def _fake_template(name: str): 533 + return ( 534 + { 535 + "name": name, 536 + "description": "bad cadence offset", 537 + "default_cadence": { 538 + "type": "activity-anticipation", 539 + "offset_minutes": "not-a-number", 540 + }, 541 + "default_timezone": "UTC", 542 + "default_facets": [], 543 + }, 544 + "Instruction body", 545 + ) 546 + 547 + monkeypatch.setattr(routines_cli, "_load_template", _fake_template) 548 + result = runner.invoke( 549 + call_app, 550 + ["routines", "create", "--template", "bad-template"], 551 + ) 552 + assert result.exit_code == 1 553 + assert "offset_minutes" in result.stderr 470 554 471 555 472 556 class TestNameResolution:
+1
think/routines.py
··· 439 439 if cron_matches(cadence, local_now): 440 440 _last_fired[routine_id] = minute_key 441 441 _run_routine(routine) 442 + # Keep this cadence-object dispatch in sync with think.tools.routines._validate_routine_cadence(). 442 443 elif ( 443 444 isinstance(cadence, dict) and cadence.get("type") == "activity-anticipation" 444 445 ):
+32 -1
think/tools/routines.py
··· 112 112 raise typer.Exit(1) 113 113 return 114 114 115 - typer.echo("Error: invalid cadence: unsupported cadence type", err=True) 115 + # Keep this cadence-object validation in sync with think.routines.check(). 116 + if isinstance(cadence, dict): 117 + if "type" not in cadence: 118 + typer.echo("Error: invalid cadence: missing 'type' field", err=True) 119 + raise typer.Exit(1) 120 + 121 + cadence_type = cadence["type"] 122 + if not isinstance(cadence_type, str): 123 + typer.echo("Error: invalid cadence: 'type' must be a string", err=True) 124 + raise typer.Exit(1) 125 + 126 + if cadence_type == "activity-anticipation": 127 + if "offset_minutes" in cadence: 128 + try: 129 + int(cadence["offset_minutes"]) 130 + except (TypeError, ValueError): 131 + typer.echo( 132 + "Error: invalid cadence: offset_minutes must be an integer", 133 + err=True, 134 + ) 135 + raise typer.Exit(1) 136 + return 137 + 138 + typer.echo( 139 + f"Error: invalid cadence: unsupported cadence type {cadence_type!r}", 140 + err=True, 141 + ) 142 + raise typer.Exit(1) 143 + 144 + typer.echo( 145 + "Error: invalid cadence: must be a cron string or cadence object", err=True 146 + ) 116 147 raise typer.Exit(1) 117 148 118 149