From 4c186ac6771e5a3e2b502d07cd81b2e72297ee85 Mon Sep 17 00:00:00 2001 From: Dominik Roth Date: Sat, 19 Sep 2026 00:37:20 +0200 Subject: [PATCH] Add the puga command and plan pull; document the UI plan workflow - pyproject.toml + puga/cli.py: `pip install -e .` installs a `puga ` command, so no venv path is needed (replaces requirements.txt) - tools/plan_push.py: `pull ` reads a plan (incl. UI edits) back into a YAML spec; recipe entries may carry an amount so switched-off recipes round trip; fixes a variable that overwrote the plan name in build_payload, with a regression test - README: usage with the puga command, and a section on working on the same plans in the PRUNplanner UI and from Claude (write, edit in UI, read back) - CLAUDE.md updated accordingly Co-Authored-By: Claude Sonnet 5 --- CLAUDE.md | 5 ++-- README.md | 47 +++++++++++++++++++++--------- puga.egg-info/PKG-INFO | 8 +++++ puga.egg-info/SOURCES.txt | 23 +++++++++++++++ puga.egg-info/dependency_links.txt | 1 + puga.egg-info/entry_points.txt | 2 ++ puga.egg-info/requires.txt | 4 +++ puga.egg-info/top_level.txt | 1 + puga/cli.py | 29 ++++++++++++++++++ pyproject.toml | 19 ++++++++++++ requirements.txt | 6 ---- tests/test_plan_push.py | 8 +++++ tools/plan_push.py | 30 +++++++++++++++++-- 13 files changed, 158 insertions(+), 25 deletions(-) create mode 100644 puga.egg-info/PKG-INFO create mode 100644 puga.egg-info/SOURCES.txt create mode 100644 puga.egg-info/dependency_links.txt create mode 100644 puga.egg-info/entry_points.txt create mode 100644 puga.egg-info/requires.txt create mode 100644 puga.egg-info/top_level.txt create mode 100644 puga/cli.py create mode 100644 pyproject.toml delete mode 100644 requirements.txt diff --git a/CLAUDE.md b/CLAUDE.md index a84a9e1..b33cac1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -21,7 +21,7 @@ Agent-first toolkit for advising a Prosperous Universe player. The player asks q 3. `ref/` is a gitignored copy of the PRUNplanner repos; refresh with `tools/refresh_refs.sh`. ## Setup -- Python venv at `.venv` (`.venv/bin/python`, `.venv/bin/pip`). Deps in `requirements.txt`. +- Python venv at `.venv`, package installed editable (`pip install -e ".[dev]"`), which provides the `puga` command: `puga [args]` (`.venv/bin/puga` if the venv is not activated). `tools/*.py` also run directly with `.venv/bin/python`. Dependencies are in `pyproject.toml`. - Secrets in `.env` (gitignored; template `.env.example`): FIO REST key, FIO API key, PRUNplanner key, FIO username, company code. Never print or commit them; do not ask the player to paste keys into chat. - Scope: whole universe supported; default exchange from `DEFAULT_CX` (AI1 = Antares), `--cx` to change. @@ -33,8 +33,9 @@ Agent-first toolkit for advising a Prosperous Universe player. The player asks q - Rule: game facts and generic methods go in `docs/`; anything about the player's own bases, cash, decisions or preferences goes in `empire/`. ## Tools +Run as `puga ...` (e.g. `puga scan --min-n 3`, `puga plan pull `); the `tools/x.py` paths below are the same programs. - `tools/state.py sync|show` refreshes `empire/state/company.yaml` from live FIO (cash, permits, buildings, real production efficiency, storage, ships). -- `tools/plan_push.py` builds/validates PRUNplanner plans from YAML specs (`plans/examples/`, `empire/plans/`). Dry run by default; `--apply` only after the player says yes; only `[PuGa]`-named plans are created/updated/deleted (`delete `, only when asked); `list` shows the account's plans. +- `tools/plan_push.py` builds/validates PRUNplanner plans from YAML specs (`plans/examples/`, `empire/plans/`). Dry run by default; `--apply` only after the player says yes; only `[PuGa]`-named plans are created/updated/deleted (`delete `, only when asked); `list` shows the account's plans, `pull -o file.yaml` reads a plan back into a YAML spec including edits made in the PRUNplanner UI. Workflow: push a plan, the player refines it in the UI and saves, then `pull` / `simulate --uuid` read it back and you continue from there. - `tools/simulate.py [--off EXT,SME] [--basis real|uni30|vwap30|ask|...]` replica of PRUNplanner's simulator (efficiency, workforce, material I/O, profit). Flows and efficiency verified exact vs screenshots; profit within ~2%. The NEW CAPEX line = planned minus already built (from state), the honest payback; ignore the plan-level ROI (PRUNplanner always adds a core module). `--uuid` reads the SAVED plan from the account (UI edits must be saved first). - `tools/scan.py` DEPTH-AWARE recipe scan (use this): buildings the market could absorb, ROI for our own size, patient prices, ask-walked inputs. Fully staffed AND understaffed variants by default (`--staffing`), freight netted out (`--trip-cost`, `--cargo`), HQ/COGC/experts/faction, `--planet ID` adds that planet's extraction, fertility and COGC (a planet not in state is a new base: no HQ, permits+1), `--deprec 60` prices demolish-later. `--min-n` is ONLY a noise filter on market capacity (use 3, ideally 10); ROI is always for our own size (`--own`, default 1 building), never at the filtered scale. Below 1 lets sub-building junk in. `--json rows.json` feeds `persistence.py`. Library: `puga/saturation.py`. - `tools/history.py TICKER` monthly margin history of the recipe producing TICKER; `tools/persistence.py rows.json` re-prices scan rows over history (mean ROI 7/14/30/90/180d, payback, net gain over 7 and 14 days, % days profitable). ALWAYS check persistence before recommending: current margins are often a spike. diff --git a/README.md b/README.md index 898f37b..177c0ce 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,17 @@ Plutonium–gallium alloy (Pu–Ga) is a specialized metallic blend of plutonium ## How it is meant to be used -Open Claude Code in this folder. `CLAUDE.md` is loaded automatically: it tells Claude who the player is, which tools exist, which numbers to trust, and how to answer. Claude also reads `empire/PROFILE.md` (who you are, how you want answers) if you have one. Then just ask, e.g. *"what should I build on Sunday?"*, *"is BHP still worth it?"*, *"simulate my HWP plan"*. Every tool is also a normal command line script, so you can run them yourself. +Open Claude Code in this folder. `CLAUDE.md` is loaded automatically: it tells Claude which tools exist, which numbers to trust, and how to answer. Claude also reads `empire/PROFILE.md` (who you are, how you want answers) if you have one. Then just ask, e.g. *"what should I build on Sunday?"*, *"is BHP still worth it?"*, *"how long until I can afford it?"*. Claude answers by running the tools against live market data and your own company data, instead of guessing. Every tool is also a normal command (`puga `), so you can run them yourself. + +### Plans: you and Claude work on the same plans + +The PRUNplanner web UI is where you look at a plan and tweak it. Claude works on the very same plans through the PRUNplanner API, so either of you can pick up where the other stopped: + +1. Claude works out a setup with the tools (`scan`, `chain`, `simulate`) and **writes it as a plan into your PRUNplanner account** (`puga plan`), named `[PuGa] ...`. +2. You **open it in PRUNplanner**, check the simulation, change buildings, recipes, housing, experts or COGC, and save. +3. Claude **reads your saved version back** (`puga plan pull ` gives an editable YAML file, `puga simulate --uuid ` re-simulates it with your edits) and carries on from there: re-pricing it, comparing variants, or adjusting it. + +Only plans named `[PuGa] ...` are ever written or deleted; your other plans are only read. Writing is a dry run unless you (or Claude, after you ask) pass `--apply`. Everything the tools print is an **estimate** from public and personal game data. Prices move; re-run before acting. @@ -39,7 +49,7 @@ Everything the tools print is an **estimate** from public and personal game data **Your own position** - `state`: syncs `empire/state/company.yaml` from FIO: cash, permits, buildings, real production efficiency, storage and ships. - `simulate`: a Python replica of the PRUNplanner simulator (workforce, efficiency, material I/O, profit), verified against screenshots of it. Reads a local plan or a saved plan from your account, and reports the real new capex (planned minus already built). -- `plan_push`: creates, updates and deletes plans in your PRUNplanner account from small YAML files (examples in `plans/examples/`). Dry run by default; only plans named `[PuGa] ...` can be written or deleted. +- `plan` (plan_push): reads plans from your PRUNplanner account into small YAML files and writes them back (create, update, delete), so you and Claude can edit the same plan, you in the UI and Claude in code. Dry run by default; only plans named `[PuGa] ...` can be written or deleted. **Knowledge for Claude** (`CLAUDE.md`, `docs/`) - Verified game mechanics, the market-saturation design, a decision log and a roadmap (`docs/`). Everything specific to your own empire (profile, state, plans, notes) lives in the gitignored `empire/` folder, so this repo can stay public. @@ -52,9 +62,10 @@ Requires Python 3.12 or newer and git. git clone PuGa cd PuGa -# 1. virtual environment +# 1. virtual environment; installs the `puga` command into it python3 -m venv .venv -.venv/bin/pip install -r requirements.txt +.venv/bin/pip install -e ".[dev]" +source .venv/bin/activate # once per shell, so `puga` is on your PATH # 2. keys cp .env.example .env @@ -65,8 +76,8 @@ $EDITOR .env # fill in the values below tools/refresh_refs.sh # 4. check it works -.venv/bin/python -m pytest -.venv/bin/python tools/price.py BHP --cx AI1 +puga test +puga price BHP --cx AI1 ``` ### `.env` @@ -82,18 +93,26 @@ tools/refresh_refs.sh | `COMPANY_CODE` | your company code (used by `tools/state.py` for the first sync) | | `DEFAULT_CX`, `DEFAULT_REGION` | default exchange (`AI1` = Antares) and region | -Only the public data works without keys (prices, recipes, planets). Your own data needs the FIO extension for the browser client to have uploaded it recently: run `tools/state.py sync` and check the `as_of` time. +Only the public data works without keys (prices, recipes, planets). Your own data needs the FIO extension for the browser client to have uploaded it recently: run `puga state sync` and check the `as_of` time. ## Usage +`puga help` lists the tools; `puga --help` shows the options. + ```sh -.venv/bin/python tools/state.py sync # refresh your company state from FIO -.venv/bin/python tools/scan.py --cogc METALLURGY --experts METALLURGY=2 --min-n 3 --top 20 -.venv/bin/python tools/scan.py --planet ZV-307d --min-n 3 # a specific planet -.venv/bin/python tools/scan.py --min-n 3 --json rows.json && .venv/bin/python tools/persistence.py rows.json -.venv/bin/python tools/chain.py KV --qty 13 -.venv/bin/python tools/simulate.py plans/examples/hwp_buildout.yaml --basis vwap30 -.venv/bin/python tools/plan_push.py plans/examples/hwp_buildout.yaml # dry run; add --apply to write +puga state sync # refresh your company state from FIO +puga price BHP --cx AI1 # prices, VWAP, volume, fill price for a quantity +puga scan --cogc METALLURGY --experts METALLURGY=2 --min-n 3 --top 20 +puga scan --planet ZV-307d --min-n 3 # a specific planet +puga scan --min-n 3 --json rows.json && puga persistence rows.json +puga chain KV --qty 13 # make-vs-buy and input sourcing +puga simulate plans/examples/hwp_buildout.yaml --basis vwap30 + +puga plan list # plans in your PRUNplanner account +puga plan pull -o my_plan.yaml # read a plan (including your UI edits) into a YAML file +puga plan my_plan.yaml # dry run: validated summary of what would be written +puga plan my_plan.yaml --apply # write it to your account as a [PuGa] plan +puga simulate --uuid # simulate the plan as saved in your account ``` ## Layout diff --git a/puga.egg-info/PKG-INFO b/puga.egg-info/PKG-INFO new file mode 100644 index 0000000..b73bc44 --- /dev/null +++ b/puga.egg-info/PKG-INFO @@ -0,0 +1,8 @@ +Metadata-Version: 2.4 +Name: puga +Version: 0.1.0 +Summary: Tools and know-how for Claude to assist in Prosperous Universe +Requires-Python: >=3.12 +Requires-Dist: pyyaml +Provides-Extra: dev +Requires-Dist: pytest; extra == "dev" diff --git a/puga.egg-info/SOURCES.txt b/puga.egg-info/SOURCES.txt new file mode 100644 index 0000000..1a4fea5 --- /dev/null +++ b/puga.egg-info/SOURCES.txt @@ -0,0 +1,23 @@ +README.md +pyproject.toml +puga/__init__.py +puga/cache.py +puga/cli.py +puga/config.py +puga/econ.py +puga/fio.py +puga/market.py +puga/prunplanner.py +puga/saturation.py +puga/simulate.py +puga.egg-info/PKG-INFO +puga.egg-info/SOURCES.txt +puga.egg-info/dependency_links.txt +puga.egg-info/entry_points.txt +puga.egg-info/requires.txt +puga.egg-info/top_level.txt +tests/test_econ.py +tests/test_market.py +tests/test_plan_push.py +tests/test_saturation.py +tests/test_simulate.py \ No newline at end of file diff --git a/puga.egg-info/dependency_links.txt b/puga.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/puga.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/puga.egg-info/entry_points.txt b/puga.egg-info/entry_points.txt new file mode 100644 index 0000000..ce41f2b --- /dev/null +++ b/puga.egg-info/entry_points.txt @@ -0,0 +1,2 @@ +[console_scripts] +puga = puga.cli:main diff --git a/puga.egg-info/requires.txt b/puga.egg-info/requires.txt new file mode 100644 index 0000000..84ab88f --- /dev/null +++ b/puga.egg-info/requires.txt @@ -0,0 +1,4 @@ +pyyaml + +[dev] +pytest diff --git a/puga.egg-info/top_level.txt b/puga.egg-info/top_level.txt new file mode 100644 index 0000000..1e5044b --- /dev/null +++ b/puga.egg-info/top_level.txt @@ -0,0 +1 @@ +puga diff --git a/puga/cli.py b/puga/cli.py new file mode 100644 index 0000000..157534f --- /dev/null +++ b/puga/cli.py @@ -0,0 +1,29 @@ +"""`puga [args]`: run a tool from tools/ (installed as the `puga` command by pip install -e .).""" +import runpy, sys +from . import ROOT + +ALIASES = {"plan": "plan_push"} + + +def _tools(): + return sorted(p.stem for p in (ROOT / "tools").glob("*.py")) + + +def main(): + args = sys.argv[1:] + if not args or args[0] in ("-h", "--help", "help"): + print("usage: puga [args]\n\ntools:") + for t in _tools(): + print(" " + ("plan (plan_push)" if t == "plan_push" else t)) + print(" test (run the test suite)") + return + tool, rest = args[0], args[1:] + if tool == "test": + import pytest + sys.exit(pytest.main(["-q", *rest])) + tool = ALIASES.get(tool, tool) + path = ROOT / "tools" / f"{tool}.py" + if not path.exists(): + sys.exit(f"unknown tool '{tool}' (try: puga help)") + sys.argv = [str(path), *rest] + runpy.run_path(str(path), run_name="__main__") diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..662600b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,19 @@ +[build-system] +requires = ["setuptools>=61"] +build-backend = "setuptools.build_meta" + +[project] +name = "puga" +version = "0.1.0" +description = "Tools and know-how for Claude to assist in Prosperous Universe" +requires-python = ">=3.12" +dependencies = ["pyyaml"] + +[project.optional-dependencies] +dev = ["pytest"] + +[project.scripts] +puga = "puga.cli:main" + +[tool.setuptools] +packages = ["puga"] diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index e940406..0000000 --- a/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -iniconfig==2.3.0 -packaging==26.3 -pluggy==1.6.0 -Pygments==2.21.0 -pytest==9.1.1 -PyYAML==6.0.3 diff --git a/tests/test_plan_push.py b/tests/test_plan_push.py index 8cb758b..c54aa5d 100644 --- a/tests/test_plan_push.py +++ b/tests/test_plan_push.py @@ -34,3 +34,11 @@ def test_guardrail_requires_prefix_and_validates(): pp_tool.build_payload(_spec(**bad), R, {"SME"}) with pytest.raises(ValueError): pp_tool.build_payload(_spec(buildings=[{"building": "ZZZ", "amount": 1}]), R, {"SME"}) + + +def test_recipe_dict_keeps_amount_and_does_not_clobber_plan_name(): + """Regression: a loop variable once overwrote the plan name; also switched-off recipes (amount 0) must round trip.""" + spec = _spec(buildings=[{"building": "SME", "amount": 1, "recipes": [{"recipe": "ALO,C,O=>AL", "amount": 0}]}]) + out = pp_tool.build_payload(spec, R, {"SME"}) + assert out["plan_name"] == "[PuGa] x" + assert out["plan_data"]["buildings"][0]["active_recipes"] == [{"recipeid": "SME#6xALO 1xO 1xC=>3xAL", "amount": 0}] diff --git a/tools/plan_push.py b/tools/plan_push.py index 57488f6..f286eab 100755 --- a/tools/plan_push.py +++ b/tools/plan_push.py @@ -4,6 +4,7 @@ DEFAULT IS DRY RUN. Guardrails (docs/decisions.md): only plans named '[PuGa] ... --update refuses if the existing plan's name does not start with '[PuGa]'. Show the dry-run to the user and get a yes before --apply. tools/plan_push.py list + tools/plan_push.py pull [-o file.yaml] # read a plan (UI edits included) back into a YAML spec tools/plan_push.py plans/examples/base_plus_hwp.yaml # dry run: validated payload summary tools/plan_push.py plans/examples/base_plus_hwp.yaml --json # full JSON payload tools/plan_push.py plans/examples/base_plus_hwp.yaml --apply # create (after user says yes) @@ -66,7 +67,10 @@ def build_payload(spec: dict, recipes: list[dict], building_tickers: set[str]) - tk = b["building"] if tk not in building_tickers: raise ValueError(f"unknown building {tk}") - recs = [{"recipeid": resolve_recipe(r, tk, recipes), "amount": 1} for r in b.get("recipes", [])] + recs = [] + for r in b.get("recipes", []): + rname, amt = (r["recipe"], r.get("amount", 1)) if isinstance(r, dict) else (r, 1) + recs.append({"recipeid": resolve_recipe(rname, tk, recipes), "amount": int(amt)}) buildings.append({"name": tk, "amount": int(b["amount"]), "active_recipes": recs}) infra = [{"building": k, "amount": int(v)} for k, v in (spec.get("infrastructure") or {}).items()] return { @@ -80,8 +84,9 @@ def build_payload(spec: dict, recipes: list[dict], building_tickers: set[str]) - def main(): ap = argparse.ArgumentParser() - ap.add_argument("spec", help="plans/*.yaml, 'list', or 'delete'") - ap.add_argument("target", nargs="?", help="uuid for 'delete'") + ap.add_argument("spec", help="plans/*.yaml, 'list', 'pull' or 'delete'") + ap.add_argument("target", nargs="?", help="uuid for 'delete' / 'pull'") + ap.add_argument("-o", "--out", help="with 'pull': write the YAML spec to this file") ap.add_argument("--apply", action="store_true", help="actually write to PRUNplanner (creates a new plan)") ap.add_argument("--update", metavar="UUID", help="with --apply: update this existing [PuGa] plan instead of creating") ap.add_argument("--json", action="store_true") @@ -91,6 +96,25 @@ def main(): for p in pp.request("GET", "/planning/plan/"): print(p["uuid"], p.get("plan_name"), p.get("planet_natural_id")) return + if a.spec == "pull": # read a plan from the account (e.g. after edits in the PRUNplanner UI) into a YAML spec + cur = pp.request("GET", f"/planning/plan/{a.target}/") + d = cur["plan_data"] + cg = cur.get("plan_cogc") + spec = {"name": cur["plan_name"], "planet": cur["planet_natural_id"], "permits": cur["plan_permits_used"], + "cogc": None if cg in (None, "---") else cg, "hq": cur["plan_corphq"], + "experts": {e["type"].upper(): e["amount"] for e in d["experts"] if e["amount"]}, + "lux": {w["type"]: [w["lux1"], w["lux2"]] for w in d["workforce"] if not (w["lux1"] and w["lux2"])}, + "infrastructure": {i["building"]: i["amount"] for i in d["infrastructure"]}, + "buildings": [{"building": b["name"], "amount": b["amount"], + "recipes": [r["recipeid"] if r["amount"] == 1 else {"recipe": r["recipeid"], "amount": r["amount"]} for r in b["active_recipes"]]} + for b in d["buildings"]]} + text = yaml.safe_dump(spec, sort_keys=False, default_flow_style=None, width=140) + if a.out: + Path(a.out).write_text(text) + print("wrote", a.out) + else: + print(text) + return if a.spec == "delete": # delete is allowed only on request, and only for plans this tool made ([PuGa] prefix). cur = pp.request("GET", f"/planning/plan/{a.target}/")