#!/usr/bin/env python3 """ atlas_self_prompting_loop.py - Overnight autonomy. Runs every 15 min via schtask (Atlas-Autonomy-Hourly): 1. Read state (atlas-finance.json, atlas-strategy.json, atlas-activity.json, recent vault changes) 2. Pick next task from queue OR generate new task if queue empty 3. Dispatch to cluster (atlas-01 + MET + PC) via cluster_then_polish 4. Save output to vault under 00_Atlas_Wiki/_autonomy/_.md 5. Maintain a "what happened tonight" log readable by operator in morning Generated tasks pull from a recurring backlog + state-driven prompts. NO external messages, NO code modifications, NO destructive ops — pure note generation + analysis + plan refinement. """ import sys import os import json import time import random import urllib.request from pathlib import Path from datetime import datetime, date sys.path.insert(0, r"C:\Users\Gebruiker\.config\atlas") import io if sys.stdout.encoding != 'utf-8': sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace') try: from cluster_then_polish import draft, save_draft import cluster except ImportError as e: print(f"missing dep: {e}") sys.exit(1) VAULT_AUTONOMY = Path(r"C:\Users\Gebruiker\MEGA\Atlas Corporation\00_Atlas_Wiki\_autonomy") QUEUE_FILE = Path(r"C:\Users\Gebruiker\.config\atlas\autonomy_queue.json") LOG_FILE = Path(r"C:\Users\Gebruiker\.config\atlas\autonomy_loop.log") STATE_URLS = { "finance": "http://atlas-01/atlas-finance.json", "strategy": "http://atlas-01/atlas-strategy.json", "activity": "http://atlas-01/atlas-activity.json", "priorities": "http://atlas-01/priorities.json", } MAX_RUNTIME_SECONDS = 600 # 10 min hard cap per fire — never block schtask MIN_QUEUE_SIZE = 5 # Recurring task templates — picked when queue is low RECURRING_TEMPLATES = [ { "topic": "client_deepdive", "rotation": "random_client", # picks from client list "prompt_template": "Schrijf een korte client-summary voor {client}: hun sector, laatste touchpoint, recurring revenue potential, 1 concrete actie deze week. NL, 100 woorden max.", "task": "general", "node": "atlas-01", }, { "topic": "season_action", "prompt_template": "Gegeven huidige seasonal window {window} (vakantiegeld/kermis/etc.), wat is 1 specifieke actie voor Atlas Agency deze week die geen ander cluster ook al heeft? Schrijf in NL, 150 woorden.", "task": "reason", "node": "atlas-01", }, { "topic": "vault_synthesis", "prompt_template": "Vat de 3 belangrijkste lessons uit Atlas Q1 2026 in 100 woorden NL. Bron: YTD revenue €4.578, Bisogno 57%, mei €0 ingebrekening.", "task": "general", "node": "atlas-01", }, { "topic": "anti_impulse_check", "prompt_template": "Gegeven decision rules R1-R10 (anti-impulse hardware buy + cash-before-code), schrijf 1 paragraaf 'wat ik vannacht zou doen als ik in deze valkuil viel' (om operator te helpen herkennen). NL, 100 woorden.", "task": "general", "node": "atlas-01", }, { "topic": "outreach_personalization", "rotation": "random_prospect", "prompt_template": "Schrijf 1 zin alternative opening voor cold WhatsApp aan {prospect} ({sector}) die anders is dan een template. NL, max 25 woorden, hook moet specifiek aan zaak-context aansluiten.", "task": "chat", "node": "pc", # PC heeft hermes3:8b voor chat-tone }, { "topic": "weekly_metrics", "prompt_template": "Gegeven YTD €{ytd}, MRR €{mrr}, concentration {conc}%, R10 status {r10}, wat is 1 KPI die niet maar in de dashboard zit en wel moet? NL, 80 woorden.", "task": "reason", "node": "atlas-01", }, { "topic": "scale_path", "prompt_template": "Atlas Agency target 1200 sites. Huidige: 1 (Charif). Wat is concrete week-22 actie die schaalbaarheid voorbereidt zonder grote build? NL, 150 woorden.", "task": "reason", "node": "atlas-01", }, { "topic": "vault_health_check", "prompt_template": "Atlas vault heeft 7000+ MD files. Welke 3 hygiene-acties zouden vault zoekbaarder maken zonder content te verliezen? NL, 100 woorden.", "task": "general", "node": "met", }, ] # Known prospects (for rotation) PROSPECTS = [ {"name": "Lunchroom Carrousel", "sector": "lunchroom"}, {"name": "June Tilburg", "sector": "koffiebar"}, {"name": "Buutvrij", "sector": "lunchroom"}, {"name": "Cafetaria Online", "sector": "cafetaria"}, {"name": "Cafetaria Franske", "sector": "cafetaria"}, {"name": "Kapper Muro", "sector": "kapper"}, {"name": "The Crooks Barbershop", "sector": "barber"}, {"name": "De Meesterbarbier", "sector": "kapper"}, {"name": "Fresh Up Barbershop", "sector": "barber"}, {"name": "Picasso Barbers", "sector": "kapper"}, ] KNOWN_CLIENTS = [ "Koffie Bisogno", "Cargo Nexus", "HK International Care", "Edessa Management", "Akmoh IT", "Ozside V.O.F.", "Dushi Sazon", "Ayintap Food", "GobYtes B.V.", "TRWC B.V.", "Coffee Almahaba", "WinstUITstroom", "Couscousbar", "Kolvari", "Le Miris", "Sweet Mahaba", ] def log(msg): LOG_FILE.parent.mkdir(parents=True, exist_ok=True) with open(LOG_FILE, "a", encoding="utf-8") as f: f.write(f"[{datetime.now().isoformat(timespec='seconds')}] {msg}\n") def fetch_state(url, timeout=4): try: with urllib.request.urlopen(url, timeout=timeout) as r: return json.loads(r.read().decode("utf-8", errors="replace")) except Exception: return None def load_queue(): if QUEUE_FILE.exists(): try: return json.loads(QUEUE_FILE.read_text(encoding="utf-8")) except: pass return {"pending": [], "completed": []} def save_queue(q): QUEUE_FILE.parent.mkdir(parents=True, exist_ok=True) QUEUE_FILE.write_text(json.dumps(q, indent=2, ensure_ascii=False), encoding="utf-8") def generate_task(state): """Pick a template + fill in dynamic vars.""" tpl = random.choice(RECURRING_TEMPLATES) fin = state.get("finance", {}) or {} strat = state.get("strategy", {}) or {} act = state.get("activity", {}) or {} vars_ctx = { "client": random.choice(KNOWN_CLIENTS), "ytd": fin.get("ytd_eur", 0), "mrr": fin.get("mrr_estimate_eur", 0), "conc": fin.get("concentration_top_pct", 0), "r10": "PASS" if act.get("r10_pass") else "FAIL", "window": (strat.get("current_window") or {}).get("name", "default"), } if tpl.get("rotation") == "random_prospect": p = random.choice(PROSPECTS) vars_ctx["prospect"] = p["name"] vars_ctx["sector"] = p["sector"] try: prompt = tpl["prompt_template"].format(**vars_ctx) except KeyError as e: log(f"template format fail {tpl['topic']}: {e}") return None return { "topic": tpl["topic"], "prompt": prompt, "task": tpl["task"], "node": tpl["node"], "context": vars_ctx, "queued_at": datetime.now().isoformat(timespec="seconds"), } def refill_queue_if_low(q, state): if len(q["pending"]) >= MIN_QUEUE_SIZE: return needed = MIN_QUEUE_SIZE * 2 - len(q["pending"]) for _ in range(needed): t = generate_task(state) if t: q["pending"].append(t) log(f"refilled queue: now {len(q['pending'])} pending") def execute_one(task): """Dispatch task to cluster via cluster_then_polish.draft().""" t0 = time.time() try: result = draft( task["prompt"], task=task.get("task", "general"), node_pref=task.get("node"), max_tokens=500, temperature=0.6, purpose=f"autonomy_{task['topic']}", ) elapsed = round(time.time() - t0, 1) if not result.get("ok"): return {"ok": False, "err": result.get("err"), "elapsed": elapsed} return {"ok": True, "text": result["text"], "model": result["model"], "node": result["node"], "elapsed": elapsed, "draft_id": result.get("draft_id")} except Exception as e: return {"ok": False, "err": str(e)[:200], "elapsed": round(time.time() - t0, 1)} def save_output_to_vault(task, result): """Write to 00_Atlas_Wiki/_autonomy/_.md""" VAULT_AUTONOMY.mkdir(parents=True, exist_ok=True) ts = datetime.now().strftime("%Y-%m-%d_%H%M") safe_topic = task["topic"].replace("/", "_")[:40] fname = f"{ts}_{safe_topic}.md" p = VAULT_AUTONOMY / fname body = f"""--- generated_at: {datetime.now().isoformat(timespec='seconds')} topic: {task['topic']} node: {result.get('node', '?')} model: {result.get('model', '?')} elapsed_s: {result.get('elapsed', 0)} draft_id: {result.get('draft_id', '?')} polish_status: pending tags: [autonomy, generated, {task['topic']}] context: {json.dumps(task.get('context', {}), ensure_ascii=False)} --- # {task['topic'].replace('_', ' ').title()} > [!note] Autonomous output — review needed before any external use > Generated by Atlas overnight autonomy loop. Cluster output, not Claude-polished. **Prompt**: {task['prompt']} --- {result.get('text', '(no output)')} --- *Source: atlas_self_prompting_loop.py · [[06.5 - Master Cockpit & 90-day System]]* """ p.write_text(body, encoding="utf-8") return p def main(): log("=== loop fire ===") start = time.time() # Fetch live state state = {k: fetch_state(v) for k, v in STATE_URLS.items()} log(f"state fetched: { {k: bool(v) for k, v in state.items()} }") # Verify cluster has at least one node nodes_alive = cluster.nodes_status(force=False) alive = [n for n, info in nodes_alive.items() if info.get("reachable")] log(f"alive nodes: {alive}") if not alive: log("NO CLUSTER NODES ALIVE — abort this fire") return # Queue management q = load_queue() refill_queue_if_low(q, state) # Execute up to 2 tasks per fire (rate-limit cluster + save energy) executed = 0 max_per_fire = 2 while executed < max_per_fire and q["pending"]: if time.time() - start > MAX_RUNTIME_SECONDS: log("hit runtime cap") break task = q["pending"].pop(0) log(f"executing: {task['topic']} ({task['node']}/{task['task']})") result = execute_one(task) if result["ok"]: saved = save_output_to_vault(task, result) q["completed"].append({ **task, "completed_at": datetime.now().isoformat(timespec="seconds"), "model": result["model"], "node": result["node"], "elapsed_s": result["elapsed"], "file": str(saved), }) log(f" OK {result['node']}/{result['model']} {result['elapsed']}s → {saved.name}") else: q["completed"].append({ **task, "completed_at": datetime.now().isoformat(timespec="seconds"), "err": result["err"], }) log(f" FAIL: {result['err']}") executed += 1 save_queue(q) # Keep only last 200 completed entries in queue file (rolling log) if len(q["completed"]) > 200: q["completed"] = q["completed"][-200:] save_queue(q) log(f"=== loop end: executed {executed} ===") if __name__ == "__main__": main()