diff --git a/START_HERE.md b/START_HERE.md index d91d145..6acc73c 100644 --- a/START_HERE.md +++ b/START_HERE.md @@ -12,7 +12,7 @@ That is the same as: ./scripts/ratchet next ``` -It prints the cockpit: current state, exact next action, interpretation, safe commands, and ratchet rule. +It prints the cockpit: current state, exact next action, interpretation, reference commands, and ratchet rule. ## Your Job @@ -21,8 +21,8 @@ Your job is not to understand every metric. Your job is: 1. Run `./scripts/ratchet`. -2. Do the first item under `What You Do Now`. -3. If a watch is running, leave it alone until it finishes. +2. Do only what it says under `DO THIS NOW`. +3. Ignore every other command unless `DO THIS NOW` tells you to run it. 4. After a watch finishes, run `./scripts/ratchet` again. 5. If you manually place a Braiins canary, write down the order details outside this repo and wait through the maturity window before judging it. diff --git a/docs/CLI_REFERENCE.md b/docs/CLI_REFERENCE.md index c5a18a6..e94eed7 100644 --- a/docs/CLI_REFERENCE.md +++ b/docs/CLI_REFERENCE.md @@ -23,7 +23,7 @@ PYTHONPATH=src ./.venv/bin/python -m braiins_ratchet.cli ## `next` -Prints the cockpit: current state, exact next operator action, interpretation, and safe commands. +Prints the cockpit: current state, exact next operator action, interpretation, and reference commands. ```bash PYTHONPATH=src ./.venv/bin/python -m braiins_ratchet.cli next diff --git a/src/braiins_ratchet/experiments.py b/src/braiins_ratchet/experiments.py index 50092e3..e617b1e 100644 --- a/src/braiins_ratchet/experiments.py +++ b/src/braiins_ratchet/experiments.py @@ -3,6 +3,8 @@ from __future__ import annotations from dataclasses import dataclass from datetime import UTC, datetime from decimal import Decimal +import json +import os from pathlib import Path import uuid @@ -12,6 +14,7 @@ from .report import build_text_report REPORTS_DIR = REPO_ROOT / "reports" EXPERIMENT_LOG = REPORTS_DIR / "EXPERIMENT_LOG.md" +ACTIVE_WATCH = REPORTS_DIR / "ACTIVE_WATCH.json" @dataclass(frozen=True) @@ -60,6 +63,19 @@ def start_experiment(planned_cycles: int, interval_seconds: int, hypothesis: str "- plan: collect public Braiins depth, collect OCEAN state, compute shadow canary, store every proposal.\n" "- operator_action: none by default; manual action only if report later says manual_canary or manual_bid and operator agrees.\n" ) + ACTIVE_WATCH.write_text( + json.dumps( + { + "pid": os.getpid(), + "run_id": run_id, + "started_utc": started, + "planned_cycles": planned_cycles, + "interval_seconds": interval_seconds, + }, + indent=2, + ), + encoding="utf-8", + ) return ExperimentRun(run_id=run_id, started_utc=started) @@ -87,6 +103,7 @@ def finish_experiment( report_path.write_text(_render_run_report(summary, text_report), encoding="utf-8") with EXPERIMENT_LOG.open("a", encoding="utf-8") as handle: handle.write(_render_log_completion(summary, report_path, status)) + _clear_active_watch(run_id) return str(report_path.relative_to(REPO_ROOT)) @@ -242,6 +259,18 @@ def _ensure_log() -> None: ) +def _clear_active_watch(run_id: str) -> None: + if not ACTIVE_WATCH.exists(): + return + try: + payload = json.loads(ACTIVE_WATCH.read_text(encoding="utf-8")) + except json.JSONDecodeError: + ACTIVE_WATCH.unlink() + return + if payload.get("run_id") == run_id: + ACTIVE_WATCH.unlink() + + def _default_hypothesis() -> str: return ( "Depth-aware fillable price plus a small overpay cushion is a better canary trigger " diff --git a/src/braiins_ratchet/guidance.py b/src/braiins_ratchet/guidance.py index f09cbf7..ecafbd4 100644 --- a/src/braiins_ratchet/guidance.py +++ b/src/braiins_ratchet/guidance.py @@ -1,9 +1,12 @@ from __future__ import annotations from datetime import UTC, datetime +import json +import os from pathlib import Path +import subprocess -from .experiments import EXPERIMENT_LOG, REPORTS_DIR +from .experiments import ACTIVE_WATCH, EXPERIMENT_LOG, REPORTS_DIR from .storage import latest_market_snapshot, latest_ocean_snapshot, latest_proposal @@ -13,6 +16,7 @@ def build_operator_cockpit(conn) -> str: proposal = latest_proposal(conn) latest_report = _latest_report() running_runs = _running_runs() + active_watch = _active_watch() freshness = _freshness_minutes(market.timestamp_utc if market else None) is_fresh = freshness is not None and freshness <= 30 @@ -27,14 +31,16 @@ def build_operator_cockpit(conn) -> str: f" Latest strategy action: {proposal.action if proposal else 'none'}", f" Latest run report: {latest_report or 'none yet'}", f" Experiment ledger: {EXPERIMENT_LOG.relative_to(REPORTS_DIR.parent) if EXPERIMENT_LOG.exists() else 'none yet'}", + f" Active watch: {active_watch or 'none detected'}", ] if running_runs: lines.append(f" Ledger has unfinished run markers: {', '.join(running_runs)}") - lines.extend(["", "What You Do Now"]) + lines.extend(["", "DO THIS NOW"]) lines.extend( - _next_steps( + _do_this_now( + active_watch=active_watch, has_ocean=ocean is not None, has_market=market is not None, is_fresh=is_fresh, @@ -51,9 +57,10 @@ def build_operator_cockpit(conn) -> str: " Do not increase spend until multiple mature runs point in the same direction.", ] ) - lines.extend(["", "Safe Commands"]) + lines.extend(["", "Reference Commands"]) lines.extend( [ + " Ignore this list unless DO THIS NOW explicitly tells you to use one.", " ./scripts/ratchet next # read this cockpit", " ./scripts/ratchet once # fetch one fresh sample and report", " ./scripts/ratchet watch 2 # run a bounded 2-hour experiment", @@ -64,45 +71,56 @@ def build_operator_cockpit(conn) -> str: return "\n".join(lines) -def _next_steps(has_ocean: bool, has_market: bool, is_fresh: bool, action: str | None) -> list[str]: +def _do_this_now( + active_watch: str | None, + has_ocean: bool, + has_market: bool, + is_fresh: bool, + action: str | None, +) -> list[str]: + if active_watch: + return [ + " WAIT.", + " A watch is already running. Do not start another command.", + " After the watch terminal finishes by itself, run exactly:", + " ./scripts/ratchet", + ] + if not has_ocean or not has_market: return [ - " 1. Run: ./scripts/ratchet setup", - " 2. Run: ./scripts/ratchet once", - " 3. Then run: ./scripts/ratchet next", - " Reason: the cockpit needs at least one OCEAN sample and one Braiins sample.", + " Run exactly:", + " ./scripts/ratchet setup", + " Reason: local setup is missing. The setup command will print the next command.", ] if not is_fresh: return [ - " 1. If a watch is currently running in another terminal, do nothing until it finishes.", - " 2. If no watch is running, run: ./scripts/ratchet once", - " 3. Then run: ./scripts/ratchet next", - " Reason: the latest Braiins sample is stale; do not interpret old price action as a current signal.", + " Run exactly:", + " ./scripts/ratchet once", + " Reason: the latest Braiins sample is stale. The once command will fetch a fresh sample and print this cockpit again.", ] if action == "manual_bid": return [ - " 1. Run: ./scripts/ratchet report", - " 2. Read the Plain English section.", - " 3. If you manually bid, keep spend tiny and write down the Braiins order parameters.", - " 4. After the order ends, wait through the maturity window before judging it.", + " Run exactly:", + " ./scripts/ratchet report", + " Then read only the Plain English section.", + " Manual Braiins action is allowed only after that report still says manual_bid.", " Reason: manual_bid is the only profit-seeking signal, but execution is still manual.", ] if action == "manual_canary": return [ - " 1. If a watch is currently running in another terminal, do nothing until it finishes.", - " 2. If no watch is running, run: ./scripts/ratchet watch 2", - " 3. After the watch finishes, run: ./scripts/ratchet next", - " 4. Read: ./scripts/ratchet experiments", - " Reason: manual_canary means the model sees a bounded learning opportunity, not proven profit.", + " Run exactly:", + " ./scripts/ratchet watch 2", + " Reason: manual_canary means the model sees a bounded learning opportunity, not proven profit. The watch command will print this cockpit again when it ends.", ] return [ - " 1. If a watch is currently running in another terminal, do nothing until it finishes.", - " 2. If no watch is running and you want more data, run: ./scripts/ratchet watch 2", - " 3. If you are done for now, stop. No action is expected from you.", + " STOP.", + " No Braiins action is expected from you.", + " If you want to continue passive learning later, run exactly:", + " ./scripts/ratchet watch 2", " Reason: observe means the strategy did not find a useful action window.", ] @@ -158,6 +176,63 @@ def _running_runs() -> list[str]: return [run for run in running if run not in completed] +def _active_watch() -> str | None: + from_state_file = _active_watch_from_state_file() + if from_state_file: + return from_state_file + return _active_watch_from_process_table() + + +def _active_watch_from_state_file() -> str | None: + if not ACTIVE_WATCH.exists(): + return None + try: + payload = json.loads(ACTIVE_WATCH.read_text(encoding="utf-8")) + except json.JSONDecodeError: + return "state file exists but is unreadable" + pid = payload.get("pid") + run_id = payload.get("run_id", "unknown-run") + if isinstance(pid, int) and _pid_exists(pid): + return f"{run_id} pid={pid}" + return None + + +def _active_watch_from_process_table() -> str | None: + try: + output = subprocess.check_output( + ["ps", "-axo", "pid=,command="], + text=True, + timeout=2, + ) + except (OSError, subprocess.SubprocessError): + return None + current_pid = os.getpid() + for line in output.splitlines(): + stripped = line.strip() + if not stripped: + continue + pid_text, _, command = stripped.partition(" ") + try: + pid = int(pid_text) + except ValueError: + continue + if pid == current_pid: + continue + if "braiins_ratchet.cli watch" in command or "./scripts/ratchet watch" in command: + return f"process pid={pid}" + return None + + +def _pid_exists(pid: int) -> bool: + try: + os.kill(pid, 0) + except ProcessLookupError: + return False + except PermissionError: + return True + return True + + def _freshness_minutes(timestamp_utc: str | None) -> int | None: if not timestamp_utc: return None diff --git a/tests/test_guidance.py b/tests/test_guidance.py index ef22f3d..1e76354 100644 --- a/tests/test_guidance.py +++ b/tests/test_guidance.py @@ -96,8 +96,9 @@ class GuidanceTests(unittest.TestCase): text = build_operator_cockpit(conn) self.assertIn("Braiins sample freshness: stale", text) - self.assertIn("run: ./scripts/ratchet once", text) - self.assertIn("do not interpret old price action", text) + self.assertIn("./scripts/ratchet once", text) + self.assertIn("latest Braiins sample is stale", text) + self.assertIn("DO THIS NOW", text) if __name__ == "__main__":