Build the serverless friendly battle foundation through PR7#40
Closed
ThunderConch wants to merge 12 commits into
Closed
Build the serverless friendly battle foundation through PR7#40ThunderConch wants to merge 12 commits into
ThunderConch wants to merge 12 commits into
Conversation
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…
Owner
Author
|
Superseded by squash merge of #46 into master |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
docs/friendly-battle/as the source of truth for this tracktokenmon friendly-battlehost,join,ready, andleavesurfacesJOIN_COMMANDto re-enter through the public CLICurrent UX / scope
This branch is intentionally serverless + friendly-only.
Included now:
Not included yet:
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✅Notes for review
src/cli/friendly-battle.tsis intentionally a thin product-facing facade over the local harness.src/cli/friendly-battle-local.tsremains the local harness / reproduction surface.