mirror of
https://github.com/saymrwulf/BraiinsRatchet.git
synced 2026-05-14 20:37:52 +00:00
Prevent repeated watch loops
This commit is contained in:
parent
a4a19f30d8
commit
70beecd615
7 changed files with 214 additions and 37 deletions
|
|
@ -39,6 +39,15 @@ You do not need to babysit it. It will:
|
||||||
|
|
||||||
If you want the technical report, run `./scripts/ratchet report`. The normal workflow intentionally shows the cockpit first.
|
If you want the technical report, run `./scripts/ratchet report`. The normal workflow intentionally shows the cockpit first.
|
||||||
|
|
||||||
|
After a watch finishes, the cockpit enters a post-watch cooldown. That is deliberate.
|
||||||
|
|
||||||
|
Post-watch cooldown means:
|
||||||
|
|
||||||
|
1. The current experimental stage is complete.
|
||||||
|
2. Starting another identical watch immediately is not useful ratcheting.
|
||||||
|
3. The run report is the evidence artifact.
|
||||||
|
4. The next planned touch is a later fresh sample, usually `./scripts/ratchet once`.
|
||||||
|
|
||||||
## Research Pathway
|
## Research Pathway
|
||||||
|
|
||||||
The cockpit has two different time horizons:
|
The cockpit has two different time horizons:
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,8 @@ At the end of a normal watch, inspect the ratchet record:
|
||||||
|
|
||||||
The ledger is the main artifact. It says what was tested, how long it ran, what actions the strategy would have considered, and what adaptation should be considered next.
|
The ledger is the main artifact. It says what was tested, how long it ran, what actions the strategy would have considered, and what adaptation should be considered next.
|
||||||
|
|
||||||
|
After a completed watch, the cockpit should not immediately recommend another identical watch. It enters post-watch cooldown, which means the current stage is complete and the next useful operator touch is a later fresh sample.
|
||||||
|
|
||||||
If a run already happened before automatic bookkeeping was available, reconstruct it from the stored SQLite snapshots:
|
If a run already happened before automatic bookkeeping was available, reconstruct it from the stored SQLite snapshots:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
|
|
@ -25,3 +25,25 @@ Karpathy-style ratchet rule: every run states a hypothesis, collects data, score
|
||||||
- action_counts: manual_canary=24
|
- action_counts: manual_canary=24
|
||||||
- report: reports/retro-2026-04-25T19-08-00-00-00.md
|
- report: reports/retro-2026-04-25T19-08-00-00-00.md
|
||||||
- adaptation: The run repeatedly found bounded canary conditions, but modeled net was negative on average. Next ratchet: do not escalate spend; test a smaller depth target or a lower overpay cushion.
|
- adaptation: The run repeatedly found bounded canary conditions, but modeled net was negative on average. Next ratchet: do not escalate spend; test a smaller depth target or a lower overpay cushion.
|
||||||
|
|
||||||
|
## run-20260427T135327Z-222826
|
||||||
|
|
||||||
|
- status: running
|
||||||
|
- started_utc: 2026-04-27T13:53:27+00:00
|
||||||
|
- planned_cycles: 24
|
||||||
|
- interval_seconds: 300
|
||||||
|
- planned_duration_minutes: 120.0
|
||||||
|
- hypothesis: Depth-aware fillable price plus a small overpay cushion is a better canary trigger than raw best ask.
|
||||||
|
- plan: collect public Braiins depth, collect OCEAN state, compute shadow canary, store every proposal.
|
||||||
|
- operator_action: none by default; manual action only if report later says manual_canary or manual_bid and operator agrees.
|
||||||
|
|
||||||
|
- status_update: completed
|
||||||
|
- ended_utc: 2026-04-27T15:48:52+00:00
|
||||||
|
- collected_samples: 24
|
||||||
|
- action_counts: manual_canary=24
|
||||||
|
- strategy_price_min_avg_max: 0.47763 / 0.4798921698412225 / 0.48492
|
||||||
|
- expected_net_min_avg_max_btc: -0.00000383101148933686517479144602 / -0.000002821503981988926325106899014 / -0.00000236319764547711127977695701
|
||||||
|
- latest_action: manual_canary
|
||||||
|
- latest_reason: bounded research canary: profit guardrails not cleared, but expected_net=-0.00000238363529903760002629072482 is within loss budget 0.000025
|
||||||
|
- report: reports/run-20260427T135327Z-222826.md
|
||||||
|
- adaptation: The run repeatedly found bounded canary conditions, but modeled net was negative on average. Next ratchet: do not escalate spend; test a smaller depth target or a lower overpay cushion.
|
||||||
|
|
|
||||||
73
reports/run-20260427T135327Z-222826.md
Normal file
73
reports/run-20260427T135327Z-222826.md
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
# run-20260427T135327Z-222826
|
||||||
|
|
||||||
|
## Ratchet Question
|
||||||
|
|
||||||
|
Depth-aware fillable price plus a small overpay cushion is a better canary trigger than raw best ask.
|
||||||
|
|
||||||
|
## Run Summary
|
||||||
|
|
||||||
|
- started_utc: 2026-04-27T13:53:27+00:00
|
||||||
|
- ended_utc: 2026-04-27T15:48:52+00:00
|
||||||
|
- planned_cycles: 24
|
||||||
|
- interval_seconds: 300
|
||||||
|
- collected_samples: 24
|
||||||
|
- first_sample_utc: 2026-04-27T13:53:28+00:00
|
||||||
|
- last_sample_utc: 2026-04-27T15:48:52+00:00
|
||||||
|
- action_counts: manual_canary=24
|
||||||
|
- strategy_price_min_avg_max: 0.47763 / 0.4798921698412225 / 0.48492
|
||||||
|
- expected_net_min_avg_max_btc: -0.00000383101148933686517479144602 / -0.000002821503981988926325106899014 / -0.00000236319764547711127977695701
|
||||||
|
|
||||||
|
## Interpretation
|
||||||
|
|
||||||
|
The run repeatedly found bounded canary conditions, but modeled net was negative on average. Next ratchet: do not escalate spend; test a smaller depth target or a lower overpay cushion.
|
||||||
|
|
||||||
|
## Operator Reading
|
||||||
|
|
||||||
|
This run collected 24 public Braiins market samples. The strategy outcomes were: manual_canary=24. Expected net ranged from -0.00000383101148933686517479144602 to -0.00000236319764547711127977695701 BTC. For ratcheting, do not ask whether one line was green or red. Ask whether this run changed one control knob: depth target, overpay cushion, canary spend, duration, or timing window.
|
||||||
|
|
||||||
|
## Current Full Report Context
|
||||||
|
|
||||||
|
The block below is the latest complete human report available when this markdown was written. For retroactive reports, the authoritative reconstructed run data is the summary above.
|
||||||
|
|
||||||
|
```text
|
||||||
|
Braiins Ratchet Report
|
||||||
|
|
||||||
|
OCEAN snapshot: 2026-04-27T15:48:52+00:00
|
||||||
|
pool_hashrate_eh_s: 15.51
|
||||||
|
network_difficulty_t: 135.59
|
||||||
|
share_log_window_t: 1084.76
|
||||||
|
avg_block_time_hours: 9
|
||||||
|
|
||||||
|
Braiins market snapshot: 2026-04-27T15:48:52+00:00
|
||||||
|
status: SPOT_INSTRUMENT_STATUS_ACTIVE
|
||||||
|
best_bid_btc_per_eh_day: 0.50343
|
||||||
|
best_ask_btc_per_eh_day: 0.46075
|
||||||
|
fillable_target_ph: 2
|
||||||
|
fillable_price_btc_per_eh_day: 0.46773
|
||||||
|
suggested_bid_btc_per_eh_day: 0.47773
|
||||||
|
last_price_btc_per_eh_day: 0.47933997316187
|
||||||
|
available_hashrate_eh_s: 1.91368833114474
|
||||||
|
sampled_strategy_price_count: 50
|
||||||
|
sampled_strategy_price_min_avg_max: 0.46103 / 0.4792744415237868 / 0.48492
|
||||||
|
|
||||||
|
Strategy action: manual_canary
|
||||||
|
reason: bounded research canary: profit guardrails not cleared, but expected_net=-0.00000238363529903760002629072482 is within loss budget 0.000025
|
||||||
|
breakeven_btc_per_eh_day: 0.4663426590859076733944013201
|
||||||
|
expected_reward_btc: 0.00009761636470096239997370927518
|
||||||
|
expected_net_btc: -0.00000238363529903760002629072482
|
||||||
|
score_btc: -0.00003654936294437444001708897113
|
||||||
|
maturity: treat canary as immature for about 72 hours after spend
|
||||||
|
proposed_price_btc_per_eh_day: 0.47773
|
||||||
|
proposed_spend_btc: 0.00010
|
||||||
|
proposed_duration_minutes: 180
|
||||||
|
implied_hashrate_eh_s: 0.001674586063257488539551629582
|
||||||
|
|
||||||
|
Plain English
|
||||||
|
Decision: a tiny manual research canary is allowed by the loss budget, but this is not a profit signal.
|
||||||
|
Market depth: the visible best ask is 0.46075, but enough depth for 2 PH/s starts at 0.46773 (gap 0.00698).
|
||||||
|
Price check: proposed price is 0.47773; estimated breakeven is 0.4663426590859076733944013201; edge is -0.0113873409140923266055986799 BTC/EH/day.
|
||||||
|
Manual canary card: spend 0.00010 BTC (~10000 sats), duration 180 minutes, estimated speed 1.674586063257488539551629582 PH/s.
|
||||||
|
Expected result: -0.00000238363529903760002629072482 BTC (~-238 sats) before luck; this is a model estimate, not a promise.
|
||||||
|
Wait time: treat canary as immature for about 72 hours after spend.
|
||||||
|
Rule: manual_canary means buying information with bounded downside; manual_bid means the stricter profit-seeking guardrails cleared.
|
||||||
|
```
|
||||||
|
|
@ -97,7 +97,7 @@ cmd_watch() {
|
||||||
echo
|
echo
|
||||||
echo "Watch finished. Reading the cockpit now."
|
echo "Watch finished. Reading the cockpit now."
|
||||||
echo
|
echo
|
||||||
run_python -m braiins_ratchet.cli next
|
BRAIINS_RATCHET_IGNORE_PROCESS_WATCH=1 run_python -m braiins_ratchet.cli next
|
||||||
return "$status"
|
return "$status"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ import subprocess
|
||||||
from .experiments import ACTIVE_WATCH, EXPERIMENT_LOG, REPORTS_DIR
|
from .experiments import ACTIVE_WATCH, EXPERIMENT_LOG, REPORTS_DIR
|
||||||
from .storage import latest_market_snapshot, latest_ocean_snapshot, latest_proposal
|
from .storage import latest_market_snapshot, latest_ocean_snapshot, latest_proposal
|
||||||
|
|
||||||
|
POST_WATCH_COOLDOWN_MINUTES = 360
|
||||||
|
|
||||||
|
|
||||||
def build_operator_cockpit(conn) -> str:
|
def build_operator_cockpit(conn) -> str:
|
||||||
ocean = latest_ocean_snapshot(conn)
|
ocean = latest_ocean_snapshot(conn)
|
||||||
|
|
@ -17,6 +19,7 @@ def build_operator_cockpit(conn) -> str:
|
||||||
latest_report = _latest_report()
|
latest_report = _latest_report()
|
||||||
running_runs = _running_runs()
|
running_runs = _running_runs()
|
||||||
active_watch = _active_watch()
|
active_watch = _active_watch()
|
||||||
|
completed_watch = _recent_completed_watch(latest_report, market.timestamp_utc if market else None)
|
||||||
freshness = _freshness_minutes(market.timestamp_utc if market else None)
|
freshness = _freshness_minutes(market.timestamp_utc if market else None)
|
||||||
is_fresh = freshness is not None and freshness <= 30
|
is_fresh = freshness is not None and freshness <= 30
|
||||||
|
|
||||||
|
|
@ -32,6 +35,7 @@ def build_operator_cockpit(conn) -> str:
|
||||||
f" Latest run report: {latest_report or 'none yet'}",
|
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" Experiment ledger: {EXPERIMENT_LOG.relative_to(REPORTS_DIR.parent) if EXPERIMENT_LOG.exists() else 'none yet'}",
|
||||||
f" Active watch: {active_watch or 'none detected'}",
|
f" Active watch: {active_watch or 'none detected'}",
|
||||||
|
f" Research stage: {_research_stage(active_watch, completed_watch)}",
|
||||||
]
|
]
|
||||||
|
|
||||||
if running_runs:
|
if running_runs:
|
||||||
|
|
@ -41,6 +45,7 @@ def build_operator_cockpit(conn) -> str:
|
||||||
lines.extend(
|
lines.extend(
|
||||||
_do_this_now(
|
_do_this_now(
|
||||||
active_watch=active_watch,
|
active_watch=active_watch,
|
||||||
|
completed_watch=completed_watch,
|
||||||
has_ocean=ocean is not None,
|
has_ocean=ocean is not None,
|
||||||
has_market=market is not None,
|
has_market=market is not None,
|
||||||
is_fresh=is_fresh,
|
is_fresh=is_fresh,
|
||||||
|
|
@ -51,6 +56,7 @@ def build_operator_cockpit(conn) -> str:
|
||||||
lines.extend(
|
lines.extend(
|
||||||
_pathway_forecast(
|
_pathway_forecast(
|
||||||
active_watch=active_watch,
|
active_watch=active_watch,
|
||||||
|
completed_watch=completed_watch,
|
||||||
has_ocean=ocean is not None,
|
has_ocean=ocean is not None,
|
||||||
has_market=market is not None,
|
has_market=market is not None,
|
||||||
is_fresh=is_fresh,
|
is_fresh=is_fresh,
|
||||||
|
|
@ -83,6 +89,7 @@ def build_operator_cockpit(conn) -> str:
|
||||||
|
|
||||||
def _do_this_now(
|
def _do_this_now(
|
||||||
active_watch: str | None,
|
active_watch: str | None,
|
||||||
|
completed_watch: tuple[str, int] | None,
|
||||||
has_ocean: bool,
|
has_ocean: bool,
|
||||||
has_market: bool,
|
has_market: bool,
|
||||||
is_fresh: bool,
|
is_fresh: bool,
|
||||||
|
|
@ -97,6 +104,19 @@ def _do_this_now(
|
||||||
" ./scripts/ratchet",
|
" ./scripts/ratchet",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if completed_watch and action == "manual_canary":
|
||||||
|
report_path, age_minutes = completed_watch
|
||||||
|
remaining = max(0, POST_WATCH_COOLDOWN_MINUTES - age_minutes)
|
||||||
|
return [
|
||||||
|
" STOP.",
|
||||||
|
f" The latest 2-hour watch already finished {age_minutes} minutes ago.",
|
||||||
|
f" Report written: {report_path}",
|
||||||
|
" Do not start another identical watch now.",
|
||||||
|
f" Next planned operator touch: after about {remaining} minutes, run exactly:",
|
||||||
|
" ./scripts/ratchet once",
|
||||||
|
" Reason: this ratchet stage is complete; repeating it immediately would be loop-chasing, not research.",
|
||||||
|
]
|
||||||
|
|
||||||
if not has_ocean or not has_market:
|
if not has_ocean or not has_market:
|
||||||
return [
|
return [
|
||||||
" Run exactly:",
|
" Run exactly:",
|
||||||
|
|
@ -140,6 +160,7 @@ def _do_this_now(
|
||||||
|
|
||||||
def _pathway_forecast(
|
def _pathway_forecast(
|
||||||
active_watch: str | None,
|
active_watch: str | None,
|
||||||
|
completed_watch: tuple[str, int] | None,
|
||||||
has_ocean: bool,
|
has_ocean: bool,
|
||||||
has_market: bool,
|
has_market: bool,
|
||||||
is_fresh: bool,
|
is_fresh: bool,
|
||||||
|
|
@ -153,6 +174,15 @@ def _pathway_forecast(
|
||||||
" Longterm, possible: adjust one strategy knob if the report says the run taught us something.",
|
" Longterm, possible: adjust one strategy knob if the report says the run taught us something.",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if completed_watch and action == "manual_canary":
|
||||||
|
report_path, _age_minutes = completed_watch
|
||||||
|
return [
|
||||||
|
" Planning probabilities are workflow estimates, not profit probabilities.",
|
||||||
|
f" Immediate, certain: stop this stage and keep {report_path} as the evidence artifact.",
|
||||||
|
" Midterm, likely: after cooldown, refresh with one once command and compare against this report.",
|
||||||
|
" Longterm, possible: adjust exactly one knob only if repeated reports show the same pattern.",
|
||||||
|
]
|
||||||
|
|
||||||
if not has_ocean or not has_market:
|
if not has_ocean or not has_market:
|
||||||
return [
|
return [
|
||||||
" Planning probabilities are workflow estimates, not profit probabilities.",
|
" Planning probabilities are workflow estimates, not profit probabilities.",
|
||||||
|
|
@ -228,6 +258,32 @@ def _latest_report() -> str | None:
|
||||||
return str(reports[0].relative_to(REPORTS_DIR.parent))
|
return str(reports[0].relative_to(REPORTS_DIR.parent))
|
||||||
|
|
||||||
|
|
||||||
|
def _recent_completed_watch(latest_report: str | None, latest_market_timestamp: str | None) -> tuple[str, int] | None:
|
||||||
|
if latest_report is None:
|
||||||
|
return None
|
||||||
|
report_path = REPORTS_DIR.parent / latest_report
|
||||||
|
if not report_path.name.startswith("run-") or not report_path.exists():
|
||||||
|
return None
|
||||||
|
market_dt = _parse_utc(latest_market_timestamp)
|
||||||
|
if market_dt is not None and report_path.stat().st_mtime < market_dt.timestamp():
|
||||||
|
return None
|
||||||
|
age_seconds = datetime.now(UTC).timestamp() - report_path.stat().st_mtime
|
||||||
|
age_minutes = max(0, int(age_seconds // 60))
|
||||||
|
if age_minutes > POST_WATCH_COOLDOWN_MINUTES:
|
||||||
|
return None
|
||||||
|
return latest_report, age_minutes
|
||||||
|
|
||||||
|
|
||||||
|
def _research_stage(active_watch: str | None, completed_watch: tuple[str, int] | None) -> str:
|
||||||
|
if active_watch:
|
||||||
|
return "watch running"
|
||||||
|
if completed_watch:
|
||||||
|
report_path, age_minutes = completed_watch
|
||||||
|
remaining = max(0, POST_WATCH_COOLDOWN_MINUTES - age_minutes)
|
||||||
|
return f"post-watch cooldown ({remaining} minutes left, report {report_path})"
|
||||||
|
return "ready"
|
||||||
|
|
||||||
|
|
||||||
def _running_runs() -> list[str]:
|
def _running_runs() -> list[str]:
|
||||||
if not EXPERIMENT_LOG.exists():
|
if not EXPERIMENT_LOG.exists():
|
||||||
return []
|
return []
|
||||||
|
|
@ -266,6 +322,8 @@ def _active_watch_from_state_file() -> str | None:
|
||||||
|
|
||||||
|
|
||||||
def _active_watch_from_process_table() -> str | None:
|
def _active_watch_from_process_table() -> str | None:
|
||||||
|
if os.environ.get("BRAIINS_RATCHET_IGNORE_PROCESS_WATCH") == "1":
|
||||||
|
return None
|
||||||
try:
|
try:
|
||||||
output = subprocess.check_output(
|
output = subprocess.check_output(
|
||||||
["ps", "-axo", "pid=,command="],
|
["ps", "-axo", "pid=,command="],
|
||||||
|
|
@ -302,6 +360,14 @@ def _pid_exists(pid: int) -> bool:
|
||||||
|
|
||||||
|
|
||||||
def _freshness_minutes(timestamp_utc: str | None) -> int | None:
|
def _freshness_minutes(timestamp_utc: str | None) -> int | None:
|
||||||
|
parsed = _parse_utc(timestamp_utc)
|
||||||
|
if parsed is None:
|
||||||
|
return None
|
||||||
|
age = datetime.now(UTC) - parsed
|
||||||
|
return max(0, int(age.total_seconds() // 60))
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_utc(timestamp_utc: str | None) -> datetime | None:
|
||||||
if not timestamp_utc:
|
if not timestamp_utc:
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
|
|
@ -310,8 +376,7 @@ def _freshness_minutes(timestamp_utc: str | None) -> int | None:
|
||||||
return None
|
return None
|
||||||
if parsed.tzinfo is None:
|
if parsed.tzinfo is None:
|
||||||
parsed = parsed.replace(tzinfo=UTC)
|
parsed = parsed.replace(tzinfo=UTC)
|
||||||
age = datetime.now(UTC) - parsed.astimezone(UTC)
|
return parsed.astimezone(UTC)
|
||||||
return max(0, int(age.total_seconds() // 60))
|
|
||||||
|
|
||||||
|
|
||||||
def _freshness_text(freshness: int | None) -> str:
|
def _freshness_text(freshness: int | None) -> str:
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ from datetime import UTC, datetime
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from braiins_ratchet.guidance import build_operator_cockpit
|
from braiins_ratchet.guidance import _do_this_now, _pathway_forecast, build_operator_cockpit
|
||||||
from braiins_ratchet.models import CandidateOrder, MarketSnapshot, OceanSnapshot, StrategyProposal
|
from braiins_ratchet.models import CandidateOrder, MarketSnapshot, OceanSnapshot, StrategyProposal
|
||||||
from braiins_ratchet.storage import init_db, save_market_snapshot, save_ocean_snapshot, save_proposal
|
from braiins_ratchet.storage import init_db, save_market_snapshot, save_ocean_snapshot, save_proposal
|
||||||
|
|
||||||
|
|
@ -66,44 +66,50 @@ class GuidanceTests(unittest.TestCase):
|
||||||
self.assertIn("Longterm, possible", text)
|
self.assertIn("Longterm, possible", text)
|
||||||
|
|
||||||
def test_stale_market_data_routes_operator_to_once(self) -> None:
|
def test_stale_market_data_routes_operator_to_once(self) -> None:
|
||||||
conn = sqlite3.connect(":memory:")
|
lines = _do_this_now(
|
||||||
init_db(conn)
|
active_watch=None,
|
||||||
save_ocean_snapshot(
|
completed_watch=None,
|
||||||
conn,
|
has_ocean=True,
|
||||||
OceanSnapshot(
|
has_market=True,
|
||||||
timestamp_utc="2000-01-01T00:00:00+00:00",
|
is_fresh=False,
|
||||||
pool_hashrate_eh_s=Decimal("16.95"),
|
action="manual_canary",
|
||||||
),
|
|
||||||
)
|
|
||||||
save_market_snapshot(
|
|
||||||
conn,
|
|
||||||
MarketSnapshot(
|
|
||||||
timestamp_utc="2000-01-01T00:00:01+00:00",
|
|
||||||
best_price_btc_per_eh_day=Decimal("0.48031"),
|
|
||||||
source="braiins-public",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
save_proposal(
|
|
||||||
conn,
|
|
||||||
StrategyProposal(
|
|
||||||
action="manual_canary",
|
|
||||||
reason="inside research loss budget",
|
|
||||||
order=None,
|
|
||||||
breakeven_btc_per_eh_day=Decimal("0.46634"),
|
|
||||||
expected_reward_btc=Decimal("0.000097"),
|
|
||||||
expected_net_btc=Decimal("-0.000003"),
|
|
||||||
score_btc=Decimal("-0.000037"),
|
|
||||||
maturity_note="treat canary as immature",
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
text = build_operator_cockpit(conn)
|
text = "\n".join(lines)
|
||||||
|
|
||||||
self.assertIn("Braiins sample freshness: stale", text)
|
|
||||||
self.assertIn("./scripts/ratchet once", text)
|
self.assertIn("./scripts/ratchet once", text)
|
||||||
self.assertIn("latest Braiins sample is stale", text)
|
self.assertIn("latest Braiins sample is stale", text)
|
||||||
self.assertIn("DO THIS NOW", text)
|
|
||||||
self.assertIn("if the fresh state still says manual_canary", text)
|
def test_recent_completed_watch_stops_identical_watch_loop(self) -> None:
|
||||||
|
lines = _do_this_now(
|
||||||
|
active_watch=None,
|
||||||
|
completed_watch=("reports/run-example.md", 4),
|
||||||
|
has_ocean=True,
|
||||||
|
has_market=True,
|
||||||
|
is_fresh=True,
|
||||||
|
action="manual_canary",
|
||||||
|
)
|
||||||
|
|
||||||
|
text = "\n".join(lines)
|
||||||
|
|
||||||
|
self.assertIn("STOP.", text)
|
||||||
|
self.assertIn("Do not start another identical watch now.", text)
|
||||||
|
self.assertIn("./scripts/ratchet once", text)
|
||||||
|
|
||||||
|
def test_recent_completed_watch_forecast_enters_cooldown(self) -> None:
|
||||||
|
lines = _pathway_forecast(
|
||||||
|
active_watch=None,
|
||||||
|
completed_watch=("reports/run-example.md", 4),
|
||||||
|
has_ocean=True,
|
||||||
|
has_market=True,
|
||||||
|
is_fresh=True,
|
||||||
|
action="manual_canary",
|
||||||
|
)
|
||||||
|
|
||||||
|
text = "\n".join(lines)
|
||||||
|
|
||||||
|
self.assertIn("Immediate, certain: stop this stage", text)
|
||||||
|
self.assertIn("Midterm, likely: after cooldown", text)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue