> For the complete documentation index, see [llms.txt](https://faction-os.gitbook.io/faction-os-docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://faction-os.gitbook.io/faction-os-docs/.spec_system/specs/phase24-session02-camp-mission-linkage/spec.md).

# Session Specification

**Session ID**: `phase24-session02-camp-mission-linkage` **Phase**: 24 - Legion II - Live Tier And Combat Playback **Status**: Not Started **Created**: 2026-07-08 **Packages**: apps/web (primary), apps/server (secondary) **Package Stack**: TypeScript

***

## 1. Session Overview

This session wires the section 5A camp-to-mission linkage end to end. The projection substrate already exists and is validated: `gameProjection.ts` exports `recordProjectionPendingLink` (dedup, trigger replacement, expiry pruning, `MAX_PENDING_LINKS` cap) and `reduceMissionStart` already binds a `mission_start` whose heroId plus verbatim prompt match a pending link into `campLinks` (with same-hero fallback, replay guard, and expired-link cleanup). What is missing is the recording side: no Quest Board action ever calls `recordProjectionPendingLink`, so `pendingLinks` is permanently empty and no mission can ever bind to a camp.

The session adds the wiring in the store's `acceptQuestSuggestion` and `assignQuestSuggestion` success paths: derive the camp for the accepted card's codebase issue (camps already carry `issueIds`), take the verbatim prompt from the `SuggestionAcceptRestResponse` (both `send_prompt` and `prompt_sent` variants return `prompt`), resolve the hero id, and record the pending link through the projection with the same persistence guards `applyEvent` uses (replay and mock contexts must not persist link state).

It also adds the small pure helpers later sessions need: an issue-to-camp lookup in `legionCamps.ts` and a joint-assault derivation over `campLinks` (all missions currently linked to one camp) so Session 04 can render a coordinated siege. Cards without a camp-backed issue (idle suggestions, follow-ups) record nothing - links exist only where a real camp exists.

**Planning conflict resolved during implementation grounding**: the server's existing `POST /suggestions/:id/accept` handler resolves ids against idle suggestions only, and codebase-issue cards expose only `dismissIssue` - so as originally drafted, no camp-backed accept could ever occur and the session would wire dead code. The design (section 5A, doc 15 grounding) presumes camp quests are acceptable with their `suggestedPrompt`. The minimal honest resolution, kept inside the phase guardrail of "no new protocol events, server routes, or storage keys": extend the existing accept route's id resolution to fall through to current codebase issues (returning the same `SuggestionAcceptResponse` shapes with the issue's `suggestedPrompt`; the issue is not dismissed by acceptance), and give codebase-issue cards accept/assign capabilities client-side. Same route, same request/response contracts, no protocol change.

***

## 2. Objectives

1. Record a pending link on Quest Board accept and assign success for camp-backed issue cards, using real camp id, hero id, and verbatim prompt.
2. Persist link state through the existing guarded projection persistence path; replay and mock contexts must not persist.
3. Provide `legionCampIdForIssueId` (issue-to-camp lookup) and a joint-assault selector exposing missions linked per camp.
4. Prove recording, binding, fallback, expiry, no-camp, failure/rollback, and replay/mock no-persist behavior with focused tests.

***

## 3. Prerequisites

### Required Sessions

* [x] `phase23-session08-validation-and-documentation` - camps, Quest Board focus, and projection folding shipped.
* [x] `phase24-session01-pure-strike-and-combo-model` - not a code dependency, but establishes the phase's pure-module conventions.

### Required Tools/Knowledge

* `recordProjectionPendingLink`, `reduceMissionStart`, `withPendingLinks` internals in `apps/web/src/lib/gameProjection.ts` (verified present).
* Store accept/assign flows at `useGameStore.ts` `acceptQuestSuggestion` / `assignQuestSuggestion`; `foldGameProjection` / `shouldPersistGameProjection` / `scheduleGameProjectionPersist` guard pattern in `applyEvent`.
* `SuggestionAcceptSendPromptResponse` (`heroId` + `prompt`) and `SuggestionAcceptPromptSentResponse` (`prompt`, no heroId) in `packages/protocol/src/suggestions.ts`.
* `QuestBoardCard.actions.dismissIssue?.issueId` as the card's issue id; camps carry `issueIds` in the projection.

### Environment Requirements

* Node 26.2.0+, npm 11.16.0, workspace install present.

***

## 4. Scope

### In Scope (MVP)

* A player accepting a camp-backed quest links the resulting mission to that camp: on accept/assign success, look up the card's issue id in the current projection camps and record `{campId, heroId, prompt}` through `recordProjectionPendingLink`.
* Hero resolution is honest: `send_prompt` responses use the returned `heroId`; `prompt_sent` responses use the explicitly assigned hero id (assign flow); a plain accept with `prompt_sent` and no known hero records nothing rather than guessing.
* Non-camp cards record nothing: idle suggestions, session follow-ups, and analysis items without a camp-resident issue id produce no link.
* Persistence honesty: link recording reuses the `applyEvent` guard shape - fold in memory always, persist only when neither replaying nor mock mode.
* `legionCampIdForIssueId(camps, issueId)` pure helper in `legionCamps.ts`.
* Joint assault: pure `campMissionsFromLinks(campLinks)` inverse derivation in `gameProjection.ts` plus a `selectGameCampMissions` store selector.
* Focused tests across the store action paths, helpers, and persistence guards.

### Out of Scope (Deferred)

* Strike rendering, HP drain, camp kills - *Reason: Session 04.*
* `mission_start` binding changes - *Reason: already implemented and tested; this session only verifies it end to end from the new recording side.*
* Full replay/mock/persistence regression matrix - *Reason: Session 07 owns the phase-wide honesty sweep; this session covers the direct guards.*
* Any protocol or server change - *Reason: linkage is client-derived by design (section 5A).*

***

## 5. Technical Approach

### Architecture

The projection stays the single authority: recording goes through the existing `recordProjectionPendingLink` entry point, applied inside a store `set` with the same persist-context guards as `foldGameProjection`. A small store-internal helper (`recordQuestCampPendingLink`) centralizes the guard shape so both accept and assign call one path. Camp lookup and link inversion are pure functions beside the existing helpers.

### Design Patterns

* Reducer equality guards: `recordProjectionPendingLink` already returns reference-stable no-ops; the store helper skips `set` churn when the projection reference is unchanged.
* Same Quest Board action path (P23 lesson): no new action verbs or routes; recording rides the existing accept/assign success branch after `clearOptimisticQuestCard`.
* Pure derivation for presentation (P22/P23 lesson): joint assault is a pure inverse map over `campLinks`, not new state.

### Technology Stack

* TypeScript (web workspace), Vitest, Biome.

***

## 6. Deliverables

### Files to Create

| File   | Purpose                           | Est. Lines |
| ------ | --------------------------------- | ---------- |
| (none) | Session modifies existing modules | -          |

### Files to Modify

| File                                            | Changes                                                                          | Est. Lines |
| ----------------------------------------------- | -------------------------------------------------------------------------------- | ---------- |
| `apps/server/src/managers/suggestionManager.ts` | Accept fallthrough to current codebase issues (no dismissal on accept)           | \~30       |
| `apps/server/tests/suggestionManager.test.ts`   | Issue-accept semantics tests                                                     | \~40       |
| `apps/server/tests/suggestionRoutes.test.ts`    | Route-level issue accept tests                                                   | \~40       |
| `apps/web/src/lib/questBoardModel.ts`           | Accept/assign capabilities on codebase-issue cards                               | \~10       |
| `apps/web/tests/questBoardModel.test.ts`        | Card capability tests                                                            | \~20       |
| `apps/web/src/lib/legionCamps.ts`               | Add `legionCampIdForIssueId` lookup                                              | \~20       |
| `apps/web/src/lib/gameProjection.ts`            | Add `campMissionsFromLinks` inverse derivation                                   | \~25       |
| `apps/web/src/store/useGameStore.ts`            | Record pending links on accept/assign success; `selectGameCampMissions` selector | \~70       |
| `apps/web/tests/questBoardStore.test.ts`        | Accept/assign link recording, no-camp, failure, guard tests                      | \~150      |
| `apps/web/tests/gameProjection.test.ts`         | `campMissionsFromLinks` tests                                                    | \~40       |
| `apps/web/tests/legionCamps.test.ts`            | `legionCampIdForIssueId` tests                                                   | \~30       |

(Existing test file names verified; if store quest tests live in a different file, extend the file that already covers `acceptQuestSuggestion`.)

***

## 7. Success Criteria

### Functional Requirements

* [ ] Accepting a camp-backed issue card records a pending link with the camp's id, the response hero id, and the verbatim response prompt.
* [ ] Assigning to an idle hero records the link with that hero id even for `prompt_sent` responses.
* [ ] A plain accept returning `prompt_sent` (no hero id) records nothing.
* [ ] Cards without a camp-resident issue id record nothing.
* [ ] Failed or rolled-back accept/assign actions record nothing.
* [ ] A subsequent matching `mission_start` binds `missionId -> campId` through the existing reducer (end-to-end store test).
* [ ] `campMissionsFromLinks` inverts `campLinks` deterministically (sorted mission ids per camp) and `selectGameCampMissions` exposes it.
* [ ] `legionCampIdForIssueId` finds the camp containing an issue id and returns null for unknown ids.

### Testing Requirements

* [ ] Unit tests written and passing for all paths above.
* [ ] Guard tests: with `replayingSinceMs` set or `mockEnabled` true, link recording folds in memory but does not schedule persistence.

### Non-Functional Requirements

* [ ] No new protocol events, routes, storage keys, or state authorities.
* [ ] No projection reference churn when recording is a no-op.

### Quality Gates

* [ ] All files ASCII-encoded, Unix LF line endings.
* [ ] Biome format/lint clean; web typecheck passes; full web project tests pass from the repo root (`npx vitest run --project web`).

***

## 8. Implementation Notes

### Key Considerations

* The recording moment is action success, not `suggestion_update` arrival: the accept response is the only source of the verbatim prompt.
* Persist-context capture must read `useSettingsStore.getState().mockEnabled` and `replayingSinceMs` at recording time, mirroring `foldGameProjection`.
* `recordProjectionPendingLink` already prunes expired links and enforces `MAX_PENDING_LINKS`; do not duplicate that logic in the store.
* The card is removed optimistically before the request; capture the issue id and camp id from the card before `removeQuestCardOptimistically`.

### Potential Challenges

* Hero id absence on `prompt_sent`: resolved by recording only when a hero id is known (assign parameter or `send_prompt` response) - honesty over coverage.
* Camp membership drift between accept and scan updates: the link carries `campId` at accept time; if the camp is later re-reconciled the binding simply points at the surviving camp id or dangles harmlessly (Session 04 renders only links whose camp still exists).

### Relevant Considerations

* \[P23-apps/web] **Scanner camps are projection-owned**: linkage reads projection camps and folds through the projection entry point; no camp copies.
* \[P22-apps/web] **Do not persist replay or drill progress**: guard tests are a hard requirement, not polish.
* \[P23-apps/web] **Same Quest Board action path**: no new verbs; recording is invisible except for later combat playback.
* \[P20] **Broad privacy gates**: pending links persist the verbatim prompt inside the existing projection storage boundary - verify the prompt is already length-bounded by `normalizePendingLinkPrompt` and add no other raw fields.

### Behavioral Quality Focus

Checklist active: Yes Top behavioral risks for this session:

* Link recording firing on failed/rolled-back actions or duplicate in-flight accepts (mutation safety).
* Persistence guard drift: links persisting during replay or mock drills.
* Contract mismatch between the two accept response variants (missing exhaustive handling of `action`).

***

## 9. Testing Strategy

### Unit Tests

* `legionCampIdForIssueId`: hit, miss, empty camps, duplicate issue ids.
* `campMissionsFromLinks`: empty, single, multi-mission per camp, ordering.
* Store accept/assign: success records (both response variants), no-camp records nothing, failure records nothing, `prompt_sent` without hero records nothing.

### Integration Tests

* End-to-end store flow: accept success -> pending link present -> dispatch `mission_start` with matching hero and prompt -> `campLinks` binds and the pending link is consumed; `selectGameCampMissions` reflects it.

### Manual Testing

* Not required beyond the store-level integration test; the visible surface arrives in Session 04.

### Edge Cases

* Accept response prompt differing from `suggestedPrompt` (server rewrote it): the response prompt wins.
* Two concurrent accepts on different camps: both links recorded, distinct triggers.
* Re-accepting the same card after rollback: single link (dedup by trigger).

***

## 10. Dependencies

### External Libraries

* None new.

### Other Sessions

* **Depends on**: none strictly; Session 01 conventions.
* **Depended by**: `phase24-session04-combat-playback-effects-layer`, `phase24-session07-honesty-and-persistence-boundaries`.

***

## Next Steps

Run `/implement` to begin AI-led implementation.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://faction-os.gitbook.io/faction-os-docs/.spec_system/specs/phase24-session02-camp-mission-linkage/spec.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
