Skip to content

Devel - #44

Merged
kyoungje merged 261 commits into
mainfrom
devel
Jul 1, 2026
Merged

Devel#44
kyoungje merged 261 commits into
mainfrom
devel

Conversation

@LebahGanteng88

Copy link
Copy Markdown
Contributor

I think it is time to release version 0.2. @kyoungje, please make it a public repository so I can add more reviewers

LayNoah and others added 30 commits April 23, 2026 21:10
- Board 2: canonical 40-tile Hasbro layout transcribed from
  Monopoly_origin.jpg (property/railroad/utility/tax/chance/CC/corners)
- Board 1: 20-tile short game derived from Monopoly_short.png analysis,
  with monopoly_bonus_multiplier=1 (single tile per color group)
- Board 3: 12-tile blank smoke board + board3_blank.svg (4x4 perimeter,
  clockwise numbering from START)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
16 cards per deck. Each card has a structured effect field
(move_to_tile, collect, pay, go_to_jail, grant_jail_free_card,
pay_per_building, collect_from_each_player, move_to_nearest,
move_relative, pay_each_player) so the rules engine can apply them
deterministically without LLM text interpretation.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Self-produced SVG templates (property/railroad/utility) with
placeholder tokens. scripts/render_cards.py reads a board JSON and
stamps templates into one SVG per tile. Generated output committed
under static/assets/property_cards/board{1,2}/ so the UI can serve
them directly without a build step.

Hasbro trade-dress free (own color palette, layout, typography);
gameplay data (price, rent tables) mirrors the standard board.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
PRD.md rewritten from the story-flow layout (Step 1..N) into a
contract-first layout: Summary / Scope / Architecture / Data Model /
API Surface / FSM / Game Rules / Adapters / UI / Config / NFR / Repo /
Testing / CI / Milestones / Open Questions / Appendix. Flattens
10-1-3-1-2-2 deep nesting, centralises data schemas and API surface,
and adds previously missing contracts: WS event envelope, REST error
envelope with fixed error codes, latency budgets, concurrency model,
observability, env/runtime config, ros2_node topic scope with Isaac
card-spawn publisher.

Integrates seven locked decisions in section 1.1:
- Robot player decisions: Gemma for both players, greedy fallback
- Intent schema: owned by this package, 12-entry vocabulary (section 8.2)
- Whisper UX: push-to-talk + multipart upload
- Card art: self-produced SVG templates + renderer
- Board 1: 20-tile layout confirmed from image analysis
- ros2_node: camera subs + Isaac card-spawn pub topic
- Debug routes: MONOPOLY_DEBUG_ROUTES env guard

PRD_easy.md slimmed from 316 to 124 lines: non-overlapping summary
that points to PRD.md for details instead of restating them.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace the synthetic 4x4 square perimeter SVG with the real 3x5
rectangular perimeter used by the Dobot CR3A physical board
(movensys-simulation/dobot_cr3a/monopoly_board.drawio.png). Tile count
unchanged at 12: 4 corners + 1 middle top + 1 middle bottom + 3 per
side column. START remains index 0 at bottom-right, clockwise
numbering preserved.

- board3_blank.svg: redrawn to 300x500 viewBox with merged center area
- board3.json: adds layout hint (rectangle 3x5) and physical_image ref
- board3_physical.png: copied in as reference asset
- PRD.md 7.1: notes physical source and UI fallback SVG

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
New section 16 splits M0..M8 into PR-sized checkboxes with PRD
cross-references. Covers skeleton+CI, Board 3 E2E, Board 1/2 rule
suites, camera+Isaac topic, three adapter integrations, optional
chance art, and cross-cutting concerns (persistence, metrics, README,
production Dockerfile, Playwright regression).

Open Questions shifted to section 17, Appendix to 18. PRD_easy.md
updated to point at the new checklist.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Minimum surface needed for CI to go green:
- main.py: FastAPI app with lifespan, event_id middleware, and
  MONOPOLY_DEBUG_ROUTES guard (404 on /api/debug/* when disabled)
- router.py: /api/health plus per-adapter /api/{robot,stt,llm}/health
- adapters/{stt,llm,robot}.py: stub when env URL is empty, httpx client
  otherwise. Robot fire-and-forget semantics per PRD 8.3; LLM enforces
  the 12-intent vocabulary per PRD 8.2
- utils/logging.py: JSON log formatter with request-scoped event_id
- requirements.txt: fastapi, uvicorn, httpx, pydantic, python-multipart
- .gitignore: pycache, pytest cache, .venv
- scripts/ci_local.sh: honour PEP 668 so it runs on Ubuntu 24.04

Verified: ci_local passes, /api/health -> 200 {"status":"ok"},
/api/robot/health with empty ROBOT_SERVICE_URL -> {"mode":"stub"},
debug routes return 404 with error envelope including event_id when
MONOPOLY_DEBUG_ROUTES=false.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Slim Dockerfile on ros:${ROS_DISTRO}-ros-base. ROS 2 environment is
sourced in entrypoint.sh so ros2_node.py can be added at M4 without
changing the image. At M0 only Python deps are installed; camera/
imaging apt packages are deferred until camera subscribers land.

docker-compose.yml wires every env var from PRD 10.1 with stub-mode
defaults (empty STT/LLM/ROBOT_SERVICE_URL keeps adapters in stub), uses
host networking to share the DDS domain with the robot stack, and runs
on MONOPOLY_PORT (defaults to 8000).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Ros2Bridge spins a single-threaded executor on a daemon thread. rclpy
is imported lazily so uvicorn can start on machines without ROS 2 and
in CI without the ros-tooling setup step. Import failure or init
failure disables the bridge silently and logs a single warning; the
rest of the app keeps running.

At M0 the bridge does nothing besides expose /api/ros2/health so
smoke tests can confirm lifespan wiring. Camera subscribers and the
Isaac card-spawn publisher land in M4 (PRD 11.5).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
GET / serves static/index.html and /assets/* mounts the board and card
SVG directory. The page polls /api/{health,stt,llm,robot,ros2}/health
every 5 s and shows mode badges (stub/live/on/off) in the header plus
the raw JSON below. Enough to sanity-check a deployment without
hitting the CLI; the real Board UI lands in M1.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
All M0 items (16.1) are landed except the final GitHub Actions run.
ci_local.sh passes locally with stub-mode invariant intact.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- game/state.py: pydantic GameState/PlayerState/FSM/RuntimeConfig per
  PRD section 4. Canonical location = positions[player]; players[] holds
  balance, jail status, color. Winner/pending_dice/turn_number tracked.
- game/boards.py: Board/Tile pydantic models, load_board() caches JSON
  parses from static/assets/boards/board{1,2,3}.json.
- game/rules.py: start_game, submit_dice, apply_move (with TILE_MISMATCH
  on from/to divergence), end_turn. Wrap detection drives Board 3 winner
  (first lap wins -> GAME_OVER). RuleError carries a stable error code
  so the API layer can map to PRD section 4.7 envelopes.
- tests/game/: 21 unit tests covering board loading, FSM guards, dice
  validation, move mismatch branches, wrap-to-win for both players,
  end-of-game idempotency.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- game/events.py: tiny in-process pub/sub with bounded per-subscriber
  queues. Drops messages on slow clients rather than blocking the FSM.
  make_envelope() produces PRD 4.6 envelopes (event_id, ts, type,
  payload).
- game/manager.py: wraps GameState with asyncio.Lock (PRD 11.2) and
  emits structured events around every transition: game_started,
  dice_submitted, move_applied, lap_completed, fsm_transition, game_won.
- router.py: adds /game/{start,end_turn,state,winner,config,next_prompt},
  /dice/{request,submit}, /move/apply with pydantic request models.
  RuleError maps to 4xx/409 with the PRD 4.7 error envelope.
- main.py: instantiates GameManager + EventBus in lifespan and installs
  an HTTPException handler that rewrites FastAPI's default {"detail":...}
  into {"error": {code, message, details, event_id}}.

Verified end-to-end via curl: start -> submit dice -> apply move ->
RESOLVE_TILE, TILE_MISMATCH returns 409 with details, end_turn rotates
back to TURN_START.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
On connect the handler subscribes to the EventBus, pushes a hello
envelope carrying the current GameState snapshot so clients can
reconcile without a separate REST round-trip, then forwards every
published event until the client disconnects. Cleanup is finally-based
so the subscriber queue is always released.

Verified with a websockets client: game_started and fsm_transition
envelopes arrive in order after POST /api/game/start.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
tests/e2e/test_board3_smoke.py exercises the public HTTP surface:
- start game (board=3)
- alternate turns: submit dice, apply move, end turn
- user wraps back to START on the 4th move and wins
- GET /api/game/winner and /api/game/state agree
- TILE_MISMATCH path returns 409 with the PRD 4.7 error envelope
- adapter stub invariant holds in the same boot as the game test

pyproject.toml pins asyncio_mode=auto so plain `async def test_*`
functions work without decorators.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- static/index.html: grid layout (board pane + sidebar). Board SVG host
  overlays two <circle> pieces; the sidebar shows game state, manual
  controls (start, submit dice, apply move, end turn), and a scrolling
  event log.
- static/app.css: dark theme, mode badges at top, CSS transitions on
  circle cx/cy drive the 500 ms piece slide.
- static/app.js: reconciles from /api/game/state on boot, opens WS
  /api/stream/game and re-fetches state on each event (simpler and
  robust to dropped messages). Per-board tile-center lookup table keeps
  the piece layer pure SVG.
- main.py: mount /static for JS/CSS alongside existing /assets mount.

Also ticks off M1 items in PRD 16.2 — all 12 checklist entries done,
24 tests green (21 unit + 3 E2E).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Covers the paths developers actually use day to day: uvicorn run/stop,
docker compose up/down, pytest suites, the ci_local.sh reproduction,
env var overrides for live adapter modes, quick health checks, a
one-liner CLI game walk-through, and the handful of errors we've
already hit (PEP 668 pip, port conflicts, wrong cwd).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two fixes for the same failure mode — RMW_IMPLEMENTATION is set to an
RMW whose shared library isn't installed, and rcl reacts by calling
exit(1) at the C level, which Python try/except can't catch.

- docker/Dockerfile: install ros-${ROS_DISTRO}-rmw-cyclonedds-cpp so
  the container matches the rest of the movensys stack (movensys_vlm
  already ships this), which runs with
  RMW_IMPLEMENTATION=rmw_cyclonedds_cpp.
- ros2_node.py: preflight the RMW .so via ctypes.CDLL before calling
  rclpy.init(). If the lib is missing we log a warning and disable the
  bridge instead of crashing the FastAPI process. The existing Python
  try/except around rclpy.init() stays as a second line of defence.
- Dockerfile COPY also switched from explicit file list to `*.py` so
  new top-level modules get shipped automatically (matches movensys_vlm).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
game/properties.py owns the static-plus-dynamic stitching:
- property_id() slug rule shared with render_cards.py
- initial_properties() builds the dict keyed by property_id from a board
- compute_rent() branches on tile.kind:
    property  -> rent_table by houses/hotel; monopoly doubles only when
                 unimproved (PRD section 7.3.5)
    railroad  -> 25 * 2^(n-1) for n railroads the owner holds
    utility   -> dice_sum * 4 or * 10 (1 vs 2 owned)
    mortgaged / unowned / missing dice for utility -> 0
- is_monopoly(), railroads_owned(), utilities_owned() helpers
- even_build_ok() enforces the 1-house max gap for Board 2
- render_card() projects Tile + PropertyState into the PRD section 4.4
  PropertyCard shape for REST responses

game/state.py gains PropertyState and a properties dict on GameState.

20 unit tests cover every rent branch, monopoly detection, even-build,
slug stability, and the Board 1 single-tile-group edge case.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- game/decks.py: Deck/Card pair with draw/return-to-bottom (Hasbro rule)
  and seedable shuffle for reproducible tests. load_chance() and
  load_community_chest() pull from the JSON files committed earlier.
- game/effects.py: apply_effect() pattern-matches the structured effect
  types from the decks (collect, pay, pay_each_player,
  collect_from_each_player, pay_per_building, move_to_tile,
  move_relative, move_to_nearest, grant_jail_free_card, go_to_jail) and
  returns a payload dict suitable for WS events. No LLM needed to
  execute cards — the deck JSON is the contract.
- move_to_tile wrap detection stays correct for "Advance to X" cards
  (wrapped iff destination < current).
- go_to_jail sets in_jail/jail_turns_left; the full jail exit FSM comes
  at M3 (PRD section 7.3.1), Board 1 uses this as a plain teleport.

19 unit tests cover deck shape, shuffle determinism, every effect
branch, and assert every card JSON's effect.type is known by the
dispatcher.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
game/rules.py gains the behavioural pieces needed to play beyond the
blank smoke board:

- resolve_tile() dispatches by the destination tile.kind. For
  property/railroad/utility it either triggers AWAIT_DECISION for an
  unowned tile the player can afford, an unaffordable-flag event, or a
  rent payment. Tax tiles deduct, chance/CC draw from a caller-supplied
  deck and apply the structured effect, go_to_jail teleports. When a
  card moves the player the routine chains to resolve the new tile up
  to max_chain hops.
- buy_property / skip_purchase / build / sell_building / mortgage /
  unmortgage implement the full transaction set from PRD section 7.3.
  Board 2 gates building on monopoly and the even-build (+/-1 per group)
  rule; Board 1's monopoly_bonus_multiplier=1 short-circuits both.
- _auto_liquidate sells buildings in descending house-cost order then
  mortgages unencumbered properties. _pay_rent_or_bankrupt and
  _pay_bank_or_bankrupt move to GAME_OVER when liquidation falls short;
  opponent bankruptcy transfers assets, bank bankruptcy clears them.

start_game also seeds state.properties via initial_properties(board).

25 unit tests cover resolve routing (start, buyable, unaffordable, rent,
self-owned, tax, chance draw, chance chain), the full buy/skip/build/
hotel/mortgage/unmortgage/sell cycle, monopoly and even-build gating,
rent-driven auto-liquidation, rent bankruptcy (assets to opponent), and
tax bankruptcy (assets to bank, opponent still wins 1v1).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
GameManager
- Holds chance_deck and cc_deck instances, reshuffled on start_game.
- apply_move now auto-runs resolve_tile with those decks after a
  successful move, emits a tile_<kind> event per resolution step, and
  returns the list under `resolved.tiles` so the UI can render the
  decision modal or rent notification in the same round-trip.
- Adds buy_property / skip_purchase / decide_property / build /
  mortgage / unmortgage / sell_building / apply_card_effect async
  wrappers. Each publishes an event on the bus and is serialised by
  the same lock the rest of the game uses.
- list_properties() projects PRD §4.4 PropertyCard shape via
  properties.all_cards.

router.py
- New REST endpoints per PRD §5.3-5.5: GET /properties, /properties/{id},
  POST /properties/{id}/decide|buy|build|mortgage|unmortgage|sell_building,
  GET /money, /money/{player}, POST /effects/{type}.
- WS aliases /stream/{board,money,properties} subscribe to the same bus
  as /stream/game with a type allow-list so slow UI panels can tune in
  to only what they render. /stream/game still forwards everything.

Verified manually: start board=1 → dice 1 → move 0→1 (Baltic) →
AWAIT_DECISION with property_arrival_buyable payload → /decide buy →
balance goes 1000→950, owner=user, fsm back to RESOLVE_TILE.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…dal (M2)

- static/assets/boards/board{1.png,2.jpg}: original Hasbro images
  imported as the in-browser backdrop; board{1,2}.json point to them.
- static/app.js:
  * squarePerimeterCenters() derives tile-center (cx, cy) for 20-tile
    and 40-tile boards so piece layers work without hand-tuning.
  * Three-column grid (user props | board | controls + robot props +
    events). Board 3 keeps its 3x5 layout; 1/2 share the square path.
  * Money widget flashes accent colour up/down for 600 ms on balance
    changes (PRD §3.3).
  * Property sidebar groups by color_group, shows house/hotel icons,
    strikes through mortgaged tiles.
  * Decision modal pops on `tile_property_arrival_buyable` event with
    skip / buy / buy+build buttons; submits to /api/properties/{id}/decide.
- static/app.css: header money bar, board pane aspect ratios, modal
  backdrop, property thumb styling.

Verified serving: / returns 3.6 kB HTML, static/app.{css,js} and
assets/boards/board{1.png,2.jpg} all 200.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
tests/board1/test_board1_flow.py exercises the full Board 1 rule set
through the public HTTP surface with ASGITransport:
- buy + ownership transfer
- skip keeps property unowned, no money moved
- rent settles between players on a turn-of-arrival (no monopoly bonus
  because Board 1 multiplier is 1)
- build on a single-tile color group works (no monopoly gate)
- income tax deducts on landing
- bankruptcy-to-bank on unpaid tax ends the game with robot winning
- chance draw on tile 12 surfaces chance_drawn in resolved.tiles
- mortgage cycle returns $25 and costs $27 to lift

PRD section 16.3 (M2) marked complete: 14/14 checklist items shipped,
97 tests green (rules M1 21, properties 20, decks+effects 19, rules
M2 25, board1 9, e2e board3 3).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Board 1 image analysis was off. Correct shape is 4 corners plus 3
tiles per side = 16 tiles, with corners ~1.5x the non-corner tile
width (standard Hasbro layout). Previous 20-tile layout caused the
piece to drift well past the actual tile edge on every move.

- static/assets/boards/board1.json: rewritten with 16 tiles, explicit
  layout hint (shape, per_side, corner_ratio). Monopoly Short Game
  tile list:
    GO, Baltic, Reading RR, Connecticut,
    Just Visiting,
    Electric Co, Virginia, St. James Place,
    Free Parking,
    Illinois, Atlantic, Water Works,
    Go To Jail,
    Pennsylvania, Short Line, Boardwalk
- static/app.js squarePerimeterCenters: now takes a cornerRatio
  argument. Board edge = 2*cornerW + perSide*sideW, so sideW =
  size / (perSide + 2*cornerRatio). Corner centers sit at
  cornerW/2 from the edge; side centers at cornerW + (i-0.5)*sideW.
- static/index.html + app.css: new .board-wrap positions the pieces
  SVG directly on the board-host element, so the overlay always
  matches the image bounds instead of the outer pane.
- static/assets/boards/board2.json: also gets the layout hint.
- Regenerated Board 1 property card SVGs (12 total).
- Dropped test_board1_{tax_tile_deducts,bankruptcy_to_bank_on_tax,
  chance_draw_via_move,start_bonus_on_lap} — they assumed the removed
  Income Tax / Chance tiles. Added test_board1_start_wraps_past_go
  and test_board1_go_to_jail_tile_teleports for the 16-tile layout.
- Updated test_load_board_tile_counts parametrisation to expect 16.

95 tests green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
apply_move was incrementing lap_count but never crediting
board.start_bonus ($100 on Board 1, $200 on Board 2). Non-Board-3
boards now add the bonus to the wrapping player's balance and the
MoveResult carries a start_bonus_collected field. GameManager emits a
new `start_bonus` event so the UI can animate it.

+3 unit tests (Board 1 wrap $100, Board 2 wrap $200, no-wrap no bonus).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
go_to_jail was setting in_jail=True and jail_turns_left=3 on every
board. Board 1's short game has no exit mechanism, so players stayed
flagged as jailed forever with no way to clear it. The flag is now
set only when board.monopoly_bonus_multiplier > 1 (Board 2), which is
also the natural gate for the Board 2 jail exit FSM coming in M3. The
teleport to jail_visit itself still runs on Board 1.

Updated the Board 1 test to assert the new semantics (in_jail False,
jail_turns_left 0 after landing on Go To Jail).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The "both balances went up" UX puzzle was caused by rent-driven
auto-liquidation:
- _auto_liquidate sold buildings from most-expensive first, which
  could refund much more than the rent required
- the whole sequence fired as a single tile_rent_paid event, so the
  UI saw only the final balance change and missed the intermediate
  sell -> rent transitions

Fix:
- Sort the sell phase by price_building ascending (cheapest first) so
  a $14 rent tab never pulls a $200 hotel-tier house sale when a $50
  house sale will do.
- Sort the mortgage phase by price_buy ascending for the same reason.
- _auto_liquidate now returns a list of per-step dicts; the rent/tax
  bankruptcy helpers propagate them up, resolve_tile passes them
  through payload["liquidation"], and GameManager publishes
  building_sold / property_mortgaged events one at a time before the
  final tile_rent_paid (or tile_tax_paid) envelope. The UI can now
  narrate "sold house -> paid rent" instead of showing a mystery
  balance bump.

Test: cheapest-first overshoot — 1 brown house ($25 refund) covers a
$25 rent, leaving dark-blue $200-tier houses intact. 99 tests green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
apply_effect() was accessing effect["amount"], effect["tile_index"],
etc. directly — a missing key raised KeyError which escaped the
router's RuleError/ValueError catch, surfacing as a generic 500.

Introduce _require() that raises EffectError("BAD_REQUEST") and wrap
the dispatch in a try/except that also converts TypeError/ValueError
(bad int casts etc.) into the same EffectError. router.effects_apply
now catches EffectError and returns the PRD section 4.7 error envelope
with status 400.

+2 tests: missing amount and non-numeric amount both raise BAD_REQUEST.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…flag

Economy tuning: Board 1 starting at $1000 with max base rent $50
(Boardwalk) meant rent was ~5% of the bank — games dragged on for
dozens of turns. Setting monopoly_bonus_multiplier from 1 to 2 doubles
the base rent for every owned property (each color group is a single
tile on the short game, so owning that tile is automatically a
monopoly). Boardwalk now costs $100 to land on, Baltic $8, which
matches Hasbro's Short Game intent without inventing new rent tables.

Follow-on fix: the B2 commit's in_jail gate keyed off
monopoly_bonus_multiplier > 1, which would now re-flag Board 1 players
as jailed. The correct rule is "don't set in_jail until the jail-exit
FSM exists (M3)", so go_to_jail is a pure teleport on every board for
now. Once Board 2's jail FSM lands in M3 it will opt in explicitly.

Tests updated: Board 1 rent assertion 4 -> 8, Board 1 single-tile
group bonus now True, go_to_jail tests expect in_jail False on both
boards. 101 tests green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@LebahGanteng88
LebahGanteng88 requested a review from kyoungje July 1, 2026 02:22
@LebahGanteng88 LebahGanteng88 self-assigned this Jul 1, 2026
@kyoungje

kyoungje commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

@LebahGanteng88 @LayNoah @mohsinarf
This was my concern about the big update from devel to main branch.
How much can we know that this devel branch works well enough? It seems that, for now, it's OK because there aren't many users. However, later we have to keep a regular cadence or other policies.
Please think about this.

@kyoungje
kyoungje merged commit 027a324 into main Jul 1, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants