Skip to content

Build the serverless friendly battle foundation through PR7#40

Closed
ThunderConch wants to merge 12 commits into
masterfrom
feat/serverless-friendly-battle
Closed

Build the serverless friendly battle foundation through PR7#40
ThunderConch wants to merge 12 commits into
masterfrom
feat/serverless-friendly-battle

Conversation

@ThunderConch
Copy link
Copy Markdown
Owner

Summary

This PR lands the serverless friendly battle foundation through the roadmap's PR7 milestone on a single review branch.

It intentionally builds the first playable/falsifiable local-friendly-battle path without introducing server authority, ladder systems, reconnect logic, anti-cheat, internet matchmaking, or trade flows.

What landed

  • PR1 docs axis
    • establishes docs/friendly-battle/ as the source of truth for this track
    • keeps legacy PvP docs from steering new implementation decisions
  • PR2 transport feasibility evidence
    • proves direct host/join transport is viable
    • captures the transport gate evidence in repo docs
  • PR3 session/state boundary
    • stabilizes session state before engine wiring
  • PR4 battle adapter
    • ensures turn resolution stays engine-authoritative instead of letting friendly-battle turns bypass battle rules
  • PR5 local party snapshot boundary
    • uses read-only snapshots from local progression for friendly battle entry
    • battle outcomes do not mutate progression state
  • PR6 local two-terminal harness
    • makes same-machine host/join flows reproducible for development and testing
  • PR7 product-facing CLI UX v1
    • adds tokenmon friendly-battle
    • adds public host, join, ready, and leave surfaces
    • keeps the existing local harness behind a thin facade
    • updates the host-generated JOIN_COMMAND to re-enter through the public CLI

Current UX / scope

This branch is intentionally serverless + friendly-only.

Included now:

  • local snapshot-based party import
  • same-machine host/join session flow
  • engine-resolved battle turns
  • public CLI surface for friendly battle local v1

Not included yet:

  • ladder / rating / seasons
  • anti-cheat
  • reconnect / resume
  • internet-wide matchmaking
  • spectate / replay
  • trade
  • production-hardened disconnect handling

Verification

  • npm test ✅ (# pass 1131, # fail 0)
  • npm run build
  • node --import tsx --test test/friendly-battle-cli.test.ts test/friendly-battle-local-harness.test.ts
  • git diff --check
  • zero LSP diagnostics on the PR7-touched CLI files ✅

Notes for review

  • src/cli/friendly-battle.ts is intentionally a thin product-facing facade over the local harness.
  • src/cli/friendly-battle-local.ts remains the local harness / reproduction surface.
  • Future work from the roadmap starts at PR8 transport hardening and PR9 ruleset/polish after this branch is reviewed.

This imports the new friendly-battle planning/docs tree into the
master-based implementation branch and links it from the docs home so
future work can execute against one canonical direction. The legacy PvP
docs are carried alongside it as a reference track because the new docs
link back to them and maintainers still need the older contracts for
historical context.

Constraint: The serverless-friendly-battle line must proceed from master without depending on the legacy PvP PR stack
Constraint: Docs home must distinguish current source-of-truth guidance from legacy reference material
Rejected: Import only docs/friendly-battle | would break cross-links to the legacy PvP reference set
Rejected: Rewrite or delete legacy PvP docs in PR1 | too broad for the direction-lock slice
Confidence: high
Scope-risk: narrow
Directive: Treat docs/friendly-battle as the canonical direction for this implementation line and docs/pvp as legacy/reference only
Tested: npm run build && npm test; relative markdown link resolution check
Not-tested: automated markdown lint or external docs-site rendering
PR1 review showed two ambiguity sources remained after the initial docs axis landed: the legacy PvP index still read like the active implementation contract, and the transport gate lacked a canonical verdict artifact plus measurable pass/fail criteria.

This follow-up tightens the handoff. The legacy PvP index now explicitly warns readers that friendly-battle docs are the current source of truth, and the transport gate now defines mandatory artifacts, command-count expectations, failure-UX requirements, and a canonical report path for PR2 decisions.

Constraint: PR2 needs an unambiguous kill-or-commit contract before transport spike work continues
Rejected: Leave reviewer guidance in comments only | the next executor could still branch from the wrong docs axis
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep docs/pvp positioned as historical reference unless the product explicitly returns to the server-authoritative track
Tested: npm test; relative-link check across updated friendly-battle/pvp docs
Not-tested: Human review of wording outside the reviewed source-of-truth handoff
The transport feasibility gate required a report file, but the report template
could not capture two of the failure-UX proofs called out by review: the most
likely bad input and an actionable retry command. The gate doc also did not say
clearly enough that a placeholder or TBD-filled report still fails the gate.

This tightens the docs contract so PR2 cannot satisfy the gate with an empty
shell and so the canonical report structure matches the validation criteria.

Constraint: PR2 feasibility must be reviewable from repo artifacts alone
Rejected: Rely on reviewer memory for missing failure UX fields | too easy to miss during later PRs
Confidence: high
Scope-risk: narrow
Directive: Keep gate requirements and report template fields in lockstep whenever validation criteria change
Tested: npm test
Tested: git diff --check
Tested: targeted docs-only review against working tree diff
Not-tested: none
…ly battle stack

This spike keeps the product surface provisional while validating that
same-machine host/join/ready/start/action exchange works inside the
Claude Code plugin environment. It also hardens the failure UX enough
to decide whether the serverless-first direction remains viable before
session and battle contracts are formalized.

Constraint: PR2 is a transport spike, not the final friendly battle CLI
Rejected: Build session and battle abstractions first | transport feasibility had to be de-risked earlier
Confidence: high
Scope-risk: narrow
Directive: Keep the spike transport isolated from product UX and absorb only its proven contracts into later PRs
Tested: transport spike tests, CLI spike tests, typecheck, build, git diff --check, subagent code review
Not-tested: cross-machine LAN, NAT/firewall behavior
PR3 fixes the friendly-battle contract surface before the battle adapter
lands. It establishes explicit session/progression/snapshot/battle
shapes, removes accidental coupling to the current core turn-action
types, and reserves a dedicated storage namespace so later PRs can wire
battle runtime and CLI flows onto the same vocabulary.

Constraint: Progression, snapshot, session, and battle state must remain separate source-of-truth layers
Constraint: Friendly battle storage must not collide with legacy singleton battle/session files
Rejected: Reuse core TurnAction directly | would couple the session contract to current battle-engine internals
Rejected: Keep snapshot metadata on participant records | duplicates source of truth and muddies session boundaries
Confidence: high
Scope-risk: narrow
Directive: Keep downstream PR4+ adapters translating between friendly-battle contracts and core battle types instead of importing core runtime types into the contract layer
Tested: node --import tsx --test test/friendly-battle-contracts.test.ts; node --import tsx --test test/friendly-battle-paths.test.ts; npm run typecheck; npm run build; git diff --check; subagent review APPROVED
Not-tested: Live transport/session orchestration and progression-to-snapshot conversion flows
This wires the new friendly-battle session layer into the existing turn battle
engine with a minimal adapter, while keeping the battle engine as the
source of truth for switch resolution and turn settlement.

The adapter now waits for both sides, validates normal-turn submissions,
emits session-facing battle events, and preserves the legitimate
Struggle fallback instead of masking invalid client input. A small
session-generation invariant is also locked in so later snapshot work
cannot silently bridge mismatched generations.

Constraint: PR4 must stay transport-agnostic and prove session-facing battle flow in isolation
Rejected: Re-implement switch logic inside friendly-battle adapter | would fork core battle semantics too early
Rejected: Accept invalid move selections and rely on downstream fallback | would hide protocol/client bugs
Confidence: high
Scope-risk: moderate
Directive: Keep friendly-battle choice validation aligned with core engine behavior before adding transport or CLI layers
Tested: node --import tsx --test test/friendly-battle-contracts.test.ts test/friendly-battle-battle-adapter.test.ts test/turn-battle.test.ts; npm run typecheck; npm run build; git diff --check
Not-tested: End-to-end host/join transport integration
친선전 시작 시 progression을 직접 battle layer에 넘기지 않도록
snapshot 계약과 검증 로직을 추가했다. 파티 순서, 레벨, 닉네임,
기술 구성을 battle-ready 형태로 동결하고, generation hook을 통해
후속 PR에서 세대별 규칙을 확장할 수 있게 했다.

Constraint: 친선전은 로컬 progression 감성을 유지해야 한다
Constraint: PR6/PR7에서 재사용 가능한 순수 snapshot 경계가 필요하다
Rejected: session state에 full progression 참조를 그대로 보관 | battle/session 경계가 흐려짐
Rejected: learned/fallback move만 재계산 | 로컬 성장 상태와 현재 세팅 보존 요구와 충돌
Confidence: high
Scope-risk: moderate
Directive: snapshot은 battle runtime과 메모리 참조를 공유하지 않도록 clone 경계를 유지할 것
Tested: node --import tsx --test test/friendly-battle-snapshot.test.ts
Tested: node --import tsx --test test/friendly-battle-*.test.ts
Tested: npm run typecheck
Tested: npm run build
Tested: git diff --check
Tested: LSP diagnostics on changed files (0 errors)
Tested: subagent code review 승인(Schrodinger)
Not-tested: 실제 host/join 세션에 snapshot을 연결하는 통합 경로는 PR6에서 검증 예정
PR6 local harness now reuses the canonical state hydration path for external profiles, reads battle teams back from persisted snapshot files, and hardens the host/join CLI boundary with explicit numeric validation. This keeps same-machine friendly battles aligned with the snapshot artifacts we actually persist instead of drifting with later in-memory mutations.

The follow-up test pass also locks the cleanup policy on both success and failed host handshakes so the harness stays predictable while the serverless-friendly architecture evolves.

Constraint: PR6 must stay serverless-friendly and rely only on local artifact exchange plus the existing CLI surface
Rejected: Rebuild battle teams from in-memory profile objects after artifact creation | allows stale mutations to bypass persisted snapshot authority
Rejected: Duplicate partial state-default merging inside the local harness | risks drift from the canonical loader and migration path
Confidence: high
Scope-risk: moderate
Directive: Any future external profile loader should call hydrateState rather than re-implementing default merges or migrations
Tested: npm run typecheck
Tested: node --import tsx --test test/state.test.ts test/friendly-battle-local-harness.test.ts
Tested: node --import tsx --test test/friendly-battle-battle-adapter.test.ts test/friendly-battle-contracts.test.ts test/friendly-battle-local-harness.test.ts test/friendly-battle-paths.test.ts test/friendly-battle-snapshot.test.ts test/friendly-battle-spike-cli.test.ts test/friendly-battle-transport-spike.test.ts
Not-tested: Cross-machine/manual operator run outside the same workstation shell
The local harness already supported same-machine battles, but users still had to
invoke an internal script directly. This change adds a product-facing
tokenmon friendly-battle surface, keeps the existing local harness reusable,
and makes the host-generated JOIN_COMMAND re-enter through the public CLI.

Constraint: PR7 stays serverless/local-only and must avoid repo-wide recovery scans after the compact loop
Constraint: The friendly-battle wrapper must stay a thin facade over the local harness
Rejected: Rewriting the local harness into a broader transport/session layer in this slice | too broad for the current PR boundary
Confidence: high
Scope-risk: moderate
Directive: Keep src/cli/friendly-battle.ts facade-only; push future session/runtime complexity down into transport/runtime layers
Tested: npm test; npm run build; node --import tsx --test test/friendly-battle-cli.test.ts test/friendly-battle-local-harness.test.ts; git diff --check; LSP diagnostics on affected files; local help/alias-conflict quick checks
Not-tested: Manual multi-terminal run from two live Claude profiles after this wrapper change
The repository had no GitHub Actions workflow, which left PRs without
check-runs or commit statuses even when local verification passed.
This adds a minimal Node 22 CI pipeline that typechecks, runs tests,
and builds so future PRs surface machine-verifiable evidence directly
in GitHub.

Constraint: Repository currently targets Node >=22.0.0 and uses npm lockfile installs
Constraint: Keep CI minimal and aligned with existing package scripts only
Rejected: Add lint or matrix jobs now | repo does not define a lint script and extra jobs would add noise before baseline CI exists
Rejected: Add only test step | would miss type/build regressions already part of local verification expectations
Confidence: high
Scope-risk: narrow
Directive: Keep CI script list in sync with package.json; do not add non-existent repo scripts just for convention
Tested: npm run typecheck; npm test; npm run build; git diff --check; subagent code review
Not-tested: Remote GitHub Actions execution after push
The local friendly-battle tests were assuming zsh and prebuilt dist hook
artifacts, while audio playback could still surface async ENOENT failures
on CI hosts that do not ship desktop media players. This change switches
shell-based smoke tests to the platform default shell, runs the
session-start hook from source during tests, hardens detached audio spawns,
and gives the same-machine battle smoke a little more room under full-suite
load.

Constraint: GitHub Actions runners may not have zsh or desktop audio players
Constraint: npm test runs before npm run build in CI
Rejected: Install zsh and audio players in CI | would hide portability bugs in the repo
Rejected: Keep the 4s local battle timeout | remained flaky under full-suite load
Confidence: high
Scope-risk: narrow
Directive: Keep integration tests shell-agnostic and avoid dist dependencies in prebuild test paths
Tested: node --import tsx --test test/friendly-battle-local-harness.test.ts
Tested: node --import tsx --test test/friendly-battle-spike-cli.test.ts
Tested: node --import tsx --test test/session-start.test.ts
Tested: node --import tsx --test test/sfx.test.ts
Tested: npm test
Tested: npm run typecheck
Tested: npm run build
Not-tested: GitHub Actions rerun on the pushed commit
Friendly battle local v1 started as a same-machine spike, but the current product goal is same-network instant battles between two Claude profiles. This change separates the host bind address from the guest-facing join address, threads that contract through the CLI, and reshapes the smoke tests so they keep exercising the printed command flow without relying on two heavyweight shell children racing each other.

The result is that hosts can listen on wildcard interfaces while still printing a concrete join command for the guest, and the surrounding tests better reflect the supported same-network flow.

Constraint: This feature must stay inside the existing Claude Code plugin repo without adding a separate always-on server component
Constraint: Friendly battle v1 still optimizes for ad-hoc matches, not cheat resistance, matchmaking, or reconnect recovery
Rejected: Keep a single --host flag for both bind and guest join address | breaks same-network use when the host listens on 0.0.0.0
Rejected: Keep two spawned shell children in spike smoke tests | too slow and flaky on headless CI runners
Confidence: high
Scope-risk: moderate
Directive: Do not collapse listen host and guest-facing join host back together unless same-network host UX is re-specified end-to-end
Tested: npm test
Tested: npm run typecheck
Tested: npm run build
Tested: Subagent code review + focused friendly-battle regression review
Not-tested: Real two-machine manual LAN run outside the automated harness
ThunderConch added a commit that referenced this pull request Apr 14, 2026
Friendly step-by-step walkthrough so anyone can reproduce the PR44–PR46
visual QA on a single machine without needing two physical hosts.
Covers:

- What each PR in the stack (#40#46) adds and what to eyeball
- How to sync the tkm plugin install to feat/friendly-battle-pvp-leave
  via a plain git checkout when the plugin clone is ThunderConch/tkm
- How to use an isolated CLAUDE_CONFIG_DIR (~/.claude-fb-guest) for the
  second Claude Code session so the two terminals don't race over the
  same tokenmon state
- Per-PR screenshot checklist (basic turn loop, forced switch, surrender
  confirm, leave / opponent-left)
- Rollback instructions (git checkout master + ~/.claude-fb-guest
  cleanup)
- Troubleshooting the 6 most common stuck spots

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ThunderConch added a commit that referenced this pull request Apr 16, 2026
…urrender, and leave (#40-#46)

* Establish a source-of-truth docs axis for serverless friendly battles

This imports the new friendly-battle planning/docs tree into the
master-based implementation branch and links it from the docs home so
future work can execute against one canonical direction. The legacy PvP
docs are carried alongside it as a reference track because the new docs
link back to them and maintainers still need the older contracts for
historical context.

Constraint: The serverless-friendly-battle line must proceed from master without depending on the legacy PvP PR stack
Constraint: Docs home must distinguish current source-of-truth guidance from legacy reference material
Rejected: Import only docs/friendly-battle | would break cross-links to the legacy PvP reference set
Rejected: Rewrite or delete legacy PvP docs in PR1 | too broad for the direction-lock slice
Confidence: high
Scope-risk: narrow
Directive: Treat docs/friendly-battle as the canonical direction for this implementation line and docs/pvp as legacy/reference only
Tested: npm run build && npm test; relative markdown link resolution check
Not-tested: automated markdown lint or external docs-site rendering

* Prevent legacy PvP docs from derailing the friendly-battle branch

PR1 review showed two ambiguity sources remained after the initial docs axis landed: the legacy PvP index still read like the active implementation contract, and the transport gate lacked a canonical verdict artifact plus measurable pass/fail criteria.

This follow-up tightens the handoff. The legacy PvP index now explicitly warns readers that friendly-battle docs are the current source of truth, and the transport gate now defines mandatory artifacts, command-count expectations, failure-UX requirements, and a canonical report path for PR2 decisions.

Constraint: PR2 needs an unambiguous kill-or-commit contract before transport spike work continues
Rejected: Leave reviewer guidance in comments only | the next executor could still branch from the wrong docs axis
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep docs/pvp positioned as historical reference unless the product explicitly returns to the server-authoritative track
Tested: npm test; relative-link check across updated friendly-battle/pvp docs
Not-tested: Human review of wording outside the reviewed source-of-truth handoff

* Prevent PR2 gate sign-off from passing on template-only evidence

The transport feasibility gate required a report file, but the report template
could not capture two of the failure-UX proofs called out by review: the most
likely bad input and an actionable retry command. The gate doc also did not say
clearly enough that a placeholder or TBD-filled report still fails the gate.

This tightens the docs contract so PR2 cannot satisfy the gate with an empty
shell and so the canonical report structure matches the validation criteria.

Constraint: PR2 feasibility must be reviewable from repo artifacts alone
Rejected: Rely on reviewer memory for missing failure UX fields | too easy to miss during later PRs
Confidence: high
Scope-risk: narrow
Directive: Keep gate requirements and report template fields in lockstep whenever validation criteria change
Tested: npm test
Tested: git diff --check
Tested: targeted docs-only review against working tree diff
Not-tested: none

* Prove direct host-join transport is viable before building the friendly battle stack

This spike keeps the product surface provisional while validating that
same-machine host/join/ready/start/action exchange works inside the
Claude Code plugin environment. It also hardens the failure UX enough
to decide whether the serverless-first direction remains viable before
session and battle contracts are formalized.

Constraint: PR2 is a transport spike, not the final friendly battle CLI
Rejected: Build session and battle abstractions first | transport feasibility had to be de-risked earlier
Confidence: high
Scope-risk: narrow
Directive: Keep the spike transport isolated from product UX and absorb only its proven contracts into later PRs
Tested: transport spike tests, CLI spike tests, typecheck, build, git diff --check, subagent code review
Not-tested: cross-machine LAN, NAT/firewall behavior

* Stabilize friendly battle state boundaries before engine integration

PR3 fixes the friendly-battle contract surface before the battle adapter
lands. It establishes explicit session/progression/snapshot/battle
shapes, removes accidental coupling to the current core turn-action
types, and reserves a dedicated storage namespace so later PRs can wire
battle runtime and CLI flows onto the same vocabulary.

Constraint: Progression, snapshot, session, and battle state must remain separate source-of-truth layers
Constraint: Friendly battle storage must not collide with legacy singleton battle/session files
Rejected: Reuse core TurnAction directly | would couple the session contract to current battle-engine internals
Rejected: Keep snapshot metadata on participant records | duplicates source of truth and muddies session boundaries
Confidence: high
Scope-risk: narrow
Directive: Keep downstream PR4+ adapters translating between friendly-battle contracts and core battle types instead of importing core runtime types into the contract layer
Tested: node --import tsx --test test/friendly-battle-contracts.test.ts; node --import tsx --test test/friendly-battle-paths.test.ts; npm run typecheck; npm run build; git diff --check; subagent review APPROVED
Not-tested: Live transport/session orchestration and progression-to-snapshot conversion flows

* Prevent friendly battle turns from bypassing engine rules

This wires the new friendly-battle session layer into the existing turn battle
engine with a minimal adapter, while keeping the battle engine as the
source of truth for switch resolution and turn settlement.

The adapter now waits for both sides, validates normal-turn submissions,
emits session-facing battle events, and preserves the legitimate
Struggle fallback instead of masking invalid client input. A small
session-generation invariant is also locked in so later snapshot work
cannot silently bridge mismatched generations.

Constraint: PR4 must stay transport-agnostic and prove session-facing battle flow in isolation
Rejected: Re-implement switch logic inside friendly-battle adapter | would fork core battle semantics too early
Rejected: Accept invalid move selections and rely on downstream fallback | would hide protocol/client bugs
Confidence: high
Scope-risk: moderate
Directive: Keep friendly-battle choice validation aligned with core engine behavior before adding transport or CLI layers
Tested: node --import tsx --test test/friendly-battle-contracts.test.ts test/friendly-battle-battle-adapter.test.ts test/turn-battle.test.ts; npm run typecheck; npm run build; git diff --check
Not-tested: End-to-end host/join transport integration

* 로컬 파티를 안전한 친선전 snapshot 경계로 고정한다

친선전 시작 시 progression을 직접 battle layer에 넘기지 않도록
snapshot 계약과 검증 로직을 추가했다. 파티 순서, 레벨, 닉네임,
기술 구성을 battle-ready 형태로 동결하고, generation hook을 통해
후속 PR에서 세대별 규칙을 확장할 수 있게 했다.

Constraint: 친선전은 로컬 progression 감성을 유지해야 한다
Constraint: PR6/PR7에서 재사용 가능한 순수 snapshot 경계가 필요하다
Rejected: session state에 full progression 참조를 그대로 보관 | battle/session 경계가 흐려짐
Rejected: learned/fallback move만 재계산 | 로컬 성장 상태와 현재 세팅 보존 요구와 충돌
Confidence: high
Scope-risk: moderate
Directive: snapshot은 battle runtime과 메모리 참조를 공유하지 않도록 clone 경계를 유지할 것
Tested: node --import tsx --test test/friendly-battle-snapshot.test.ts
Tested: node --import tsx --test test/friendly-battle-*.test.ts
Tested: npm run typecheck
Tested: npm run build
Tested: git diff --check
Tested: LSP diagnostics on changed files (0 errors)
Tested: subagent code review 승인(Schrodinger)
Not-tested: 실제 host/join 세션에 snapshot을 연결하는 통합 경로는 PR6에서 검증 예정

* Keep local friendly battles bound to persisted snapshots

PR6 local harness now reuses the canonical state hydration path for external profiles, reads battle teams back from persisted snapshot files, and hardens the host/join CLI boundary with explicit numeric validation. This keeps same-machine friendly battles aligned with the snapshot artifacts we actually persist instead of drifting with later in-memory mutations.

The follow-up test pass also locks the cleanup policy on both success and failed host handshakes so the harness stays predictable while the serverless-friendly architecture evolves.

Constraint: PR6 must stay serverless-friendly and rely only on local artifact exchange plus the existing CLI surface
Rejected: Rebuild battle teams from in-memory profile objects after artifact creation | allows stale mutations to bypass persisted snapshot authority
Rejected: Duplicate partial state-default merging inside the local harness | risks drift from the canonical loader and migration path
Confidence: high
Scope-risk: moderate
Directive: Any future external profile loader should call hydrateState rather than re-implementing default merges or migrations
Tested: npm run typecheck
Tested: node --import tsx --test test/state.test.ts test/friendly-battle-local-harness.test.ts
Tested: node --import tsx --test test/friendly-battle-battle-adapter.test.ts test/friendly-battle-contracts.test.ts test/friendly-battle-local-harness.test.ts test/friendly-battle-paths.test.ts test/friendly-battle-snapshot.test.ts test/friendly-battle-spike-cli.test.ts test/friendly-battle-transport-spike.test.ts
Not-tested: Cross-machine/manual operator run outside the same workstation shell

* Expose local friendly battles through the main tokenmon CLI

The local harness already supported same-machine battles, but users still had to
invoke an internal script directly. This change adds a product-facing
tokenmon friendly-battle surface, keeps the existing local harness reusable,
and makes the host-generated JOIN_COMMAND re-enter through the public CLI.

Constraint: PR7 stays serverless/local-only and must avoid repo-wide recovery scans after the compact loop
Constraint: The friendly-battle wrapper must stay a thin facade over the local harness
Rejected: Rewriting the local harness into a broader transport/session layer in this slice | too broad for the current PR boundary
Confidence: high
Scope-risk: moderate
Directive: Keep src/cli/friendly-battle.ts facade-only; push future session/runtime complexity down into transport/runtime layers
Tested: npm test; npm run build; node --import tsx --test test/friendly-battle-cli.test.ts test/friendly-battle-local-harness.test.ts; git diff --check; LSP diagnostics on affected files; local help/alias-conflict quick checks
Not-tested: Manual multi-terminal run from two live Claude profiles after this wrapper change

* Establish repository CI evidence for ongoing friendly-battle work

The repository had no GitHub Actions workflow, which left PRs without
check-runs or commit statuses even when local verification passed.
This adds a minimal Node 22 CI pipeline that typechecks, runs tests,
and builds so future PRs surface machine-verifiable evidence directly
in GitHub.

Constraint: Repository currently targets Node >=22.0.0 and uses npm lockfile installs
Constraint: Keep CI minimal and aligned with existing package scripts only
Rejected: Add lint or matrix jobs now | repo does not define a lint script and extra jobs would add noise before baseline CI exists
Rejected: Add only test step | would miss type/build regressions already part of local verification expectations
Confidence: high
Scope-risk: narrow
Directive: Keep CI script list in sync with package.json; do not add non-existent repo scripts just for convention
Tested: npm run typecheck; npm test; npm run build; git diff --check; subagent code review
Not-tested: Remote GitHub Actions execution after push

* Keep friendly-battle CI green across shells and headless runners

The local friendly-battle tests were assuming zsh and prebuilt dist hook
artifacts, while audio playback could still surface async ENOENT failures
on CI hosts that do not ship desktop media players. This change switches
shell-based smoke tests to the platform default shell, runs the
session-start hook from source during tests, hardens detached audio spawns,
and gives the same-machine battle smoke a little more room under full-suite
load.

Constraint: GitHub Actions runners may not have zsh or desktop audio players
Constraint: npm test runs before npm run build in CI
Rejected: Install zsh and audio players in CI | would hide portability bugs in the repo
Rejected: Keep the 4s local battle timeout | remained flaky under full-suite load
Confidence: high
Scope-risk: narrow
Directive: Keep integration tests shell-agnostic and avoid dist dependencies in prebuild test paths
Tested: node --import tsx --test test/friendly-battle-local-harness.test.ts
Tested: node --import tsx --test test/friendly-battle-spike-cli.test.ts
Tested: node --import tsx --test test/session-start.test.ts
Tested: node --import tsx --test test/sfx.test.ts
Tested: npm test
Tested: npm run typecheck
Tested: npm run build
Not-tested: GitHub Actions rerun on the pushed commit

* Make same-network friendly battles publish a usable join address

Friendly battle local v1 started as a same-machine spike, but the current product goal is same-network instant battles between two Claude profiles. This change separates the host bind address from the guest-facing join address, threads that contract through the CLI, and reshapes the smoke tests so they keep exercising the printed command flow without relying on two heavyweight shell children racing each other.

The result is that hosts can listen on wildcard interfaces while still printing a concrete join command for the guest, and the surrounding tests better reflect the supported same-network flow.

Constraint: This feature must stay inside the existing Claude Code plugin repo without adding a separate always-on server component
Constraint: Friendly battle v1 still optimizes for ad-hoc matches, not cheat resistance, matchmaking, or reconnect recovery
Rejected: Keep a single --host flag for both bind and guest join address | breaks same-network use when the host listens on 0.0.0.0
Rejected: Keep two spawned shell children in spike smoke tests | too slow and flaky on headless CI runners
Confidence: high
Scope-risk: moderate
Directive: Do not collapse listen host and guest-facing join host back together unless same-network host UX is re-specified end-to-end
Tested: npm test
Tested: npm run typecheck
Tested: npm run build
Tested: Subagent code review + focused friendly-battle regression review
Not-tested: Real two-machine manual LAN run outside the automated harness

* Preserve a passing remote-battle checkpoint before team fanout

The single-owner loop was spending too much time compacting context instead of advancing the remaining two-machine product flow. This checkpoint captures the passing transport, snapshot, and CLI groundwork so OMX team workers can branch from a clean base and attack the next bounded gaps in parallel.

Constraint: omx team requires a clean leader workspace before it can create worker worktrees
Rejected: stash the worktree | would hide the current passing base from team workers
Confidence: high
Scope-risk: narrow
Directive: Treat this commit as the team launch pad; follow-up work should target authoritative repeated-turn flow and Claude Code-friendly UX only
Tested: node --import tsx --test test/friendly-battle-transport-spike.test.ts test/friendly-battle-local-harness.test.ts test/friendly-battle-spike-cli.test.ts test/friendly-battle-cli.test.ts test/friendly-battle-snapshot.test.ts test/friendly-battle-battle-adapter.test.ts; npm run typecheck
Not-tested: Cross-machine manual run across two physical hosts

* omx(team): auto-checkpoint worker-1 [1]

* omx(team): auto-checkpoint worker-2 [unknown]

* omx(team): auto-checkpoint worker-2 [unknown]

* omx(team): auto-checkpoint worker-1 [1]

* omx(team): auto-checkpoint worker-1 [1]

* omx(team): auto-checkpoint worker-2 [unknown]

* omx(team): auto-checkpoint worker-2 [unknown]

* Keep remote friendly battles event-driven and time-bounded

This switches the spike/local friendly-battle smoke flow from a one-off action exchange to the authoritative battle-event loop we actually need for remote play, and it tightens the guest join path so user-provided timeout-ms is respected instead of leaking through socket shutdown.

Constraint: Friendly battles must stay in-repo and Claude Code friendly without introducing a separate authoritative server stack yet
Constraint: Remote same-network play still needs deterministic CLI/test coverage before higher-level product UX work
Rejected: Leave the first-action smoke in place | no longer matched the authoritative event contract used by the adapter
Rejected: Depend on socket end/close after timeout | leaked well past timeout-ms in join failure scenarios
Confidence: high
Scope-risk: moderate
Reversibility: clean
Directive: Keep transport/API tests aligned with the authoritative battle_event and submit_choice flow; do not reintroduce action-exchange shims
Tested: npm run typecheck; node --import tsx --test test/friendly-battle-battle-adapter.test.ts test/friendly-battle-cli.test.ts test/friendly-battle-local-harness.test.ts test/friendly-battle-spike-cli.test.ts test/friendly-battle-transport-spike.test.ts
Not-tested: Real two-machine same-network manual smoke outside the local test harness

* Document the remaining friendly-battle product gap after handshake hardening

This records what the current remote snapshot handshake branch already
ships versus what is still missing before we can call the
same-network friendly battle flow a complete product experience.

The new roadmap note keeps the host/join surface, session foundation,
and transport hardening separate from the still-missing turn choice UX
so the next PR can stay focused.

Constraint: Keep this PR docs-only and describe the remaining gap in markdown
Rejected: Keep extending code in this branch | would blur the transport-hardening checkpoint
Confidence: high
Scope-risk: narrow
Directive: Update the gap note whenever product-facing friendly-battle UX lags behind transport/session groundwork
Tested: git diff --check; node --import tsx --test test/friendly-battle-cli.test.ts
Not-tested: cross-machine manual LAN smoke for this docs-only commit

* omx(team): auto-checkpoint worker-1 [1]

* omx(team): auto-checkpoint worker-1 [1]

* omx(team): auto-checkpoint worker-1 [1]

* omx(team): auto-checkpoint worker-2 [unknown]

* omx(team): auto-checkpoint worker-1 [1]

* omx(team): auto-checkpoint worker-1 [1]

* omx(team): auto-checkpoint worker-1 [1]

* omx(team): auto-checkpoint worker-1 [1]

* Lock repeated-turn CLI gaps behind failing local harness coverage

These tests capture the missing interactive same-machine battle loop so follow-up implementation can replace auto-play behavior without losing the current regression signals.

Constraint: Task scope is test-only; production CLI behavior must remain untouched in this slice
Rejected: Implement stdin-driven CLI now | task explicitly requires failing tests first
Confidence: high
Scope-risk: narrow
Directive: Keep these assertions failing until repeated move, forced switch, and surrender prompts are wired to stdin
Tested: node --import tsx --test test/friendly-battle-local-harness.test.ts --test-name-pattern "(waits for repeated move input|waits for a forced replacement choice|accepts an explicit surrender command)"
Tested: npm run typecheck
Tested: LSP diagnostics for test/friendly-battle-local-harness.test.ts
Not-tested: Full repository test suite

* omx(team): auto-checkpoint worker-2 [unknown]

* omx(team): auto-checkpoint worker-1 [1]

* omx(team): auto-checkpoint worker-1 [1]

* omx(team): auto-checkpoint worker-1 [1]

* omx(team): auto-checkpoint worker-1 [1]

* omx(team): auto-checkpoint worker-1 [1]

* Lock repeated-turn CLI gaps behind failing local harness coverage

These tests capture the missing interactive same-machine battle loop so follow-up implementation can replace auto-play behavior without losing the current regression signals.

Constraint: Task scope is test-only; production CLI behavior must remain untouched in this slice
Rejected: Implement stdin-driven CLI now | task explicitly requires failing tests first
Confidence: high
Scope-risk: narrow
Directive: Keep these assertions failing until repeated move, forced switch, and surrender prompts are wired to stdin
Tested: node --import tsx --test test/friendly-battle-local-harness.test.ts --test-name-pattern "(waits for repeated move input|waits for a forced replacement choice|accepts an explicit surrender command)"
Tested: npm run typecheck
Tested: LSP diagnostics for test/friendly-battle-local-harness.test.ts
Not-tested: Full repository test suite

* omx(team): auto-checkpoint worker-2 [unknown]

* Lock repeated-turn CLI gaps behind a standalone interaction test lane

This keeps the repeated-turn UX coverage in a separate local-CLI stdin/stdout file so the follow-up implementation can land without colliding with other worker edits in the local harness suite.

Constraint: Leader refinement required a separate file instead of test/friendly-battle-local-harness.test.ts
Constraint: Task scope remains test-only; production CLI behavior must stay unchanged here
Rejected: Keep harness-file coverage from the earlier slice | conflicts with worker-1 ownership of that file
Rejected: Implement interactive stdin handling now | task explicitly requires failing tests first
Confidence: high
Scope-risk: narrow
Directive: Preserve this file as the dedicated repeated-turn CLI interaction regression lane until stdin-driven prompts are implemented
Tested: node --import tsx --test test/friendly-battle-local-cli-interaction.test.ts
Tested: npm run typecheck
Tested: LSP diagnostics for test/friendly-battle-local-cli-interaction.test.ts
Not-tested: Full repository test suite

* omx(team): auto-checkpoint worker-1 [1]

* omx(team): auto-checkpoint worker-1 [1]

* omx(team): auto-checkpoint worker-1 [1]

* omx(team): auto-checkpoint worker-1 [1]

* Preserve repeated-turn prompts for piped local battle sessions

Claude Code and the local CLI harness drive friendly battles through piped stdin rather than an interactive TTY, so the local battle loop now distinguishes prompt-capable pipes from explicit auto-choice runs. The supporting regression coverage also fixes the harness host stdin setup and locks in that force-prompt mode outranks auto-choice mode when both env flags are present.

Constraint: Claude Code and test harnesses commonly drive stdin through pipes instead of TTY sessions
Constraint: Same-machine smoke coverage still needs an explicit auto-choice path to finish without manual input
Rejected: TTY-only prompting | broke repeated-turn stdin flows under Claude Code and child-process tests
Rejected: Always prompt on every pipe | would hang auto smoke runs that intentionally provide no interactive input
Confidence: medium
Scope-risk: narrow
Directive: Keep TOKENMON_FORCE_PROMPTS precedence above TOKENMON_AUTO_CHOICES unless the CLI transport contract changes together with tests
Tested: npm run typecheck; node --import tsx --test test/friendly-battle-local-cli-interaction.test.ts; timeout 60s node --import tsx --test test/friendly-battle-local-harness.test.ts
Not-tested: Non-Socket stdin implementations on platforms that do not expose pipe-backed stdin as a Socket

* Seed a tokenmon profile for friendly-battle spike CLI tests

The runJoin CLI now loads the current profile and builds a party snapshot
before announcing STAGE: connected, so CI runners without persisted config
hit "must include at least one pokemon" and fail 5 spike tests. Point
every spawned CLI at a dedicated CLAUDE_CONFIG_DIR seeded with a single
pokemon so the tests no longer depend on the host machine's tokenmon data.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Document the PvP PR stack and PR43 TDD plan after handshake hardening

Captures the foreground-blocking architecture decision (no daemon), the
PR43-PR47 stack that turns /tkm:friendly-battle into a real slash-command
surface, and a task-by-task TDD plan for PR43 (the turn driver CLI). These
docs seed the next stacked branch and give reviewers of #42 visibility into
what comes next.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add disk-backed friendly-battle session store

Observation state only — writes to
~/.claude/tokenmon/<gen>/friendly-battle/sessions/<id>.json so SKILL.md
and the statusbar can inspect host/guest driver state between slash
command invocations. Not a rendezvous channel.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add gym-compatible friendly-battle turn JSON formatter

Matches the JSON shape that skills/gym/SKILL.md already parses (sessionId,
role, phase, status, questionContext, moveOptions, partyOptions,
animationFrames, currentFrameIndex) so the upcoming friendly-battle SKILL.md
can reuse gym's parsing rules verbatim.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Scaffold friendly-battle-turn CLI skeleton and arg parser

Usage and unknown-subcommand handling only. Each subcommand stub throws
"not implemented" — Tasks 4-8 land the actual host listen, guest join,
action/refresh/status paths one at a time.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Implement friendly-battle-turn --init-host waiting_for_guest path

Reuses createFriendlyBattleSpikeHost, writes phase=waiting_for_guest via
the session store, emits PORT on stderr for integration tests, and
transitions to phase=battle or phase=aborted depending on whether
waitForGuestJoin resolves before --timeout-ms.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Implement friendly-battle-turn --init-join handshake

Loads the current guest profile, builds a party snapshot, and connects
to an --init-host process through the existing tcp-direct transport.
Integration test spawns both processes in parallel with a seeded
CLAUDE_CONFIG_DIR so the handshake completes against real TCP.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Rescope PR43 to the handshake driver after Task 5

Tasks 6-9 (action / refresh / status / deterministic gating) are moved to
PR44 because they only become testable once a SKILL.md drives them inside
an AskUserQuestion turn loop. PR43's final shape is the session store,
turn JSON formatter, CLI skeleton, --init-host waiting path, and the
--init-join handshake integration test.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Harden friendly-battle-turn driver against reviewer-flagged inputs

Addresses the security + architect + code-reviewer feedback on PR #43:
- session-store.ts now validates sessionId + generation segments against
  a safe-ID pattern on both write and read paths, and runtime-guards the
  parsed shape before returning a record, closing the HIGH path-traversal
  finding from security review.
- friendly-battle-turn.ts validates --port / --timeout-ms as non-negative
  integers, --session-code / --generation against strict patterns, and
  sanitizes --player-name to block control-char injection into the JSON
  envelope's questionContext.
- parseArgs now runs with strict: true, so flag typos surface as a clean
  REASON line instead of a silent missing-required-flag later.
- runInitJoin now mirrors runInitHost's try/catch, writing phase=aborted
  on failure and emitting an aborted envelope on stdout so the upcoming
  SKILL.md has a single stdout JSON contract for both success and
  failure paths.
- runInitHost tracks currentStage mutably so a failure between handshake
  and startBattle labels FAILED_STAGE accurately instead of always
  reporting waiting_for_guest.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Gate friendly-battle spike CLI wall-clock timing guard to local dev

The "honors join timeout-ms while waiting for hello acknowledgement" test
asserts elapsedMs < 900 to catch a bug where the CLI silently falls back
to the 1s default timeout. PR43's new turn-driver tests spawn several tsx
child processes in parallel, and on GitHub Actions runners that extra CPU
load drifts a 200ms setTimeout past 1.4s via scheduler starvation — the
CLI is still honoring the flag, it just can't fire the callback in time.

Keep the wall-clock guard as a local dev check so developers still catch
the "default leaks" regression. On CI, rely on the RETRY_HINT regex and
exit-code assertions that already verify the flag was received and the
join failed for the right reason.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Plan PR44 with a minimal daemon reversal for the turn loop

PR43 shipped on the assumption that /tkm:friendly-battle would use gym's
foreground-blocking one-shot CLI model. Execution revealed that does not
generalize to networked play: tcp-direct.ts exposes a fully live-socket
API (waitForGuestJoin, markHostReady, waitUntilCanStart, startBattle,
waitForGuestChoice, sendBattleEvents, submitChoice, waitForBattleEvent)
and a TCP file descriptor cannot survive across tsx invocations. Gym's
pattern works because its state is purely local — friendly-battle's is
not.

This plan reverses the roadmap's "no daemon" decision for PR44 only:

- --init-host and --init-join fork a detached daemon child that holds
  the transport and battle state
- The daemon exposes a local UNIX socket at $CLAUDE_CONFIG_DIR/.../<id>.sock
- Per-action subcommands (--wait-next-event, --action move:N, --status)
  are one-shot tsx calls that open the socket, send one JSON command,
  read one JSON response, close — same ergonomics as gym from the
  skill's perspective
- Session store gains daemonPid + socketPath fields; PR43's existing
  reap infra handles orphan cleanup

The user-visible "it feels like one contiguous battle session" model
from the roadmap is preserved — the daemon is an implementation detail
invisible to /tkm:friendly-battle users, same as a shell pipe.

Plan lists 11 tasks from session-store extension through skill contract
test. Visual QA merge gate is called out explicitly — autopilot will
land the code and open a draft PR; the human operator runs visual-verdict
before flipping from draft to ready.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Extend friendly-battle session record with daemonPid and socketPath

PR44 introduces a detached daemon per side to hold the TCP socket across
SKILL.md subcommand calls. The session record now stores the daemon's
PID and its local UNIX socket path so per-action subcommands
(--wait-next-event, --action, --status) can find the right daemon.

Shape guard rejects records missing either field so an old on-disk record
from PR43 won't confuse a PR44 client.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add friendly-battle daemon IPC protocol types

Types-only module shared by daemon.ts (server) and daemon-ipc.ts (client)
so the IPC shape cannot drift. Line protocol = one JSON message per line
terminated by \n. PR44 only needs the move-action variant; switch and
surrender variants ship with PR45.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add UNIX-socket IPC server/client for friendly-battle daemon

One request per connection, one response, then close — same ergonomics
as gym's one-shot CLI pattern from SKILL.md's perspective. The daemon
uses createDaemonIpcServer; --wait-next-event / --action / --status use
sendDaemonIpcRequest. Handler errors are surfaced as op=error responses
so the client never crashes on daemon-internal failures.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add friendly-battle daemon long-lived event loop

Holds the TCP transport and battle runtime in a detached child process
so per-action subcommands (--wait-next-event / --action / --status) can
be one-shot tsx calls that talk to the daemon via a local UNIX socket.
Host daemon owns the authoritative battle-adapter runtime; guest daemon
only forwards events from TCP to its local queue.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Add end-to-end turn loop integration test across two daemons

Spawns host and guest daemons with real TCP transport and real UNIX
socket IPC. Drives a full move-only turn until battle_finished, then
asserts both daemons exit cleanly. Proves the daemon + battle-adapter
+ transport stack is wired end-to-end.

Also fixes daemon shutdown to drain the event queue before closing the
IPC server, so battle_finished events are delivered to callers before
the socket goes away.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fork the friendly-battle daemon from --init-host and --init-join

The init CLI subcommands no longer run the TCP transport inline. They
spawn src/friendly-battle/daemon.ts as a detached child, read the
DAEMON_READY signal from its stdout, persist the daemon PID and UNIX
socket path to the session record, emit the first JSON envelope, and
exit. Per-action subcommands in Task 7 will talk to the daemon via
daemon-ipc instead of reaching across a live TCP socket.

The existing handshake test is updated to match the new lifecycle
(parent exits early; daemon transitions the session record
asynchronously). Timeout test is relaxed to accept either
waiting_for_guest or aborted, since the daemon handles the timeout
after the parent is gone.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Implement friendly-battle-turn --wait-next-event / --action move / --status

Per-action CLI subcommands talk to the forked daemon via the UNIX socket
stored in the session record, so SKILL.md can invoke them as one-shot
tsx calls — same ergonomics as gym. --action move:N translates the
1-based SKILL.md token into the 0-based index the battle adapter uses.
--action switch:N and --action surrender intentionally error with a PR45
deferred message. --status falls back to the persisted record envelope
when the daemon is dead, so the skill can always render something.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add skills/friendly-battle/SKILL.md and contract test

First playable /tkm:friendly-battle open / join / status surface. The
skill dispatches to friendly-battle-turn.ts --init-host / --init-join
to fork the daemon, polls --wait-next-event for turn state, and drives
move selection through AskUserQuestion. Switch / surrender / leave
are intentionally deferred to PR45/46 — the skill shows a "not yet
supported" message for those inputs.

The contract test statically parses SKILL.md and asserts every bash
block's --flag matches a CLI flag the driver actually supports, and
every --action token matches the PR44 move-only scope. Catches skill
rot before it ships.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Document the PR44 daemon reversal in the friendly-battle roadmap

Appends §8 "Architecture revision (PR44 daemon reversal)" explaining
why the minimal daemon model was necessary once PR44 hit a live TCP
socket that cannot survive across tsx invocations, and why that does
not actually violate the spirit of the original "no daemon" decision
(which targeted user-visible persistence/reconnect semantics, none of
which PR44 adds). Preserves §7 as a historical marker of the PR43
position.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Stabilize PR44 test suite against concurrency + daemon teardown races

- node --test now runs with --test-concurrency=1 so daemon-spawning
  tests in different files do not starve each other's handshake
  timings under parallel CPU pressure
- killAllDaemons in friendly-battle-turn-driver.test.ts now waits for
  each SIGTERM'd PID to actually exit (and SIGKILL after 3s) before
  returning, so the afterEach rmSync does not race the daemon's socket
  unlink + session record write and fail with ENOTEMPTY

3 consecutive full-suite runs green (1192/1192 each) with the fixes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Apply PR44 reviewer fixups: reap daemon PID, socket ACL, DoS cap, path validation

Addresses architect / security / code-reviewer feedback:

- session-store reap now checks record.daemonPid (living daemon) instead
  of record.pid (CLI parent that always exits immediately after fork)
- session-store interface promotes daemonPid + socketPath to required;
  the shape guard already enforced this at read time
- isValidRecord now bounds socketPath inside the generation's sessions
  dir with a basename-matches-sessionId check (security M2 / code-review
  major)
- daemon-ipc chmods the UNIX socket to 0600 after listen, caps each
  incoming line at 64 KiB, and enforces a 5s idle timeout (security H1
  + M1)
- daemon fork options move from argv --options-json to env var
  TKM_FB_OPTIONS_B64 so the session code is not visible via ps (M3)
- daemon shutdown drains the event queue then arms a sentinel so late
  wait_next_event callers get a clean finished envelope instead of a
  silent timeout
- friendly-battle-turn --refresh wiring removed (was pure dead stub)
- handshake timeout in runInitHost/runInitJoin now unrefs the child
  before SIGTERM so the process group does not pin the parent event
  loop
- killAllDaemons in the turn-driver test polls for SIGKILL completion
  up to 500ms instead of waiting a fixed 100ms, reducing teardown
  ENOTEMPTY risk on slow CI

Deferred to PR45: parent-crash-between-fork-and-record race, SKILL.md
bash arg-quoting hardening, environment whitelist.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Close the drain-sentinel arming race flagged by the Codex review

The previous clean-shutdown flow drained the event queue, then armed
queueClosedEnvelope. A wait_next_event call arriving between "queue
emptied" and "sentinel armed" would enter shift() on an empty queue and
block until its own timeout fired.

Fix is twofold:
1. After draining, explicitly fail() the event queue so any already-
   blocked shift() rejects with a finite error.
2. The wait_next_event IPC handler now catches that rejection and, if
   the sentinel has been armed, returns the sentinel envelope instead
   of propagating the error.

Ordering stays drain-first-then-arm so real buffered events are still
delivered to legitimate waiters before the sentinel takes over.

Full suite: 1195/1195 pass across two consecutive runs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Plan PR45 fainted forced-switch + surrender activation

Most of the machinery for switch / surrender / fainted_switch is
already wired through battle-adapter.ts and daemon.ts eventStatus.
PR45 just exposes it at the user-input surface: DaemonAction union,
serializeDaemonAction cases, runAction parser, SKILL.md flows, and
two integration tests (forced switch after faint, explicit surrender).
Scope is much smaller than PR44 — roughly 250 LOC of new code + tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add switch and surrender variants to friendly-battle DaemonAction

* Serialize switch and surrender DaemonActions for the TCP transport

* Zero out moveOptions on friendly-battle fainted_switch envelopes

* Implement friendly-battle-turn --action switch:N and --action surrender

* Allow switch and surrender tokens in the friendly-battle skill contract test

* Add switch menu, surrender confirm, and forced-switch flows to SKILL.md

* Add integration test for explicit friendly-battle surrender end-to-end

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Add integration test for fainted forced-switch end-to-end

Marked .todo() — the host daemon's turn loop (daemon.ts) unconditionally
waits for both sides' actions on every iteration. During a single-side
awaiting_fainted_switch phase, submitFriendlyBattleChoice rejects the
non-waiting side with "not waiting for <role>", causing the daemon to error.
The fix requires a source change to daemon.ts to check getWaitingFor before
calling Promise.all. Detailed analysis and proposed fix are in the test file.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix friendly-battle daemon host turn loop to honor fainted-switch waiters

The host turn loop unconditionally awaited both localActionQueue.shift
and host.waitForGuestChoice every iteration, which worked for normal
turns where both roles always submit, but threw "not waiting for X"
during awaiting_fainted_switch where only the fainted side needs to
submit.

Daemon now consults getFriendlyBattleWaitingForRoles (newly exported
from battle-adapter) and skips Promise.all branches for non-waiting
roles. The surrender and normal-turn paths are unchanged — both sides
still submit per turn in those cases.

The guest daemon startup path was also fixed to eagerly drain the TCP
init events (battle_initialized + choices_requested) from the host
before entering the outer turn loop. Previously these events sat in
the TCP buffer and interleaved with real turn-resolution events when
the guest's inner pump ran for the first time, causing the
awaiting_fainted_switch choices_requested to be missed on the guest
side.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Replace the PR45 fainted-switch .todo skeleton with a real end-to-end test

Spawns host + guest daemons with separate CLAUDE_CONFIG_DIR per role.
Host leads with a lv80 Turtwig (starter) and lv5 backup; guest leads
with a lv1 Cyndaquil (guaranteed KO by Tackle) and lv5 Chikorita backup.

Drives both sides through the initial move, observes the fainted_switch
envelope on the KO'd side (guest), submits a switch:1 action on the
fainted side, and asserts the battle continues with select_action.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Use a unique tmp suffix in writeFriendlyBattleSessionRecord

CI surfaced a rename ENOENT on the PR45 surrender test: the daemon and
the test helper were both writing the same session record concurrently,
sharing the path.tmp filename, so the second renameSync ran into a
vanished tmp after the first had already renamed it to the final
destination.

Adds process-pid + randomUUID to the tmp suffix so no two writers ever
collide on the same intermediate file. Orphaned tmps from a failed
write are now unlinked best-effort in the finally block. Also closes
PR44 security reviewer L3 (tmp-file leak on crash).

1197/1197 full suite pass locally.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Plan PR46 /tkm:friendly-battle leave using the existing disconnect path

Avoids touching tcp-direct.ts by routing voluntary leave through the
existing EOF-driven shutdown: the leaving daemon pushes a synthetic
battle_finished{reason:'cancelled'} locally, closes the transport,
acks the IPC leave request, and shuts down. The peer's daemon catches
the transport EOF in its turn loop and pushes a synthetic
battle_finished{reason:'disconnect'} into its own local queue so the
peer skill's next wait_next_event returns a clean envelope instead of
timing out.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Extend friendly-battle daemon protocol with a leave request variant

Add { op: 'leave' } to DaemonRequest union and extend the protocol
round-trip test to cover the new variant.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Implement /tkm:friendly-battle leave subcommand and peer disconnect eventing

- daemon.ts: add 'leave' IPC handler — marks session aborted, closes TCP
  transport (so peer gets EOF), returns ack, then calls shutdown(0,'finished')
  via setImmediate so the response flushes first
- daemon.ts: add transportClose ref so ipcHandler can tear down TCP on leave
- daemon.ts: both host+guest catch blocks now push a synthetic
  battle_finished{winner:null,reason:'disconnect'} event before shutdown so
  any pending wait_next_event on the leaving side resolves cleanly
- friendly-battle-turn.ts: add --leave subcommand (runLeave) — reads session
  record, skips to frozen envelope if daemon already gone, otherwise sends
  {op:'leave'} via IPC and prints the ack envelope
- SKILL.md: add Step 9 (leave flow), update Step 0 dispatch, Step 2 turn loop
  aborted-phase handling, Step 8 help text, and usage table
- test/friendly-battle-daemon-leave.test.ts: new end-to-end integration test
  (1 test case, 1198 total)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Address Codex review findings on the PR46 leave flow

Q4 fix (major): the daemon's battle_finished envelope now emits a
distinct questionContext for voluntary leave (cancelled), peer
disconnect (disconnect), and normal win/loss, and eventStatus maps
cancelled and disconnect to 'aborted' instead of victory/defeat. The
skill's Step 2 aborted branch can now key on questionContext to
distinguish "You left the battle." from "Opponent left the battle."
without sniffing stderr or the reason field.

Q5 fix (minor): the leave test's afterEach now does SIGTERM → 500ms
poll → SIGKILL → 200ms wait instead of naked SIGTERM, mirroring PR44's
killAllDaemons pattern. Closes the tmp-dir / socket teardown race that
the naked SIGTERM left open on slow CI runners.

Full suite: 1198/1198 pass after the fixes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add local two-terminal visual QA guide for the friendly-battle PR stack

Friendly step-by-step walkthrough so anyone can reproduce the PR44–PR46
visual QA on a single machine without needing two physical hosts.
Covers:

- What each PR in the stack (#40 → #46) adds and what to eyeball
- How to sync the tkm plugin install to feat/friendly-battle-pvp-leave
  via a plain git checkout when the plugin clone is ThunderConch/tkm
- How to use an isolated CLAUDE_CONFIG_DIR (~/.claude-fb-guest) for the
  second Claude Code session so the two terminals don't race over the
  same tokenmon state
- Per-PR screenshot checklist (basic turn loop, forced switch, surrender
  confirm, leave / opponent-left)
- Rollback instructions (git checkout master + ~/.claude-fb-guest
  cleanup)
- Troubleshooting the 6 most common stuck spots

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Clear the daemon IPC idle timeout once a request is parsed

The PR44 reviewer fixup added a 5-second idle timeout on the daemon's
UNIX socket to guard against DoS-by-stalled-clients. The guard was
applied to the whole socket lifetime, which also covered the handler's
execution window — so wait_next_event long polls (default 60_000ms)
were being killed at the 5s mark, dropping the response and breaking
the real /tkm:friendly-battle open flow as soon as the host started
polling for a guest.

Fix: keep the 5s idle timeout for the pre-request phase (still catches
a client that connects and never writes), but clear it via
socket.setTimeout(0) as soon as the request line is parsed. Long
handlers can now legitimately block for the full timeoutMs the client
requested without being reaped.

Two regression tests added:
- handler that blocks 6s still delivers its response (would have
  caught this bug before merging the reviewer fixup)
- client that connects without writing is still destroyed in ~5s
  (proves the DoS guard still protects the pre-request phase)

Full suite: 1200/1200 pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix 0-vs-1-based move/party index mismatch + richer questionContext

Two bugs visual QA surfaced on the PR46 stack:

1. Daemon emitted moveOptions / partyOptions with 0-based indexes from
   Array#map, while SKILL.md and the CLI action parser expect 1-based
   indexes (gym contract). The skill would render "0. 파동탄" and dispatch
   --action move:0, which runAction's /^move:([1-4])$/ regex rejected
   as "unknown action token", leaving the daemon stuck mid-turn.
   Fix: daemon now emits index + 1 for both moveOptions and partyOptions.
   The CLI still subtracts 1 before forwarding to the battle adapter, so
   the wire format (0-based) is unchanged.

2. questionContext only had the turn headline, so the AskUserQuestion
   prompt showed no battle state — users couldn't see opponent HP or
   their own current HP without squinting at the status line.
   Fix: new buildBattleContext helper renders a gym-style line beneath
   the headline:
     ⚔️ 상대 <name> Lv.L HP:cur/max | 내 <name> Lv.L HP:cur/max
   On the host side the battle-adapter runtime has both teams so both
   sides render. On the guest side there is no runtime yet so we only
   render the local pokemon from ownSnapshot with a "(상대 HP는 다음
   턴 결과에서 확인)" hint.

Full suite: 1200/1200 pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add per-session crash log to the friendly-battle daemon

Visual QA on the live PR46 stack hit a daemon death with no observable
stack trace. The host CLI destroys child.stdout/stderr right after
DAEMON_READY (so the parent can exit), which means any subsequent
process.stderr.write or uncaughtException in the daemon was silently
discarded — making post-mortem impossible.

Fix: open a per-session append-mode log file at
$CLAUDE_CONFIG_DIR/tokenmon/<gen>/friendly-battle/sessions/<id>.crash.log
during runDaemon's startup. Mirror process.stderr.write into the file,
and install uncaughtException + unhandledRejection handlers that flush
the stack to disk before exit. Logging is best-effort and never crashes
the daemon itself (every fs call is wrapped).

Next time a daemon dies during /tkm:friendly-battle the operator can
just `cat` the crash log to see the actual error.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Stop the daemon stderr wrapper from EPIPE-killing itself + use a
30-minute turn-loop timeout instead of the init handshake timeout

Crash logs from the live PR46 visual QA showed two real bugs that the
612d31c crash-log feature actually surfaced:

1. The stderr.write wrapper still forwarded to the original stream.
   The parent CLI destroys child.stderr right after DAEMON_READY, so
   any subsequent process.stderr.write triggered an async 'error' event
   on the dead pipe → uncaughtException → daemon dead. This made the
   crash-log feature actively worse: every legitimate "daemon host
   error: …" stderr write killed the daemon mid-catch before its
   shutdown path could update the session record. Fix: the wrapper now
   ONLY writes to the crash log file and never touches the original
   stream.

2. The host/guest turn loops used the daemon's options.timeoutMs (the
   init handshake budget, ~30s in real use) for both localActionQueue
   shifts and TCP waitForGuestChoice / waitForBattleEvent calls. So a
   user thinking about a move for >30s had their daemon time out and
   die mid-battle. Fix: introduce TURN_LOOP_TIMEOUT_MS = 30 minutes,
   used for all four in-loop wait sites. Tests can override it via
   the TKM_FB_TURN_TIMEOUT_MS env var so leave/disconnect specs still
   resolve in seconds.

Init transport waits (waitForGuestJoin, waitUntilCanStart, eager event
drain right after waitForStarted) keep using the CLI's --timeout-ms so
the handshake still aborts quickly when nobody joins.

Full suite: 1200/1200 pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Push battle state to the wire so guests render real HP/PP, and stop
showing the forced-switch menu to spectator daemons

Visual QA on a real two-terminal battle hit three real bugs that
shared a root cause: the guest daemon was making display decisions
locally instead of trusting the host's battle-adapter runtime.

1. Guest's HP showed "100/100" forever because buildBattleContext was
   reading the species base stat from the frozen snapshot instead of
   the level-scaled current/max HP.
2. Guest's move PP never decreased after using a move because nothing
   was decrementing the snapshot.
3. When one Dialga fainted, BOTH terminals popped the forced-switch
   menu because daemon eventStatus mapped phase=awaiting_fainted_switch
   to status=fainted_switch without checking whether the local role
   was actually in waitingFor.

Architecture fix: host is now the single source of truth.

- contracts.ts: FriendlyBattleChoicesRequestedEvent gains an optional
  liveState field carrying both teams' active pokemon (name, level,
  hp, maxHp, fainted, moves with currentPp/maxPp/disabled) plus the
  full party (name, level, hp, maxHp, fainted).
- battle-adapter.ts: new buildFriendlyBattleLiveBattleState helper +
  populates liveState in all three choices_requested emit sites
  (initial battle setup, normal turn-end, fainted_switch).
- daemon.ts: new buildEnvelopeFieldsFromLiveState helper renders
  envelope display fields straight from the host-authored liveState.
  eventToEnvelopeFields prefers liveState when present and falls back
  to the legacy runtime/snapshot path only for older synthetic test
  events. eventStatus now keys fainted_switch / select_action on
  whether the local role is in event.waitingFor — spectator side gets
  'ongoing' so the skill loops back to wait_next_event instead of
  prompting for a duplicate switch.

Test updates:
- friendly-battle-battle-adapter.test.ts: stripLiveState helper so
  the structural deepEqual assertions stay focused on the wire-shape
  contract; liveState payload is exercised separately via daemon
  integration tests.
- friendly-battle-daemon-fainted-switch.test.ts: drainUntilFaintedSwitch
  now resolves to false on inner timeout (the spectator side never
  reaches fainted_switch under the new role-aware behavior).
- friendly-battle-daemon-ipc.test.ts: idle-guard regression test's
  lower-bound elapsed window relaxed from 4500ms to 4000ms so node:test
  scheduler jitter under parallel CPU load doesn't flake.

Full suite: 1200/1200 pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Send turn events over TCP before pushing to local queue (Codex Q1)

Codex adversarial review flagged that the host turn loop was pushing
resolved events to localEventQueue BEFORE calling
host_transport.sendBattleEvents(). The window meant the host skill's
wait_next_event could surface the next choices_requested and prompt
the user for a fresh action while the guest still hadn't received the
previous turn over TCP. The guest's stale in-flight choice would then
hit the battle-adapter's "not waiting for X" reject path.

Swapping the order makes the guest no later than the host on event
arrival — TCP send commits to the kernel buffer before the local
push, so loopback delivery is effectively atomic with the local push.

Q2 (spectator wait_next_event blocking) verdict was PASS — spectator
unblocks via the next host-pushed event, peer-disconnect synthetic
event, or the 30-min queue timeout.
Q3 (liveState host/guest mapping) verdict was PASS — internally
consistent player→host / opponent→guest across battle-adapter,
daemon, contracts.

Full suite: 1200/1200 pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Run guest as two parallel coroutines + localize names per client locale

Two real bugs found during live visual QA:

1. Guest daemon's outer loop awaited localActionQueue.shift FIRST and
   only then pumped TCP events. So when the host did anything that
   didn't require guest input (forced switch, etc.), the host's TCP
   events sat in the buffer indefinitely while guest waited for a
   local action that would never come. Symptom: "host 쪽 포켓몬
   교체하면 바로 guest 쪽으로 신호가 안감, guest 는 계속 대기만 함".

   Fix: split the guest path into two independent coroutines:
   - tcpPump continuously drains incoming battle events into the
     local event queue regardless of action timing
   - actionPump continuously pulls local actions and submits them
     via TCP regardless of incoming events
   They share a battleFinished flag so actionPump exits cleanly via
   localActionQueue.fail() when tcpPump sees battle_finished.

2. liveState was carrying the host's pre-resolved name strings
   (displayName, nameKo) so guests rendered HOST locale names even
   for their own pokemon and moves. Fix:
   - contracts.ts now also carries moveId on each move entry and
     pokemonId on the active pokemon + each party entry
   - battle-adapter.ts populates the IDs from BattlePokemon.id and
     BattleMove.data.id
   - daemon.ts new localizeMoveName / localizePokemonName helpers
     consult getLoadedMovesDB / getPokemonDisplayName + getLocale to
     render in the LOCAL client's language; legacy wire name is
     preserved as a fallback when ID lookup misses

Full suite: 1200/1200 pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Speed up friendly-battle CLI calls via dist/ helper + recommend lighter model

bin/run-friendly-battle-turn.sh prefers dist/cli/friendly-battle-turn.js
when present (saves ~700ms tsx cold start per per-action call) and falls
back to tsx on src for dev iteration.

SKILL.md now invokes the helper instead of tsx-resolve directly, and the
open/join flow tells the user they can drop to /model haiku or sonnet
during the battle since turn dispatch is mostly mechanical.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Address PR46 Codex review findings (H1/H2/M1/L1) + recommend sonnet

H1 (HIGH): Guest tcpPump now routes battle_finished through eventStatus()
so peer disconnect / voluntary leave (winner=null, reason='disconnect'|
'cancelled') maps to 'aborted' instead of being silently downgraded to
'defeat'. The leave integration test is tightened to require status==
'aborted' on the non-leaving side.

H2 (HIGH): shutdown(exitCode!=0) now arms queueClosedEnvelope BEFORE
failing localEventQueue, so a wait_next_event caller already blocked in
shift() falls through to the sentinel path in the IPC catch branch and
receives an aborted envelope instead of a generic 'handler_error'.

M1 (MEDIUM): The queueClosedEnvelope fallback message is now derived
from record.status (victory / aborted / else), so a voluntary leave no
longer flips "You left the battle." into a contradictory "You lost!"
on a subsequent poll. Aborted states render as "Battle ended."

L1 (LOW): parseCliOptions now re-validates sessionId / generation /
sessionCode against the safe-id regex after decoding TKM_FB_OPTIONS_B64,
so launching the daemon directly with a crafted env can't slip a bad
segment into socket / crash-log paths.

Also: SKILL.md now recommends /model sonnet instead of haiku. Haiku is
too weak to keep the AskUserQuestion loop stable during a battle.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix three guest-render bugs that broke real-asset visual QA

1) DAEMON_ENTRY now resolves daemon.js when running from dist/ (plain
   node) and daemon.ts when running under tsx. The old hardcoded
   .ts path meant the compiled CLI spawned a nonexistent file, forcing
   users back onto the slow tsx path or crashing outright. daemonSpawnArgs()
   also drops the `--import tsx` shim for the compiled case.

2) DEFAULT_PLUGIN_ROOT in friendly-battle/snapshot.ts now walks up from
   import.meta.url until it finds package.json. The old `../..` hop
   pointed at `dist/` in compiled form, so loadMovesData() silently
   failed to find data/moves.json, the moves DB stayed null, and
   localizeMoveName / localizePokemonName fell back to English wire
   names even for Korean clients. Walking to package.json handles both
   src/ and dist/ layouts without hardcoding either tree.

3) battle_initialized and turn_resolved events now carry liveState from
   the host's battle-adapter runtime. Guest daemons render these events
   via buildEnvelopeFieldsFromLiveState instead of falling back to
   buildBattleContext, which previously showed species-base HP (100/100
   for Dialga) from ownSnapshot.baseStats.hp. The test stripLiveState
   helper now strips liveState from all three event types; three
   battle-adapter assertions that compared raw turn_resolved events are
   updated to use the strippers.

Net effect: running /tkm:friendly-battle from the compiled dist/ now
shows real HP and locale-correct names on both sides. The battle
state flow is fully host-authoritative for every event that carries
state, matching the "host computes, guest receives" architecture.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix PLUGIN_ROOT in dist/ + graceful end-of-battle envelope

core/paths.ts PLUGIN_ROOT is the canonical data-root used by pokemon-data
(species names), battle-setup (move names), sprites, cries, and i18n
dictionaries. The old `join(import.meta.dirname, '..', '..')` pointed at
`dist/` when compiled, so every data file load silently fell through to
empty defaults — which is why Pokemon species names rendered as raw IDs
or English even for Korean clients. Now walks up from the module
directory until a package.json is found, matching the snapshot.ts fix
from 740690f so both src/ (tsx) and dist/ (node) layouts work.

friendly-battle-turn.ts --wait-next-event now survives ENOENT /
ECONNREFUSED on the daemon socket. When the host daemon exits cleanly
after the final turn, the skill's next poll used to crash with a socket
connect error; the skill then hallucinated a plausible-sounding Korean
recap instead of displaying the actual record. The CLI now reads the
frozen session record from disk, synthesizes a terminal envelope with
"You won!" / "You lost!" / "Battle ended." based on record.status, and
writes it to stdout so the skill's turn-loop can exit cleanly. Matches
the "status never fails" guarantee used by the --status subcommand.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Initialize locale in friendly-battle daemon subprocess

The daemon process is a standalone node entry spawned by the CLI and never
called initLocale(), so i18n/index.ts stayed at its default currentLocale='en'.
Every getPokemonName / getGameI18n / localizeMoveName / localizePokemonName /
t() lookup returned English even when the user's tokenmon config was 'ko',
which is why species and move names kept rendering as "Dialga / Aura Sphere /
Roar of Time" instead of "디아루가 / 파동탄 / 시간의포효" after the PLUGIN_ROOT
fix in 0389713 already made the i18n JSON files loadable.

runDaemon() now reads the global config and calls initLocale() once at
startup, mirroring the pattern used by cli/battle-turn.ts main(). Wrapped
in try/catch so a missing or corrupt config still lets the daemon come
up (the battle will just show English names).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add LAN mode to /tkm:friendly-battle open

The CLI and transport already supported binding to any address via
--listen-host; only the skill gated it to 127.0.0.1. Now
`/tkm:friendly-battle open lan` flips the bind to 0.0.0.0 and resolves
the host's first non-loopback IPv4 to advertise in the share strings.

- Step 0 dispatch recognizes the optional `lan` second token on open.
- Step 1a parameterizes LISTEN_HOST and ADVERTISED_HOST; loopback mode
  stays 127.0.0.1 for safety by default.
- Share strings now interpolate ADVERTISED_HOST instead of hardcoding
  127.0.0.1, so the guest sees the real LAN IP.
- LAN mode also prints a firewall warning (WSL2 users need Windows
  firewall + WSL2 port forwarding; internet/NAT routing is NOT
  supported — same LAN only).
- Help text updated.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Flip /tkm:friendly-battle open default to LAN mode

Same-machine testing is the edge case; real PvP across different
machines on the same network is the expected flow. Default now binds
0.0.0.0 and advertises the detected LAN IP. The explicit `local`
second token switches back to 127.0.0.1 for loopback testing.

Help text + Step 0 dispatch updated accordingly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add PR47 scaffold: two-machine smoke evidence doc + helper script (#48)

Scaffolds the evidence PR for PR42 gap doc §4-4. LAN mode itself
already landed in PR46 (8631e4c), so this PR is documentation +
convenience script only.

New:
- scripts/friendly-battle-smoke.sh — host / host local / guest /
  lan-ip subcommands wrapping bin/run-friendly-battle-turn.sh for
  copy-paste-free smoke setup on two real machines
- docs/friendly-battle/validation/two-machine-smoke.md — §1-§9 with
  prerequisites, LAN IP discovery, firewall setup, walkthrough,
  success log sample, 3 failure scenarios (guest timeout / bad code /
  peer disconnect), troubleshooting, smoke script reference. Real
  two-machine logs left as USER ACTION placeholders; §6-1 includes a
  real loopback reference captur…
@ThunderConch
Copy link
Copy Markdown
Owner Author

Superseded by squash merge of #46 into master

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.

1 participant