- Skill progression: crew skills (NAVIGATION, SCANNING, ENGINEERING, RESEARCH) gain XP on every completed action, leveling through 5 bands; higher skill levels reduce task duration via power scaling.
- Role-based starting levels: PILOTs start NAVIGATION L2, SCIENTISTS start SCANNING+RESEARCH L2, etc. Recruits discovered during exploration get the same role-based starting profile.
- Level-up events: when a member's skill steps into the SENIOR (L3) or MASTER (L5) band, a named event is emitted (e.g. "A. OKAFOR is now a SENIOR SCIENTIST — SCANNING L3").
- CREW tab: each member now shows their four skill levels and a title (SENIOR/MASTER + role) based on their highest skill.
- Team-builder: the duration preview now accounts for crew skill power (same computation as the queue routes).
- Combat traits (SHARP SHOOTER, SHORT FUSE, JINXED IN A DOGFIGHT) now display a "STANDBY — encounter-ready" label, as promised in v0.10.0.
- OpenAPI: `CrewMember` gains `skills` (array of {skill, level}) and `title` fields; fixes pre-existing snake_case `trait_good`/`trait_bad` to camelCase to match the actual response.
## v0.10.1 — 2026-10-03 18:36
- Fix React 19 hydration mismatch on every production load — the banner's live clock was prerendered at build time in the static page and desynced at hydration (CI-verified: no hydration errors in the job logs). A real fix, but NOT the cause of the red e2e jump specs.
- Fix the world-map jump dispatch — the actual cause of the three deterministically red e2e jump specs: TeamBuilder's `blocked` term was `undefined`, not `null`, whenever no jump was queued (`Array.find` returns `undefined`), so the roster rendered (truthiness check) while the DISPATCH button's `blocked !== null` guard read it as blocked and stayed disabled — the JUMP panel's dispatch was permanently dead on an empty queue (the 300 s `waitForResponse` timeout; present since the builder shipped in v0.10.0, so the UI-jump specs were red on every run since, on a fresh DB; non-JUMP panels were unaffected, their branch yielding `null`).
# Skill Progression Implementation Plan (PR B — Gitea #45, v0.11.0)
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Crews gain skills by doing jobs; power bands make experienced teams faster; role names unchanged.
**Architecture:** A pure `lib/game/skills.ts` plus the `crew_skills` table (schema v8) feed power bands into the existing queue/tick engine defined by the PR A plan.
**Upstream:** PR A plan: docs/superpowers/plans/2026-10-01-crew-driven-actions.md — Tasks 3/4a/4b define `queueTask` / `runTurn` / `computeDuration` / `Task`; this plan assumes the post-v0.10.0 merge and starts a fresh feature branch.
## Global Constraints
- AGENTS.md change process per PR: feature branch off master, no direct pushes to master; version bump in package.json + CHANGELOG entry at top (`## v<version> — YYYY-MM-DD HH:MM`); local verify ONLY `npx tsc --noEmit`, `npx vitest run`, `npx playwright test --list` (e2e is CI-only, never run locally); push → CI auto-merge on green (verify merged_at) → tag v<version> on master → manual deploy `docker compose up -d --build` + verify /api/version at https://space-wanderer.dev.kkhost.pl.
- DDL in lib/db/client.ts must stay in sync with lib/db/schema.ts; schema changes only via idempotent backfill steps in save.ts ensureSave (SCHEMA_VERSION 6→7→8); no DB wipes ever.
- OpenAPI spec in lib/openapi.ts updated whenever an endpoint changes (and lib/openapi.test.ts pins, which must be updated with it).
- Single-player by design: NO auth, accounts, or multiplayer, ever.
- Determinism: every game outcome reproducible from seed + player actions; no wall-clock randomness.
- New pure modules (crew-costs.ts, tasks.ts, skills.ts) must not import lib/db/* — shared server+client like the tech.ts precedent (required for live duration preview in the UI).
## Review Focus
1. Existing save loaded after deploy has no tasks/crew_skills tables → backfill must create them without touching turn, crew, or inventory state, and be idempotent across restarts.
---
Task numbering continues from the PR A plan (1–11).
- Modify: `lib/db/client.ts` (add `crew_skills` to the `DDL` template, after `tasks`), `lib/db/schema.ts` (drizzle table `crewSkills` + unique index), `lib/game/save.ts` (`SCHEMA_VERSION` 7→8; idempotent `seedCrewSkills` helper; the v8 step in `backfillSave`; a `createNewGame` hook — JC-21), `lib/db/admin.ts` (register `crew_skills` in `ADMIN_TABLES`)
- Test: extend the `backfill (old saves)` describe in `lib/game/save.test.ts` (Task 2's v7-fixture pattern: `freshFile()` + `getWorldState` + stub `schemaVersion` + reopen via `openGameDb(file)`); `lib/db/admin.test.ts`'s registry test picks up the table automatically (it derives its expected list from `schema.ts`)
**Interfaces:**
- Consumes: `SkillName` + `STARTING_LEVELS` (`Record<string, Record<SkillName, number>>`) from Task 1's `lib/game/crew-costs.ts`; `schema.crew`/`schema.saves`; `backfillSave`/`ensureSave` in `lib/game/save.ts` (now `SCHEMA_VERSION = 7` after Task 2)
- Produces (for Task 13+): the `crew_skills` table — `saveId` (FK saves), `crewId` (FK crew), `skill` (`SkillName`), `level` (0–5), `xp` — seeded to **exactly 4 rows per member** (one per skill) with `level = STARTING_LEVELS[role][skill]`, `xp = 0`, for every save (fresh + upgraded). Task 13 (XP-on-completion, level-ups, power bands, UI) reads/writes this table; level→power stays in `crew-costs.ts` (`POWER_BANDS`)
- [ ]**Create the branch** (PR B, off `master` post-v0.10.0 merge): `git checkout -b feat/skill-progression master`. Every commit in this PR references `(#45)`.
- [ ]**Write the failing test** — extend the `backfill (old saves)` describe in `lib/game/save.test.ts`; add `import { STARTING_LEVELS } from './crew-costs'` at the top. Pins **Review Focus 1 (v8)**: v7→v8 seeds 4 rows per member without touching turn/crew/inventory, idempotent across restart:
```ts
// Insert one member directly (skill seeding depends only on role — traits can be dummies).
db.db.insert(schema.crew).values({saveId,name,role,traitGood:'STEADY UNDER PRESSURE',traitBad:'IMPULSIVE',origin:'STARTING CREW'}).returning().then((r)=>r[0])
// A member's 4 skill rows as { [skill]: { level, xp } }; the toHaveLength(4) asserts one row per skill.
expect(Number(reopened.raw.exec('SELECT COUNT(*) FROM crew_skills')[0]?.values[0][0]??0)).toBe(4*(awaitreopened.db.select().from(schema.crew)).length)// still 4 rows per member
})
```
- [ ]**Run — expect red**: `npx vitest run lib/game/save.test.ts` → red: `schema.crewSkills` is not exported (the test can't compile) and `SCHEMA_VERSION` is still 7. The other describes stay green (they don't reference `crewSkills`).
- [ ]**Implement**, each in the existing style of its file:
`lib/db/client.ts` — add to the `DDL` template after the `tasks` table (keep in sync with `schema.ts`):
`lib/game/save.ts` — add `import { STARTING_LEVELS, type SkillName } from './crew-costs'`; bump the constant; add the idempotent seeder (called by `createNewGame` and the v8 backfill):
In `createNewGame`, add after `await seedStartingCrew(db, save.id, seed)`: `await seedCrewSkills(db, save.id)`. In `backfillSave`, add after the v7 step:
```ts
if(version<8){
awaitseedCrewSkills(db,save.id)// DDL creates the table at open; seed each member's 4 rows
version=8
}
```
`lib/db/admin.ts` — register in `ADMIN_TABLES` after `tasks`: `crew_skills: schema.crewSkills,`
- [ ]**Run — expect green**: `npx vitest run lib/game/save.test.ts lib/db/admin.test.ts` → green (new v8 test passes; the admin registry test picks up `crew_skills` automatically); `npx tsc --noEmit` clean.
**Closeout (PR B):** version bump **v0.11.0** + `CHANGELOG.md` entry + open/auto-merge PR + tag **v0.11.0** + manual deploy + close Gitea issue #45 follow the standard per-PR loop (mirroring the v0.10.0 / #44 closeout at this plan's tail). Gitea numbering here is the plan's convention (spec §10's pre-fix numbering lists this work as #44; the actual Gitea issue is #45 (the table is corrected in PR A's Task 11 before the spec is committed)).
**Judgment calls**
- JC-21 — `ensureSave` runs `backfillSave` only for pre-existing saves; a *fresh* save is built by `createNewGame`, which never backfills. So the v8 seeding is also called from `createNewGame` (the same idempotent `seedCrewSkills`) — otherwise fresh v8 saves would have no `crew_skills` rows at all. Mirrors how `seedStartingCrew`/`seedStartingShip`/`seedStartingTech` are invoked from both `createNewGame` and their backfill steps.
### Task 13: skills.ts — XP, levels, power bands, titles (PR B)
**Files:**
- Create: `lib/game/skills.ts` (pure — no `lib/db` imports; operates on plain `SkillRow` data from Task 12's `crew_skills` table; the frontend imports it for the CREW-tab skill display + titles, same server+client precedent as `crew-costs.ts`) and test `lib/game/skills.test.ts` (colocated, pure — no DB)
**Interfaces:**
- Consumes: from `./crew-costs` — `SkillName`, `XP_THRESHOLDS` (level thresholds [5,10,15,20,25]), `POWER_BANDS` ([1,1,1,2,2,3]); the `crew_skills` rows (Task 12) as plain `{ saveId, crewId, skill, level, xp }`
- [ ] Steps: TDD — write failing tests FIRST in `lib/game/skills.test.ts` using the spec's EXACT values (run `npx vitest run lib/game/skills.test.ts` → red, the module does not exist yet):
- [ ] Implement `lib/game/skills.ts` (pure — no `lib/db` imports; frontend imports it for the CREW-tab skill display + titles, same precedent as `crew-costs.ts`):
```ts
// Pure skill-progression module (spec §6): XP accrual, level→power, titles. No lib/db imports — operates on plain SkillRow data (Task 12's crew_skills table); the frontend imports it for the CREW-tab display.
// +1 XP to every listed member's row for `skill`; returns updated rows + any level-up events. An event
// fires when the gained skill steps into a titled band (3 → SENIOR, 5 → MASTER), e.g. "BEN OKONKWO is now a SENIOR PILOT — NAVIGATION L3" (title from the member's HIGHEST skill).
if(title)events.push(`${member.name} is now a ${title} — ${skill} L${newLevel}`)
}
}
return{rows,events}
}
```
- [ ]**Run — expect green**: `npx vitest run lib/game/skills.test.ts` → green (both levelFromXp/powerForLevel tables, the three grantXp crossings, the two titleFor cases); `npx tsc --noEmit` clean.
- [ ]**Commit** (PR B, same branch as Task 12): `git add -A && git commit -m "feat(game): skills.ts — XP, levels, power bands, titles (#45)"`.
**Judgment calls**
- JC-22 — spec §6 shows one level-up event (the SENIOR PILOT example) and none for plain L1/L2/L4; so `grantXp` emits an event only when the gained skill steps into a titled band (new level 3 → SENIOR, 5 → MASTER). The event's title is computed from the member's HIGHEST skill level (via `titleFor` over all their rows); if a member is already MASTER from one skill and a second then crosses L3, this would emit a MASTER-titled event naming the L3 skill — a rare edge left as-is (per-member dedup can be added in Task 14's orchestrator if wanted).
### Task 14: Wire skill XP into the tick + power bands into duration (PR B)
**Files:**
- Modify: `lib/game/skills.ts` (add `makePowerOf` — the power-band closure); `lib/game/save.ts` (`runTurn`: grant XP on each resolved task, fold the level-up events into the tick's event batch, persist `crew_skills` in the final write; new `makePowerOfFor` route helper; imports `grantXp`/`makePowerOf`/`type SkillRow`/`ACTION_SKILL`); `lib/game/tasks.ts` (`queueTask` gains an optional `powerOf` param, forwarded to `computeDuration`); and the five queue paths — `app/api/jump/route.ts`, `app/api/explore/route.ts`, `app/api/ship/{upgrade,expand,build}/route.ts`, `app/api/research/route.ts` (each passes `makePowerOfFor(db, save, type)` into `queueTask`)
- Test: extend `lib/game/save.test.ts` (the `runTurn (tick on the DB)` describe) and `lib/game/tasks.test.ts` (the `queueTask` block). No new files.
**Interfaces:**
- Consumes: Task 4b's `runTurn(db)` orchestrator in `save.ts` — reads `saves`/`crew`/`tasks`/`inventory`/`shipSystems`/`tech` into a `QueueState`, runs `tickTurn`, writes `saves.turn` before resolving, dispatches `result.completed` through `TASK_RESOLVERS`, and ends in one whole-DB `persist(db)`; Task 13's `grantXp(skillRows, crewIds, skill, members) → { rows, events }`, `powerForLevel(level)`, `levelFromXp(xp)`, `titleFor(member, rows)`, `SkillRow`, `CrewIdentity`; Task 1's `ACTION_SKILL: Record<TaskType, SkillName>`, the unchanged `computeDuration(type, hopCount, lead, helpers, powerOf?)`, and `CrewMember`; Task 12's `schema.crewSkills` table; Task 3's `queueTask(state, req)` / `QueueState`.
- Produces: `makePowerOf(roster: CrewDto[], skillRows: SkillRow[], type: TaskType) → (m: CrewMember) => number` in `skills.ts` — the exact `powerOf` closure Task 15's live UI preview calls (Task 15's crew endpoint must expose each member's skill rows for the UI to build it); and `makePowerOfFor(db, save, type): Promise<(m: CrewMember) => number>` in `save.ts` — the route-side builder the five queue paths call.
- [ ]**Write the failing tests** — extend the two existing describes (no new files):
-`lib/game/tasks.test.ts` (`queueTask` block) — a power-band duration case: a solo member with a `powerOf` returning 2 → `queueTask(state, { type: 'EXPLORE', ... }, (m) => 2)` stores `remaining 2` (`ceil(4/2)`), while the no-`powerOf` call (default flat 1) still stores 4 — pins the new param AND its backward-compatible default.
-`lib/game/save.test.ts` (`runTurn` describe) — three cases over the 4a `buildWorldFixture`/`freshFile` pattern (patch a member's `crew_skills` row directly; a fresh save already has 4 rows per member from Task 12's seed):
1.**XP accrual** — a 3-person EXPLORE (lead + 2 helpers) completes in one tick → all three members' `SCANNING` rows each gain exactly +1 XP (lead and helpers equally).
2.**Level-up event** — a SCIENTIST whose `SCANNING` row is at xp 14 (L2) completes an EXPLORE → the tick writes the §6 text into `schema.events`: `AVA VANCE is now a SENIOR SCIENTIST — SCANNING L3` (xp 15 → L3 → SENIOR band).
3.**§6 duration-shift** — a member at `SCANNING` L0/xp 0 queues an EXPLORE solo → stored `duration 4`; after 15 completed EXPLOREs (xp 15 → L3 → power 2) the next queued EXPLORE → stored `duration 2` (`ceil(4/2)`). The lead is a SCIENTIST (the preferred EXPLORE role) so the §6 "4 → 2" holds without a mismatch penalty — see JC-23.
- [ ]**Run — expect red**: `npx vitest run lib/game/tasks.test.ts lib/game/save.test.ts` → the new `it`s red (`queueTask` has no `powerOf` param; `runTurn` doesn't read `crew_skills`, so no XP accrual, no level-up event, and the queued duration stays 4 at L3); every pre-existing test (Task 3's `queueTask` block, 4b's `runTurn` block, the 4a A/B + guard blocks) stays green.
- [ ]**Implement** — one action per file:
1.`lib/game/skills.ts` — add `import { ACTION_SKILL } from './crew-costs'` and `import type { CrewDto } from './save'` (type-only, so no runtime cycle — the `tasks.ts` precedent), then `makePowerOf`: closes over `Map<name, crewId>` (names are unique per single-player save) + `Map<crewId, level-in-ACTION_SKILL[type]>`; returns `(m) => powerForLevel(levelByCrew.get(idByName.get(m.name) ?? '') ?? 0)` — an unknown member / no row falls back to L0 → power 1 (Task 1's default).
2.`lib/game/tasks.ts` — `queueTask(state: QueueState, req: QueueTaskRequest, powerOf: (m: CrewMember) => number = () => 1)`; the one line `const duration = computeDuration(req.type, hops, lead, helpers)` becomes `computeDuration(req.type, hops, lead, helpers, powerOf)`. `computeDuration` itself is unchanged.
3.`lib/game/save.ts` — import `grantXp`, `makePowerOf`, `type SkillRow` from `./skills` and `ACTION_SKILL` from `./crew-costs`. In `runTurn`: add `crewSkills` to the top `Promise.all` read; map the rows to `SkillRow[]` (stringify `saveId`/`crewId`) and build `members: Record<string, CrewIdentity>` keyed by `String(crew.id)`; in the `result.completed` loop, after each `TASK_RESOLVERS[task.type](db, task)`, call `grantXp(skillRows, [String(task.leadCrewId), ...task.helpers.map(String)], ACTION_SKILL[task.type], members)` and reassign `skillRows` (so overlapping crew across several completions in one tick accumulate), pushing its `events` into a tick-level array; after the loop, insert the level-up events as one `schema.events` batch at `result.turn` and update every changed `crew_skills` row — both inside the existing final `persist(db)`. Add `makePowerOfFor(db, save, type)` (reads `crew` + `crew_skills`, returns `makePowerOf(roster, rows, type)`).
4. The five queue-path routes — pass `await makePowerOfFor(db, save, type)` as the third `queueTask` arg so the stored duration uses power bands.
- [ ]**Run — expect green**: `npx vitest run` → the whole unit suite passes (the three new `runTurn` cases, the new `queueTask` power-band case, and every pre-existing block); `npx tsc --noEmit` clean.
- [ ]**Commit** (PR B, same branch as Tasks 12–13): `git add -A && git commit -m "feat(game): skill XP on task completion + power bands in duration (#45)"`.
**Judgment calls**
- JC-23 — the locked design's "e.g. a COMMANDER" for the §6 duration-shift would in fact incur EXPLORE's mismatch penalty (COMMANDER ≠ the preferred SCIENTIST → +1 → 5, not 4), contradicting the cited "duration 4 → 2 (`ceil(4/2)`)". The test therefore leads the EXPLORE with a SCIENTIST (the preferred role, no penalty) seeded at `SCANNING` L0 so the exact §6 "rookie 4 → 2" numbers hold; the power-band effect (the point of the test) is identical either way.
- JC-24 — `SkillRow.crewId`/`saveId` are strings (Task 13) but `crew_skills.crew_id`/`save_id` and `tasks.leadCrewId`/`helpers` are numeric; `runTurn` stringifies `crew.id`/`save.id` when mapping rows to `SkillRow[]` and when building `members` and the `grantXp``crewIds`, keeping one string-id domain end to end so `grantXp`'s `crewIds.includes(r.crewId)` matches.
- JC-25 — 4a's resolvers each insert their own resolution event and `persist`; to keep the level-up events "in the same event batch" and the `crew_skills` updates "in the same single write" as the resolution, `runTurn` writes the level-up events as one batch insert and the `crew_skills` updates inside its existing final `persist(db)` (same `result.turn`, one atomic commit) rather than refactoring every 4a resolver to return its event.
- Modify: `lib/game/save.ts` — the crew-state reader `getCrewState` (post-PR-A it returns the `CrewState { crew, tasks }` wrapper; this is the only lib change in the task) attaches per-member skill data to each `CrewDto`: `skills` (the member's four `crew_skills` rows as `{ skill, level }` in the fixed order NAVIGATION → SCANNING → ENGINEERING → RESEARCH) and `title` (Task 13's `titleFor({ name, role }, memberRows)` — `null` when the member's highest skill is below L3); the `CrewDto` interface gains those two fields; imports `titleFor`/`type SkillRow` from `./skills` and `type SkillName` from `./crew-costs` (type-only, no runtime cycle); the `crew_skills` select joins the existing crew read in the same `Promise.all`.
- Modify: `components/crew-panel.tsx` — per member, render the four skill levels on one `·`-separated line (e.g. `NAVIGATION L3 · SCANNING L0 · ENGINEERING L0 · RESEARCH L2`) from `member.skills`, and, only when `member.title` is non-null, a title line (e.g. `SENIOR PILOT`). Display-only — no new interaction. Also label the three STANDBY traits (SHARP SHOOTER, SHORT FUSE, JINXED IN A DOGFIGHT) as "STANDBY — encounter-ready" in each member's trait line (spec §8).
- Modify: `components/team-builder.tsx` (PR A's component) — extend each member row to show that member's power contribution for the armed action's skill (e.g. `BEN OKONKWO (power 2)`), computed with Task 14's `makePowerOf`; the existing live duration preview and the dispatch control stay unchanged.
- Test: extend `lib/game/save.test.ts` — one new `it` in the `getCrewState` snapshot block for per-member `skills` + `title`. No component tests (the repo has no jsdom/@testing-library — the unit suite is pure-TS only), so the rendered UI is covered by e2e (CI-only), per the standard skip note.
- Produces: `CrewDto.skills: { skill: SkillName; level: number }[]` (the four rows, fixed order) and `CrewDto.title: string | null` on the `GET /api/crew` response — asserted by the new unit test and by e2e (the CREW-tab skill/title lines and the team-builder `(power N)` rows). No new endpoint, no schema/table change.
- [ ]**Write the failing test** — in `lib/game/save.test.ts`'s `getCrewState` snapshot block, over a fresh save (which Task 12 already seeds with four `crew_skills` rows per member): pin one member (e.g. a PILOT) to `NAVIGATION` L3 (xp 15) and its other three skills L0, and leave a second member all-L0. Assert the pinned member's `getCrewState(db).crew` entry carries `skills` = `[{ NAVIGATION, 3 }, { SCANNING, 0 }, { ENGINEERING, 0 }, { RESEARCH, 0 }]` (all four, fixed order) and `title` = `"SENIOR PILOT"`, and the all-L0 member's `title` = `null`.
- [ ]**Run — expect red**: `npx vitest run lib/game/save.test.ts` → the new `it` is red (`getCrewState` still returns `CrewDto` entries without `skills`/`title`); every pre-existing block (4b, Task 14's XP/event/duration, the PR-A snapshot-queue block) stays green.
- [ ]**Implement** — one action per file:
1.`lib/game/save.ts` — in `getCrewState`, add `crew_skills` to the read and group rows by `crewId`; for each `CrewDto` set `skills` to the four fixed-order `{ skill, level }` rows and `title` to `titleFor({ name, role }, memberRows)`; add the two fields to the `CrewDto` interface and the type-only imports.
2.`components/crew-panel.tsx` — under each member's role line, render the `·`-separated skill-level line from `member.skills` and, only when set, a `member.title` line; purely presentational.
3.`components/team-builder.tsx` — import `makePowerOf` (and `type SkillRow`) from `@/lib/game/skills`; build `skillRows: SkillRow[]` once from the roster (`roster.flatMap((c) => c.skills.map((s) => ({ saveId: '', crewId: String(c.id), skill: s.skill, level: s.level, xp: 0 })))`); `const powerOf = makePowerOf(roster, skillRows, type)`; append `(power N)` to each member row's name for that member's contribution in the armed action's skill. Leave the live `duration` preview and dispatch as-is.
- [ ]**Run — expect green**: `npx vitest run` → the whole unit suite passes (the new `getCrewState` skills/title `it` plus every pre-existing block); `npx tsc --noEmit` clean. The rendered CREW-tab lines and team-builder `(power N)` rows are e2e-covered (CI-only, per AGENTS.md).
- [ ]**Commit** (PR B, same branch as Tasks 12–14): `git add -A && git commit -m "feat(ui): skill levels + titles on CREW tab and team-builder (#45)"`.
**Judgment calls**
- JC-26 — `getCrewState` returns the lightweight `{ skill, level }` per member (not the full `SkillRow` with `xp`/`saveId`) to keep the `GET /api/crew` payload display-oriented; the team builder reconstructs just enough of a `SkillRow[]` (`crewId` + `skill` + `level`) to feed `makePowerOf`, which reads only those fields, so the full rows are not shipped to the client twice.
- JC-27 — skill levels render in the fixed NAVIGATION → SCANNING → ENGINEERING → RESEARCH order (the spec §6 skill list) rather than DB row order, so the CREW-tab line and any e2e text assertion are stable; the same order is pinned in the new unit test.
- JC-28 — the `GET /api/crew` response object gains two additive display-only fields, but per the locked design this task keeps the OpenAPI `CrewMember` schema (and its `openapi.test.ts` key pin) at its six documented keys, leaving the spec slightly under-documented; a trivial follow-up may mirror `skills`/`title` into the schema + pin later. CI stays green as-is because the pin is unchanged.
**Files touched (this ship step only — no app code, no failing-test step):**
-`docs/superpowers/plans/2026-10-01-skill-progression.md` — this plan (untracked, committed here; the spec doc was already committed + approved in PR A, so no spec edits in this PR).
-`package.json` (version bump) and `CHANGELOG.md` (new top entry).
This is a process task that ships the PR B already built by Tasks 12–15 (all code + tests already live on the same branch). It has no failing-test step and ends in a verification gate.
1. - [ ] **Commit the PR B plan document on the PR B branch** (the branch created in Task 12, `feat/skill-progression`) — the plan doc is untracked: `git add docs/superpowers/plans/2026-10-01-skill-progression.md && git commit -m "docs: skill-progression implementation plan (#45)"`. The spec doc was already committed in PR A — no spec edits in this PR.
2. - [ ] **Version bump** — `package.json` → `"version": "0.11.0"` (from the current `0.10.0`). `CHANGELOG.md`: new entry at the very top in the repo's format `## v0.11.0 — YYYY-MM-DD HH:MM` (fill the date/time at execution) with a bullet summary of the PR: the `crew_skills` table (schema v8) with role-based starting specialisation; +1 XP per completed task for all assigned crew; power bands in task duration (L0–2→1, L3–4→2, L5→3); SENIOR/MASTER titles; the CREW tab shows skill levels + title and the team-builder shows per-member power contribution.
3. - [ ] **Local verify — ONLY these three (e2e is CI-only, never run locally):**`npx tsc --noEmit` (clean), `npx vitest run` (all pass), `npx playwright test --list` (every spec compiles + lists). All three must pass. Do NOT run the e2e suite (`npx playwright test`) — CI-only per AGENTS.md.
4. - [ ] **Push + open the PR to master** — `git push -u origin feat/skill-progression`, then open a pull request to `master` on Gitea (repo `OpenCode/SpaceWanderer` at git.kkhost.pl). Gitea API access per Task 11's JC-20: prefer the pre-configured **gitea MCP tools** to create the PR; if you must use raw REST/curl, `source` the repo-root `.env` (`GITEA_TOKEN`, gitignored).
5. - [ ] **Auto-merge on green** — trigger Gitea's auto-merge: `POST /repos/OpenCode/SpaceWanderer/pulls/<n>/merge` with body `{"do":"merge","merge_style":"merge","merge_when_checks_succeed":true}`. The PR merges automatically once the required status check `CI / build + unit + e2e (pull request)` passes (branch protection; CI runs the full e2e suite in an isolated dind stack, ~7 min). Poll the PR until `merged_at` is set (max ~15 min; poll the API every ~30s). If CI is red, **STOP and report the failure** — never force-merge.
6. - [ ] **Tag on master** — after pulling the merge on `master`: `git tag v0.11.0 && git push origin v0.11.0`.
7. - [ ] **Manual deploy** (CI never deploys) — `docker compose up -d --build` in the repo root, then verify `curl -s https://space-wanderer.dev.kkhost.pl/api/version` returns the 0.11.0 payload.
8. - [ ] **Close the issue** — Gitea API: close issue #45 with a comment summarizing the shipped state (version 0.11.0, tag v0.11.0, live deploy verified).
**Verification gate (whole task):**`/api/version` on the live URL (`https://space-wanderer.dev.kkhost.pl`) reports 0.11.0 **AND** Gitea issue #45 is closed — only then is this PR done.
expect(Number(reopened.raw.exec('SELECT COUNT(*) FROM crew_skills')[0]?.values[0][0]??0)).toBe(4*(awaitreopened.db.select().from(schema.crew)).length)// still 4 rows per member
origin:{type:'string',description:"'STARTING CREW' or '<planet>, <system>' for members found while exploring"},
skills:{
type:'array',
description:'The member four crew_skills levels in the fixed NAVIGATION → SCANNING → ENGINEERING → RESEARCH order (display-oriented — levels only, no xp)',
description:"The member's four crew_skills levels in the fixed NAVIGATION → SCANNING → ENGINEERING → RESEARCH order (display-oriented — levels only, no xp)",
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.