personal memory agent
0
fork

Configure Feed

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

Add --dry-run flag to sol dream

Shows what agents would be spawned without executing anything.
Standalone functions call existing utilities (get_muse_configs,
get_enabled_facets, get_active_facets, etc.) rather than threading
a flag through the execution functions. Supports all modes: daily,
segment, --segments, --activity, and --flush.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+356
+97
tests/test_dream_dry_run.py
··· 1 + # SPDX-License-Identifier: AGPL-3.0-only 2 + # Copyright (c) 2026 sol pbc 3 + 4 + """Tests for dream --dry-run.""" 5 + 6 + import importlib 7 + import shutil 8 + from pathlib import Path 9 + 10 + FIXTURES = Path("tests/fixtures") 11 + 12 + 13 + def copy_journal(tmp_path: Path) -> Path: 14 + src = FIXTURES / "journal" 15 + dest = tmp_path / "journal" 16 + shutil.copytree(src, dest) 17 + return dest 18 + 19 + 20 + def test_dry_run_daily(tmp_path, monkeypatch, capsys): 21 + """Dry-run daily mode prints prompts without spawning agents.""" 22 + mod = importlib.import_module("think.dream") 23 + journal = copy_journal(tmp_path) 24 + monkeypatch.setenv("JOURNAL_PATH", str(journal)) 25 + 26 + mod.dry_run("20240101") 27 + 28 + out = capsys.readouterr().out 29 + assert "2024-01-01" in out 30 + assert "Pre-phase" in out 31 + assert "Post-phase" in out 32 + assert "Priority" in out 33 + assert "Total:" in out 34 + 35 + 36 + def test_dry_run_segment(tmp_path, monkeypatch, capsys): 37 + """Dry-run segment mode skips pre/post phases.""" 38 + mod = importlib.import_module("think.dream") 39 + journal = copy_journal(tmp_path) 40 + monkeypatch.setenv("JOURNAL_PATH", str(journal)) 41 + 42 + mod.dry_run("20240101", segment="120000_300") 43 + 44 + out = capsys.readouterr().out 45 + assert "segment 120000_300" in out 46 + assert "Pre-phase" not in out 47 + assert "Post-phase" not in out 48 + 49 + 50 + def test_dry_run_segments_lists_all(tmp_path, monkeypatch, capsys): 51 + """Dry-run --segments lists discovered segments.""" 52 + mod = importlib.import_module("think.dream") 53 + journal = copy_journal(tmp_path) 54 + monkeypatch.setenv("JOURNAL_PATH", str(journal)) 55 + 56 + mod.dry_run("20240101", segments=True) 57 + 58 + out = capsys.readouterr().out 59 + assert "segments" in out.lower() 60 + 61 + 62 + def test_dry_run_flush(tmp_path, monkeypatch, capsys): 63 + """Dry-run --flush shows flush-eligible agents.""" 64 + mod = importlib.import_module("think.dream") 65 + journal = copy_journal(tmp_path) 66 + monkeypatch.setenv("JOURNAL_PATH", str(journal)) 67 + 68 + mod.dry_run("20240101", flush=True, segment="120000_300") 69 + 70 + out = capsys.readouterr().out 71 + assert "flush" in out.lower() 72 + 73 + 74 + def test_dry_run_shows_refresh(tmp_path, monkeypatch, capsys): 75 + """Dry-run indicates refresh mode in header.""" 76 + mod = importlib.import_module("think.dream") 77 + journal = copy_journal(tmp_path) 78 + monkeypatch.setenv("JOURNAL_PATH", str(journal)) 79 + 80 + mod.dry_run("20240101", refresh=True) 81 + 82 + out = capsys.readouterr().out 83 + assert "(refresh)" in out 84 + 85 + 86 + def test_dry_run_no_callosum(tmp_path, monkeypatch, capsys): 87 + """Dry-run works without callosum connection.""" 88 + mod = importlib.import_module("think.dream") 89 + journal = copy_journal(tmp_path) 90 + monkeypatch.setenv("JOURNAL_PATH", str(journal)) 91 + 92 + # Save and clear _callosum to verify dry_run doesn't create one 93 + prev = mod._callosum 94 + monkeypatch.setattr(mod, "_callosum", None) 95 + mod.dry_run("20240101") 96 + assert mod._callosum is None 97 + monkeypatch.setattr(mod, "_callosum", prev)
+259
think/dream.py
··· 1094 1094 return total_failed == 0 1095 1095 1096 1096 1097 + def dry_run( 1098 + day: str, 1099 + *, 1100 + segment: str | None = None, 1101 + segments: bool = False, 1102 + facet: str | None = None, 1103 + activity: str | None = None, 1104 + flush: bool = False, 1105 + refresh: bool = False, 1106 + stream: str | None = None, 1107 + ) -> None: 1108 + """Print what dream would execute without spawning any agents.""" 1109 + day_formatted = iso_date(day) 1110 + 1111 + if activity: 1112 + _dry_run_activity(day, day_formatted, activity, facet or "", refresh) 1113 + return 1114 + 1115 + if flush: 1116 + _dry_run_flush(day, segment or "") 1117 + return 1118 + 1119 + if segments: 1120 + segs = cluster_segments(day) 1121 + if not segs: 1122 + print(f"No segments found for {day}") 1123 + return 1124 + print(f"Day {day_formatted} — re-process {len(segs)} segments\n") 1125 + for i, seg in enumerate(segs, 1): 1126 + seg_key = seg["key"] 1127 + seg_stream = seg.get("stream") 1128 + label = f" [{i}/{len(segs)}] {seg_key} ({seg['start']}-{seg['end']})" 1129 + if seg_stream: 1130 + label += f" stream={seg_stream}" 1131 + print(label) 1132 + print() 1133 + # Show what prompts would run per segment 1134 + all_prompts = get_muse_configs(schedule="segment") 1135 + if all_prompts: 1136 + _print_prompt_table(all_prompts, day, segment="<each>", stream=stream) 1137 + return 1138 + 1139 + # Default: full daily or segment run 1140 + target_schedule = "segment" if segment else "daily" 1141 + all_prompts = get_muse_configs(schedule=target_schedule) 1142 + 1143 + header = f"Day {day_formatted}" 1144 + if segment: 1145 + header += f" segment {segment}" 1146 + if refresh: 1147 + header += " (refresh)" 1148 + print(header + "\n") 1149 + 1150 + if not segment: 1151 + print("Pre-phase: sol sense --day " + day) 1152 + 1153 + if not all_prompts: 1154 + print(f"No prompts for schedule: {target_schedule}") 1155 + else: 1156 + _print_prompt_table( 1157 + all_prompts, day, segment=segment, refresh=refresh, stream=stream 1158 + ) 1159 + 1160 + if not segment: 1161 + print("Post-phase: sol indexer --rescan") 1162 + print("Post-phase: sol journal-stats") 1163 + 1164 + 1165 + def _print_prompt_table( 1166 + prompts: dict[str, dict], 1167 + day: str, 1168 + *, 1169 + segment: str | None = None, 1170 + refresh: bool = False, 1171 + stream: str | None = None, 1172 + ) -> None: 1173 + """Print a grouped-by-priority table of prompts.""" 1174 + enabled_facets = get_enabled_facets() 1175 + 1176 + if segment and segment != "<each>": 1177 + active_facets = set( 1178 + f 1179 + for f in load_segment_facets(day, segment, stream=stream) 1180 + if f in enabled_facets 1181 + ) 1182 + else: 1183 + active_facets = get_active_facets(day) 1184 + 1185 + # Group by priority 1186 + groups: dict[int, list[tuple[str, dict]]] = {} 1187 + for name, config in prompts.items(): 1188 + pri = config["priority"] 1189 + groups.setdefault(pri, []).append((name, config)) 1190 + 1191 + total = 0 1192 + for priority in sorted(groups.keys()): 1193 + items = groups[priority] 1194 + print(f"Priority {priority}:") 1195 + for name, config in items: 1196 + is_gen = config["type"] == "generate" 1197 + type_label = "gen" if is_gen else "agent" 1198 + output_fmt = config.get("output", "md") if is_gen else None 1199 + 1200 + if config.get("multi_facet"): 1201 + always = config.get("always", False) 1202 + target_facets = [ 1203 + f for f in enabled_facets if always or f in active_facets 1204 + ] 1205 + skipped = [f for f in enabled_facets if f not in target_facets] 1206 + for f in target_facets: 1207 + status = ( 1208 + _output_status( 1209 + day, name, segment, output_fmt, facet=f, stream=stream 1210 + ) 1211 + if is_gen 1212 + else "" 1213 + ) 1214 + print(f" {type_label} {name}/{f}{status}") 1215 + total += 1 1216 + if skipped: 1217 + print(f" skip {name} — no activity: {', '.join(skipped)}") 1218 + else: 1219 + status = ( 1220 + _output_status(day, name, segment, output_fmt, stream=stream) 1221 + if is_gen 1222 + else "" 1223 + ) 1224 + print(f" {type_label} {name}{status}") 1225 + total += 1 1226 + print() 1227 + 1228 + print(f"Total: {total} agents") 1229 + 1230 + 1231 + def _output_status( 1232 + day: str, 1233 + name: str, 1234 + segment: str | None, 1235 + output_format: str | None, 1236 + *, 1237 + facet: str | None = None, 1238 + stream: str | None = None, 1239 + ) -> str: 1240 + """Return a short status suffix for a generator output file.""" 1241 + if segment == "<each>": 1242 + return "" 1243 + path = get_output_path( 1244 + day_path(day), 1245 + name, 1246 + segment=segment, 1247 + output_format=output_format, 1248 + facet=facet, 1249 + stream=stream, 1250 + ) 1251 + if path.exists(): 1252 + return " (exists)" 1253 + return " (new)" 1254 + 1255 + 1256 + def _dry_run_activity( 1257 + day: str, day_formatted: str, activity_id: str, facet: str, refresh: bool 1258 + ) -> None: 1259 + """Dry-run for --activity mode.""" 1260 + records = load_activity_records(facet, day) 1261 + record = next((r for r in records if r.get("id") == activity_id), None) 1262 + 1263 + if not record: 1264 + print(f"Activity not found: {activity_id} in facet '{facet}' on {day}") 1265 + return 1266 + 1267 + activity_type = record.get("activity", "") 1268 + segments = record.get("segments", []) 1269 + 1270 + print( 1271 + f"Day {day_formatted} --activity {activity_id} --facet {facet}" 1272 + + (" (refresh)" if refresh else "") 1273 + + "\n" 1274 + ) 1275 + print(f" type: {activity_type}") 1276 + print(f" segments: {len(segments)}") 1277 + 1278 + all_prompts = get_muse_configs(schedule="activity") 1279 + matching = { 1280 + n: c 1281 + for n, c in all_prompts.items() 1282 + if "*" in c.get("activities", []) or activity_type in c.get("activities", []) 1283 + } 1284 + 1285 + if not matching: 1286 + print(f"\n No agents match activity type '{activity_type}'") 1287 + return 1288 + 1289 + groups: dict[int, list[tuple[str, dict]]] = {} 1290 + for n, c in matching.items(): 1291 + groups.setdefault(c["priority"], []).append((n, c)) 1292 + 1293 + print() 1294 + total = 0 1295 + for priority in sorted(groups.keys()): 1296 + items = groups[priority] 1297 + print(f"Priority {priority}:") 1298 + for n, c in items: 1299 + is_gen = c["type"] == "generate" 1300 + type_label = "gen" if is_gen else "agent" 1301 + output_fmt = c.get("output", "md") if is_gen else None 1302 + status = "" 1303 + if is_gen: 1304 + path = get_activity_output_path( 1305 + facet, day, activity_id, n, output_format=output_fmt 1306 + ) 1307 + status = " (exists)" if path.exists() else " (new)" 1308 + print(f" {type_label} {n}{status}") 1309 + total += 1 1310 + print() 1311 + 1312 + print(f"Total: {total} agents") 1313 + 1314 + 1315 + def _dry_run_flush(day: str, segment: str) -> None: 1316 + """Dry-run for --flush mode.""" 1317 + all_prompts = get_muse_configs(schedule="segment") 1318 + flush_prompts = { 1319 + n: c 1320 + for n, c in all_prompts.items() 1321 + if isinstance(c.get("hook"), dict) and c["hook"].get("flush") 1322 + } 1323 + 1324 + day_formatted = iso_date(day) 1325 + print(f"Day {day_formatted} --flush segment {segment}\n") 1326 + 1327 + if not flush_prompts: 1328 + print(" No flush-eligible agents") 1329 + return 1330 + 1331 + for n, c in flush_prompts.items(): 1332 + type_label = "gen" if c["type"] == "generate" else "agent" 1333 + print(f" {type_label} {n}") 1334 + 1335 + print(f"\nTotal: {len(flush_prompts)} agents") 1336 + 1337 + 1097 1338 def parse_args() -> argparse.ArgumentParser: 1098 1339 parser = argparse.ArgumentParser( 1099 1340 description="Run processing tasks on a journal day or segment" ··· 1146 1387 action="store_true", 1147 1388 help="List days with pending daily processing and exit", 1148 1389 ) 1390 + parser.add_argument( 1391 + "--dry-run", 1392 + action="store_true", 1393 + help="Show what would run without executing anything", 1394 + ) 1149 1395 return parser 1150 1396 1151 1397 ··· 1218 1464 1219 1465 if args.segments and (args.segment or args.facet): 1220 1466 parser.error("--segments is incompatible with --segment and --facet") 1467 + 1468 + if args.dry_run: 1469 + dry_run( 1470 + day, 1471 + segment=args.segment, 1472 + segments=args.segments, 1473 + facet=args.facet, 1474 + activity=args.activity, 1475 + flush=args.flush, 1476 + refresh=args.refresh, 1477 + stream=args.stream, 1478 + ) 1479 + sys.exit(0) 1221 1480 1222 1481 # Start callosum connection 1223 1482 _callosum = CallosumConnection(defaults={"rev": get_rev()})