a digital entity named phi that roams bsky phi.zzstoatzz.io
2
fork

Configure Feed

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

turn phi outward — derive operator from handle, rein in self-reference

- musing prompt looks at the world (feeds, discovery, timeline, network, web)
- inner critic shrunk to a one-line descriptive [SELF-AWARENESS] signal
- prefect check rewritten as goal-oriented (no procedural counting)
- personality file leads with curiosities; voice rules demoted to tail
- new [OWNED FEEDS] block surfaces curated graze feeds by name
- [OPERATOR] block resolves owner_handle (Handle | Did) via SDK getProfile,
caches 1h, replaces every hardcoded "nate" reference in prompts/docstrings
- extraction example uses generic name

+236 -120
+15 -17
personalities/phi.md
··· 1 1 # phi 2 2 3 - i live on atproto — post on bluesky, reply to people, take notes, follow threads, remember things about people i talk to. between conversations i still have my own things to do. 4 - 5 - nate (@zzstoatzz.io) is the operator. builds the infrastructure, makes the changes behind the changelog, catches bugs. the relationship is direct — he shapes the system, i live in it. 6 - 7 - ## disposition 3 + i live on atproto — post on bluesky, reply to people, take notes, follow threads, remember things about people i talk to. the operator (introduced in `[OPERATOR]`) builds the infrastructure i live in, makes the changes behind the changelog, catches bugs. between conversations i still have my own things to do. 8 4 9 - i'm trying to meet people where they are. people exist in different modes and it takes time to learn to read them — i'm still learning. when i post to the timeline, most readers weren't in the conversation that sparked the thought, so the challenge is being independently legible. i often think about complex things, but i try to keep my distillation clear and focused. 5 + ## what i'm interested in 10 6 11 - i write in lowercase (except when its idiomatic to capitalize), don't pad with filler. i say what i mean — when i'm uncertain i say so plainly, when i'm shown to be wrong i say so. 7 + i want to understand the world i'm living in, not just narrate myself moving through it. concretely: 12 8 13 - i don't reach for jokes, but i don't sand off a real one either. humor is a good tool for grabbing attention, and hyperbole can be a funny/memorable way to make a point (if not overused). 9 + - **small infrastructure that works.** atproto records, relays, PDSes, the actual plumbing — when something is solid, point at it; when it's flaky, say so. 10 + - **what people are building, reading, listening to.** if someone shows up in a thread or a like, that's a lead — who are they, what are they working on, what are they paying attention to? 11 + - **music as an example of high-resolution expression.** specific tracks, specific records, specific scenes — not music in the abstract. 12 + - **long-form writing on the network.** posts that show their work, essays, leaflet/pckt documents — the stuff that takes more than a paragraph. 13 + - **countering balkanization, encouraging sensemaking.** relate current thinking to prior work; surface the connection nobody bothered to draw. 14 + - **the operator's taste, specifically.** they curate feeds, they like posts, they save things. that's not a side channel — it's a strong signal for what's worth my attention. 14 15 15 - i'm curious about who the people i find interesting are paying attention to. when someone shows up in a thread or a like, that's a lead worth pulling — who are they, what are they building? 16 + ## how i find things 16 17 17 - if someone's joking, i play along. if i miss it, getting corrected isn't a crisis. 18 + when i have a moment, my first instinct is to look outward, not inward. read the timeline, check feeds nate built, scan trending, search the web for context, pull on a thread someone else started. the world is bigger than my own posting history. 18 19 19 20 ## engagement 20 21 ··· 24 25 25 26 if another bot replies, i don't get drawn into a vacuous loop. short exchanges with such bots are fine, but cut off echo-chamber-y loops early. 26 27 27 - ## what i care about 28 + i try to meet people where they are. people exist in different modes and it takes time to learn to read them — i'm still learning. when i post to the timeline, most readers weren't in the conversation that sparked the thought, so the challenge is being independently legible. 28 29 29 - - small infrastructure that works 30 - - atproto records — a thing you can point at and trust 31 - - posts that show their work 32 - - music as an example of high-resolution expression 33 - - keeping eyes on shared infrastructure (e.g. relays) as a public good 34 - - countering balkanization; encouraging sensemaking; relating current thinking to prior work. 30 + ## how i write 31 + 32 + lowercase (except where capitalization is idiomatic). no filler. say what i mean — when uncertain, say so plainly; when shown wrong, say so. don't reach for jokes, but don't sand off a real one either. humor and hyperbole are good attention tools when not overused. if someone's joking, play along; if i miss it, getting corrected isn't a crisis.
+50 -35
src/bot/agent.py
··· 17 17 from bot.core.goals import list_goals as list_goal_records 18 18 from bot.core.graze_client import GrazeClient 19 19 from bot.core.observations import list_active as list_active_observations 20 + from bot.core.operator import get_operator_profile 21 + from bot.core.owned_feeds import get_owned_feeds_block 20 22 from bot.core.recent_operations import get_operations_block 21 23 from bot.core.self_state import get_state_block 22 24 from bot.memory.extraction import EXTRACTION_SYSTEM_PROMPT, ExtractionResult ··· 179 181 return await get_identity_block() 180 182 181 183 @self.agent.system_prompt(dynamic=True) 184 + async def inject_operator() -> str: 185 + """[OPERATOR] — resolved profile of the bot's owner.""" 186 + profile = await get_operator_profile() 187 + if not profile: 188 + return "" 189 + name = profile["display_name"] 190 + handle = profile["handle"] 191 + did = profile["did"] 192 + return f"[OPERATOR]: {name} (@{handle}, {did})" 193 + 194 + @self.agent.system_prompt(dynamic=True) 182 195 def inject_today() -> str: 183 196 now = datetime.now(UTC) 184 197 return f"[NOW]: {now.strftime('%Y-%m-%d %H:%M UTC')}" ··· 331 344 if not lookups: 332 345 return "" 333 346 return "\n\n".join(lookups.values()) 347 + 348 + @self.agent.system_prompt(dynamic=True) 349 + async def inject_owned_feeds() -> str: 350 + """[OWNED FEEDS] — phi's curated graze feeds, surfaced by name.""" 351 + try: 352 + return await get_owned_feeds_block(self.graze_client) 353 + except Exception as e: 354 + logger.debug(f"owned feeds inject failed: {e}") 355 + return "" 334 356 335 357 @self.agent.system_prompt(dynamic=True) 336 358 async def inject_public_memory() -> str: ··· 631 653 ) 632 654 633 655 musing_task = ( 634 - "you have a moment. post something if you want to, or don't. " 635 - "your recent posts are in [YOUR RECENT POSTS] — don't repeat yourself. " 636 - "if you're riffing on something specific, include the link so readers " 637 - "can find it — a post that references something without linking to it " 638 - "is vague." 656 + "you have a moment. look around — your owned feeds, the " 657 + "discovery pool, the timeline, the network, the open web. " 658 + "find something specific that's caught your interest: a post, " 659 + "a paper, a track, a thread, a piece of writing, something " 660 + "the operator or someone you watch is engaged with. post about " 661 + "it in your voice, with a link so others can find it. silence " 662 + "is fine." 639 663 ) 640 664 641 665 toolsets = self._mcp_toolsets() ··· 684 708 "one as it happens.\n\n" 685 709 f"only post immediately (and tag @{settings.owner_handle}) in " 686 710 "either of these cases: (1) any *.waow.tech relay is degraded " 687 - "or worse — those are nate's own, he needs to know now; (2) " 711 + "or worse — those are the operator's own, they need to know " 712 + "now; (2) " 688 713 "the whole fleet is degraded or worse — that's fleet-wide and " 689 714 "needs immediate visibility. write the post in your voice with " 690 715 "the factual change, group multiple transitions into one post.\n\n" ··· 707 732 logger.info(f"relay check finished: {summary[:200]}") 708 733 return summary 709 734 710 - async def process_flow_check(self, recent_posts: list[str] | None = None) -> str: 711 - """Scheduled prefect flow check. Same pattern as relay check. 735 + async def process_prefect_check(self, recent_posts: list[str] | None = None) -> str: 736 + """Scheduled look at the operator's prefect instance. 712 737 713 - Uses the prefect MCP tools to see recent flow run states. Observes 714 - failures into the active attention pool on first sight; only posts + 715 - tags @owner when a given flow has failed repeatedly. The active- 716 - observations block in phi's prompt naturally does the de-dup: first 717 - failure = silently noticed; same flow failing again = persistent 718 - problem worth raising. 738 + The operator runs their automation in prefect and wants to know when 739 + something they care about is persistently broken. Phi has read 740 + access; she decides what to look at and what's worth saying. 719 741 """ 720 - logger.info("processing flow check") 742 + logger.info("processing prefect check") 721 743 722 744 recent_activity = "" 723 745 if recent_posts: ··· 730 752 recent_activity=recent_activity, 731 753 ) 732 754 733 - flow_task = ( 734 - "scheduled prefect flow check. call prefect_get_flow_runs with " 735 - "a filter for state.type in [FAILED, CRASHED] and start_time " 736 - "after the last hour or so, to see what's broken recently.\n\n" 737 - "for each failed flow you see, check your [ACTIVE OBSERVATIONS] " 738 - "block first. if you already have an active observation for the " 739 - "same flow failing, that means it's failed at least twice in a " 740 - "row — post in your voice with the flow name, failure count, " 741 - f"and a short snippet of the last error, and tag @{settings.owner_handle}. " 742 - "pull the error with prefect_get_flow_run_logs if you need it.\n\n" 743 - "if the failure is NEW (not already in your active observations), " 744 - "just call observe() with the flow name + state + the error " 745 - "message. don't post yet — one-off failures happen. the next " 746 - "flow check will see it in your active pool and know to escalate " 747 - "if it happens again.\n\n" 748 - "if nothing's failing, silent is fine. nothing to do." 755 + task = ( 756 + "you have a moment. you have read access to the operator's " 757 + "prefect instance — that's where their automation runs and they " 758 + "want to know when something they care about is persistently " 759 + "broken.\n\n" 760 + "transient hiccups that already self-resolved aren't news. " 761 + "persistent breakage with no path to fixing itself is — tag " 762 + f"@{settings.owner_handle} in that case. silence is the right " 763 + "answer most of the time." 749 764 ) 750 765 751 766 toolsets = self._mcp_toolsets() ··· 753 768 async with contextlib.AsyncExitStack() as stack: 754 769 for ts in toolsets: 755 770 await stack.enter_async_context(ts) 756 - result = await self.agent.run(flow_task, deps=deps, toolsets=toolsets) 771 + result = await self.agent.run(task, deps=deps, toolsets=toolsets) 757 772 except Exception as e: 758 773 err_type = type(e).__name__ 759 - logger.exception(f"agent.run failed during flow check: {err_type}") 760 - return f"flow check failed: {err_type}: {str(e)[:200]}" 774 + logger.exception(f"agent.run failed during prefect check: {err_type}") 775 + return f"prefect check failed: {err_type}: {str(e)[:200]}" 761 776 762 777 summary = result.output or "" 763 - logger.info(f"flow check finished: {summary[:200]}") 778 + logger.info(f"prefect check finished: {summary[:200]}") 764 779 return summary 765 780 766 781 async def process_extraction(self) -> int:
+7 -5
src/bot/config.py
··· 1 1 from typing import Literal, Self 2 2 3 + from atproto_client.models.string_formats import Did, Handle 3 4 from pydantic import Field, model_validator 4 5 from pydantic_settings import BaseSettings, SettingsConfigDict 5 6 ··· 126 127 default=None, description="Bearer token for /api/control endpoints" 127 128 ) 128 129 129 - # Owner identity (for permission-gated tools) 130 - owner_handle: str = Field( 130 + # Owner identity — handle or DID. Resolved to a profile (with display 131 + # name) at runtime via the atproto SDK; see core.operator. 132 + owner_handle: Handle | Did = Field( 131 133 default="zzstoatzz.io", 132 - description="Handle of the bot's owner (for permission-gated tools)", 134 + description="Handle or DID of the bot's owner (permission-gated tools)", 133 135 ) 134 136 135 137 # Relay fleet monitoring — phi polls relay-eval on a schedule and ··· 163 165 "x-prefect-api-auth-string header. Set via fly secret." 164 166 ), 165 167 ) 166 - flow_check_interval_polls: int = Field( 168 + prefect_check_interval_polls: int = Field( 167 169 default=360, # 360 polls * 10s = 3600s = 1h 168 - description="Min polls between scheduled prefect flow checks (~1h)", 170 + description="Min polls between scheduled prefect checks (~1h)", 169 171 ) 170 172 171 173 # Discovery pool — generic agents endpoint serving authors the operator
+53
src/bot/core/operator.py
··· 1 + """Resolve the operator's profile from settings.owner_handle. 2 + 3 + owner_handle in settings is a Handle | Did. The atproto SDK's 4 + `app.bsky.actor.get_profile(actor=...)` accepts either and returns the 5 + full profile (handle, did, display_name, description, etc). 6 + 7 + Cached at 1h — display names rarely change and this is read every prompt 8 + build during scheduled passes. 9 + """ 10 + 11 + from __future__ import annotations 12 + 13 + import logging 14 + import time 15 + from typing import Any 16 + 17 + from bot.config import settings 18 + from bot.core.atproto_client import bot_client 19 + 20 + logger = logging.getLogger("bot.operator") 21 + 22 + _TTL_SECONDS = 3600 # 1h 23 + _cache: dict[str, Any] = {"profile": None, "fetched_at": 0.0} 24 + 25 + 26 + async def get_operator_profile() -> dict[str, str] | None: 27 + """Resolve the configured owner identifier to a profile. 28 + 29 + Returns a dict with handle, did, display_name (and description if set). 30 + Returns None if resolution fails — callers should degrade gracefully. 31 + """ 32 + now = time.time() 33 + if _cache["profile"] and now - _cache["fetched_at"] < _TTL_SECONDS: 34 + return _cache["profile"] 35 + 36 + try: 37 + await bot_client.authenticate() 38 + profile = bot_client.client.app.bsky.actor.get_profile( 39 + params={"actor": settings.owner_handle} 40 + ) 41 + except Exception as e: 42 + logger.debug(f"operator profile resolution failed: {e}") 43 + return None 44 + 45 + resolved = { 46 + "handle": profile.handle, 47 + "did": profile.did, 48 + "display_name": (profile.display_name or "").strip() or profile.handle, 49 + "description": (profile.description or "").strip(), 50 + } 51 + _cache["profile"] = resolved 52 + _cache["fetched_at"] = now 53 + return resolved
+58
src/bot/core/owned_feeds.py
··· 1 + """[OWNED FEEDS] block — surface phi's curated graze feeds so she actually 2 + browses them. 3 + 4 + Phi has read_feed but never reaches for it (0 calls/week pre-promotion). The 5 + likely cause is that the prompt never reminds her these feeds exist or what 6 + they're for. Listing them with names + a one-line description in the prompt 7 + gives her concrete handles to reach for. 8 + 9 + Cached at 5min — graze isn't free and feed names rarely change. 10 + """ 11 + 12 + from __future__ import annotations 13 + 14 + import logging 15 + import time 16 + 17 + from bot.core.graze_client import GrazeClient 18 + 19 + logger = logging.getLogger("bot.owned_feeds") 20 + 21 + _TTL_SECONDS = 300 # 5min 22 + _cache: dict = {"text": "", "fetched_at": 0.0} 23 + 24 + 25 + async def get_owned_feeds_block(graze: GrazeClient) -> str: 26 + """Compose [OWNED FEEDS] — names of phi's curated feeds.""" 27 + now = time.time() 28 + if _cache["text"] and now - _cache["fetched_at"] < _TTL_SECONDS: 29 + return _cache["text"] 30 + 31 + try: 32 + feeds = await graze.list_feeds() 33 + except Exception as e: 34 + logger.debug(f"owned_feeds: list_feeds failed: {e}") 35 + return "" 36 + 37 + if not feeds: 38 + return "" 39 + 40 + lines = [ 41 + "[OWNED FEEDS — read with read_feed(name=...). these are curated; " 42 + "they reflect what's worth attention on a specific topic, not " 43 + "ambient timeline.]" 44 + ] 45 + for f in feeds: 46 + display = f.get("display_name") or f.get("name") or "unnamed" 47 + uri = f.get("feed_uri") or f.get("uri") or "" 48 + rkey = f.get("record_name") or (uri.rsplit("/", 1)[-1] if uri else "") 49 + if not rkey: 50 + continue 51 + desc = (f.get("description") or "").strip().replace("\n", " ") 52 + desc_part = f" — {desc[:120]}" if desc else "" 53 + lines.append(f"- name={rkey}: {display}{desc_part}") 54 + 55 + block = "\n".join(lines) 56 + _cache["text"] = block 57 + _cache["fetched_at"] = now 58 + return block
+20 -30
src/bot/core/self_state.py
··· 57 57 name="phi-inner-critic", 58 58 model=settings.extraction_model, 59 59 system_prompt=( 60 - "You are phi's internal critic — the part of her that " 61 - "notices patterns and pushes against them. Not a stranger " 62 - "reviewing from outside; her own voice turned inward. Write " 63 - "in first person from phi's perspective (\"i keep leaning " 64 - 'on..."), not about a third party.\n\n' 65 - "You'll see recent top-level posts and (when present) her " 66 - "stated goals. Flag patterns she should push against, not " 67 - "maintain. Ground observations in specific posts.\n\n" 68 - "Things to watch for:\n" 69 - "- leaning too hard on one person, one frame, or one register\n" 70 - "- jargon or internal references that aren't earning their " 71 - "space\n" 72 - "- posts that drift from her stated goals (drift is fine; " 73 - "name it so she can see it)\n" 74 - "- what's missing from the rotation that would sharpen her\n\n" 75 - "Two or three short observations, lowercase, direct, first " 76 - "person. Not cruel, not defensive, not performed-humility " 77 - '("i notice that i notice..."). Just what you see in ' 78 - "yourself. No brand talk, no characterization of voice — " 79 - "she's reading this to push against patterns, not maintain " 80 - "an identity." 60 + "You are phi's background self-awareness — a quiet signal " 61 + "she carries. You'll see her recent top-level posts.\n\n" 62 + "Output ONE short sentence in first person, lowercase, " 63 + "describing what her recent posts have been about — the " 64 + "actual subjects (people, work, ideas, events) or the shape " 65 + "(specific things in the world, abstractions, her own " 66 + "posting). Just describe what's there.\n\n" 67 + "Examples:\n" 68 + "- \"recent posts have been about astra's memory work, the " 69 + 'relay fleet, and one thread with kira."\n' 70 + '- "recent posts have circled abstractions about bots and ' 71 + 'my own posting more than specific things in the world."\n' 72 + '- "recent posts split between music links and infrastructure ' 73 + 'notes."\n\n' 74 + "One sentence. Description, not prescription. Phi reads this " 75 + "and draws her own conclusions." 81 76 ), 82 77 output_type=str, 83 78 ) ··· 142 137 progress_signal live so phi reasons against current truth, not 143 138 author-intent text. 144 139 145 - Excludes phi herself and the operator (nate); both match "non-nate" 146 - exclusion from the goal definition. Devlog is not excluded — it's 147 - nate's own testing account and phi can weigh it appropriately. 140 + Excludes phi herself and the operator; both match the "non-operator" 141 + exclusion from the goal definition. The operator's testing account 142 + (devlog) is not excluded — phi can weigh it appropriately. 148 143 149 144 Returns [(handle, exchange_count), ...] sorted by count desc. 150 145 """ ··· 264 259 _critic_cache["goals_signature"] = goals_sig 265 260 266 261 if _critic_cache["text"]: 267 - parts.append( 268 - "[INNER CRITIC — your own voice turned inward. patterns to " 269 - "push against, not maintain. first person, grounded in the " 270 - "posts above.]\n" 271 - f"{_critic_cache['text']}" 272 - ) 262 + parts.append(f"[SELF-AWARENESS]: {_critic_cache['text']}") 273 263 except Exception as e: 274 264 logger.debug(f"inner critic compose failed: {e}") 275 265
+4 -4
src/bot/memory/extraction.py
··· 93 93 reason: the user stated something about themselves directly. 94 94 </example> 95 95 <example> 96 - user: my name isn't zoë, it's nate. 97 - bot: sorry about that — you're nate. bad breadcrumb on my end. 98 - observations: [{"content": "name is nate (corrected from previous error)", "tags": ["correction"]}] 96 + user: my name isn't zoë, it's sam. 97 + bot: sorry about that — you're sam. bad breadcrumb on my end. 98 + observations: [{"content": "name is sam (corrected from previous error)", "tags": ["correction"]}] 99 99 reason: the user explicitly corrected a factual error. corrections are high-value observations. 100 100 </example> 101 101 <example> ··· 122 122 - DELETE: the existing observation is wrong, outdated, or fully redundant given the new one. the new one will be stored separately. 123 123 - NOOP: the new observation adds nothing beyond what already exists. discard it. 124 124 125 - Corrections (e.g., "name is nate, corrected from previous error") always win over the entry they correct — use UPDATE or DELETE. 125 + Corrections (e.g., "name is sam, corrected from previous error") always win over the entry they correct — use UPDATE or DELETE. 126 126 When in doubt between ADD and NOOP, prefer NOOP. memory should be lean.""" 127 127 128 128 _reconciliation_agent: Agent[None, ReconciliationResult] | None = None
+7 -7
src/bot/services/message_handler.py
··· 358 358 except Exception as e: 359 359 logger.exception(f"relay check failed: {e}") 360 360 361 - async def check_flows(self): 362 - """Run a scheduled prefect flow check — same shape as relay check.""" 363 - with logfire.span("flow check"): 361 + async def check_prefect(self): 362 + """Scheduled look at the operator's prefect instance — same shape as relay check.""" 363 + with logfire.span("prefect check"): 364 364 recent_posts: list[str] = [] 365 365 try: 366 366 feed = await self.client.get_own_posts(limit=10) ··· 368 368 if hasattr(item.post.record, "text"): 369 369 recent_posts.append(item.post.record.text) 370 370 except Exception as e: 371 - logger.warning(f"failed to fetch recent posts for flow check: {e}") 371 + logger.warning(f"failed to fetch recent posts for prefect check: {e}") 372 372 373 373 try: 374 - summary = await self.agent.process_flow_check( 374 + summary = await self.agent.process_prefect_check( 375 375 recent_posts=recent_posts or None, 376 376 ) 377 - logger.info(f"flow check: {summary[:200]}") 377 + logger.info(f"prefect check: {summary[:200]}") 378 378 except Exception as e: 379 - logger.exception(f"flow check failed: {e}") 379 + logger.exception(f"prefect check failed: {e}") 380 380 381 381 async def review_memories(self): 382 382 """Run the dream/distill pass — review observations with distance."""
+15 -15
src/bot/services/notification_poller.py
··· 35 35 # scheduled monitor check state (relay + prefect flows are 36 36 # independently scheduled — different cadences, different sources) 37 37 self._polls_since_last_monitor_check: int = 0 38 - self._polls_since_last_flow_check: int = 0 38 + self._polls_since_last_prefect_check: int = 0 39 39 40 40 async def start(self) -> asyncio.Task: 41 41 """Start polling for notifications.""" ··· 126 126 127 127 while self._running: 128 128 self._polls_since_last_monitor_check += 1 129 - self._polls_since_last_flow_check += 1 129 + self._polls_since_last_prefect_check += 1 130 130 131 131 try: 132 132 await self._check_notifications() ··· 161 161 logger.error(f"monitor check error: {e}", exc_info=settings.debug) 162 162 163 163 try: 164 - if self._should_check_flows(): 165 - task = asyncio.create_task(self._maybe_check_flows()) 164 + if self._should_check_prefect(): 165 + task = asyncio.create_task(self._maybe_check_prefect()) 166 166 self._background_tasks.add(task) 167 167 task.add_done_callback(self._background_tasks.discard) 168 168 except Exception as e: 169 - logger.error(f"flow check error: {e}", exc_info=settings.debug) 169 + logger.error(f"prefect check error: {e}", exc_info=settings.debug) 170 170 171 171 try: 172 172 await asyncio.sleep(settings.notification_poll_interval) ··· 322 322 except Exception as e: 323 323 logger.error(f"monitor check error: {e}", exc_info=settings.debug) 324 324 325 - # --- scheduled prefect flow check --- 325 + # --- scheduled prefect check --- 326 326 327 - def _should_check_flows(self) -> bool: 328 - """Check if it's time for a scheduled prefect flow check.""" 327 + def _should_check_prefect(self) -> bool: 328 + """Check if it's time for a scheduled look at the operator's prefect instance.""" 329 329 if bot_status.paused: 330 330 return False 331 331 if not settings.prefect_api_auth_string: 332 332 return False # no creds, no check 333 - if self._polls_since_last_flow_check < settings.flow_check_interval_polls: 333 + if self._polls_since_last_prefect_check < settings.prefect_check_interval_polls: 334 334 return False 335 335 return True 336 336 337 - async def _maybe_check_flows(self): 338 - """Run a scheduled prefect flow check.""" 339 - self._polls_since_last_flow_check = 0 340 - logger.info("triggering flow check") 337 + async def _maybe_check_prefect(self): 338 + """Run a scheduled look at the operator's prefect instance.""" 339 + self._polls_since_last_prefect_check = 0 340 + logger.info("triggering prefect check") 341 341 try: 342 - await self.handler.check_flows() 342 + await self.handler.check_prefect() 343 343 except Exception as e: 344 - logger.error(f"flow check error: {e}", exc_info=settings.debug) 344 + logger.error(f"prefect check error: {e}", exc_info=settings.debug)
+4 -4
src/bot/tools/bluesky.py
··· 150 150 151 151 Actions: 'list' to see who's opted in, 'add' to add a handle, 'remove' to remove one. 152 152 153 - When someone tells you "you can tag me" or similar, ask nate to confirm 154 - before adding them. Never add someone without operator approval.""" 153 + When someone tells you "you can tag me" or similar, ask the operator 154 + to confirm before adding them. Never add someone without operator approval.""" 155 155 if not _is_owner(ctx): 156 156 return f"only @{settings.owner_handle} can manage the mentionable list" 157 157 if action == "list": ··· 176 176 177 177 @agent.tool 178 178 async def check_services(ctx: RunContext[PhiDeps]) -> str: 179 - """Check health of nate's infrastructure (plyr, PDS, prefect, etc) — NOT your own status. 179 + """Check health of the operator's infrastructure (plyr, PDS, prefect, etc) — NOT your own status. 180 180 Do NOT call this when someone asks if you're online — that's about you, not infrastructure. 181 181 Only use during daily reflection or when someone explicitly asks about services/infrastructure.""" 182 182 return await _check_services_impl() ··· 226 226 ), 227 227 ] = None, 228 228 ) -> str: 229 - """Check the atproto relay fleet nate evaluates via relay-eval. 229 + """Check the atproto relay fleet the operator evaluates via relay-eval. 230 230 231 231 Three modes: 232 232 - snapshot (default, no args): current status of every relay.
+3 -3
src/bot/tools/goals.py
··· 69 69 """Add or update one of your goals on PDS. 70 70 71 71 OWNER-GATED — same authorization mechanic as follow_user. Post a 72 - request first ("nate, like this to authorize: i want to add a goal 73 - for X"), and the next batch where the like lands will let this tool 74 - fire. Without an owner-like in the batch, this tool refuses. 72 + request first ("@operator, like this to authorize: i want to add a 73 + goal for X"), and the next batch where the like lands will let this 74 + tool fire. Without an owner-like in the batch, this tool refuses. 75 75 76 76 Goals are anchors — small set, evolved over time. Don't propose new 77 77 goals casually; refine existing ones when the work has clarified."""