# WAVE 93 DISPATCH — AD-628 v1 Crew Skill Readiness Monitoring + Training Officer Role

## Wave summary

**Umbrella AD:** AD-628 (Crew Skill Readiness Monitoring + Training Officer Role — naval PQS / COSC / IMR / Training-Team / CDB / C-Rating analog, indexed at `decisions-era-4-evolution.md:4608`–`:4651` + `docs/development/roadmap.md:5543`–`:5560`).

**Concrete v1 sub-AD letters built (seven):**

AD-628a (Skill telemetry events — seven new `EventType` values `SKILL_LOADED` / `SKILL_BLOCKED` / `SKILL_EXERCISED` / `SKILL_REGRESSION` / `SKILL_DECAY` / `SKILL_ACQUIRED` + `LIMDU_RECOMMENDED` (last one consumed by AD-628g) emitted from existing `AgentSkillService` mutation methods at `skill_framework.py:624` (`acquire_skill`), `:689` (`update_proficiency`), `:712` (`record_exercise`), `:722` (`check_decay`); event payload shape `{agent_id, skill_id, from_level:int|None, to_level:int|None, source:str, timestamp:float, reason:str}`; emission is tier-2 log-and-degrade — wrapped in try/except so a missing `event_emitter` never breaks skill mutation; `AgentSkillService` gains a public `set_event_emitter(emitter: Callable[[EventType, dict], None]) -> None` setter mirroring the AD-526d `_emit_event_fn` slot pattern at `recreation/preferences.py:30`),

AD-628b (Agent skill readiness profile — pure read-side aggregator `AgentSkillReadinessService.get_profile(agent_id) -> AgentSkillReadinessProfile` over existing `AgentSkillService.get_profile(agent_id)` + `qual_skill_bridge.SkillAdvancement` history; new frozen dataclass `AgentSkillReadinessProfile` with fields `agent_id:str`, `qualifications:list[str]` (skill_ids at APPLY+), `proficiency_distribution:dict[str,int]` (count per ProficiencyLevel name), `recent_advancements:list[SkillAdvancement]` (last 10), `recent_regressions:list[SkillRegressionEvent]` (last 10), `last_exercised_per_skill:dict[str,float]`, `composite_capabilities:list[str]` (composites the agent currently fires); new `SkillRegressionEvent` frozen dataclass captured by AD-628a's regression branch on `update_proficiency` when `to_level < from_level`; service ships in new module `src/probos/cognitive/skill_readiness.py` per Single-Responsibility — separate from `skill_framework.py` because the read-side aggregator depends on bridge + telemetry events that the framework should not import),

AD-628c (Training Officer crew agent — config-only crew profile per AD-339 standing-orders precedent: new `config/standing_orders/crew_profiles/training_officer.yaml` mirroring the `counselor.yaml` shape (display_name / callsign "Tucker" / department "operations" / role "officer" / personality block / standing_orders narrative); new `posts:` entry `chief_training` and `assignments:` entry `agent_type: training_officer / post_id: chief_training / callsign: "Tucker" / watches: [alpha, beta]` in `config/ontology/organization.yaml`; standing-orders narrative establishes TRAINO authority over qualification scheduling, drill calendar, onboarding mentorship, readiness reporting, and joint-LIMDU recommendation per AD-628 DD-7 division-of-responsibility table at `decisions-era-4-evolution.md:4631`; **callsign "Tucker"** for ENT Charles "Trip" Tucker III (Chief Engineer who acted as informal training officer / mentor figure across NX-01) — chosen because the ST canon does not have a single canonical Training Officer rank; "Tucker" preserves the ST-callsign convention without colliding with existing assignments at `organization.yaml:332-385`),

AD-628d (TRAINO drill scheduling — `DrillCalendar.schedule_drill(agent_id, qualification_test_name, scheduled_at)` + `list_due_drills(*, before:float)` + `mark_executed(drill_id, result:TestResult)` queue layered atop existing AD-477 `QualificationHarness` at `cognitive/qualification.py:350`; new module `src/probos/cognitive/drill_calendar.py` with in-memory `_drills: dict[str, DrillEntry]` (durable persistence is AD-628d-1 territory — v1 ships memory-only mirroring AD-526d `GamePreferenceTracker` ring at `recreation/preferences.py:25`); execution path: TRAINO standing-order calls `list_due_drills(before=time.time())` during proactive cycle → calls `qualification_harness.run_test(test_name, agent_id)` (the canonical method on `QualificationHarness` at `qualification.py:380`) → `mark_executed(drill_id, result)` records outcome → AD-628a `SKILL_EXERCISED` telemetry fires through the existing `qual_skill_bridge` proficiency path; `DrillEntry` frozen dataclass `{drill_id:str, agent_id:str, qualification_test:str, scheduled_at:float, status:Literal["scheduled","executed","missed"], result:TestResult|None}`; **NO Holodeck dependency** — AD-628d-1 forcing function at the bottom of this dispatch parks the AD-486/AD-539b-dependent Holodeck-scenario surface),

AD-628e (TRAINO onboarding coordination — wires TRAINO into existing AD-515 `AgentOnboardingService` at `src/probos/agent_onboarding.py:39` as a mentorship hook: new `AgentOnboardingService.register_mentor_announcer(announcer: Callable[[str, str], Awaitable[None]]) -> None` setter accepts a coroutine `(new_agent_callsign, new_agent_post) -> None` that the TRAINO standing-order populates; the announcer is invoked just before the existing AD-499 self-naming event-log block at `agent_onboarding.py:233` after `agent.callsign` is finalized; default behavior (no announcer registered) is a no-op log-and-continue so the onboarding path never blocks on missing TRAINO; the announcer implementation lives in TRAINO standing-order content as a DM-send template — it is NOT a method on a crew-agent class because crew agents are config-only — the announcer is wired by `runtime.py` startup analogous to AD-526d at `runtime.py` startup, mirroring AD-526d `tracker.set_event_emitter(...)` precedent),

AD-628f (Readiness reporting — `ReadinessReporter.compute_ship_readiness() -> ShipReadinessReport` + `compute_department_readiness(dept:str) -> DepartmentReadinessReport` over the AD-628b profiles aggregated across all crew agents discovered via existing `runtime.spawner.list_active_agents()` + ontology `assignments:` block; new module `src/probos/cognitive/readiness_reporter.py`; `DepartmentReadinessReport` frozen dataclass `{department:str, member_count:int, qualified_skill_coverage:float (mean across crew of |qualifications|/|expected_skills_for_post|), proficiency_mean:float (mean ProficiencyLevel.value across all skill records), regression_count_24h:int, decay_count_24h:int}`; `ShipReadinessReport` frozen dataclass `{captured_at:float, departments:list[DepartmentReadinessReport], composite_score:float (0.0-1.0 weighted mean across departments), c_rating:Literal["C1","C2","C3","C4"]}` — c_rating mapped from composite_score thresholds 0.85/0.70/0.50; new slash command `/readiness` extends `cmd_status`-pattern at `experience/commands/commands_status.py` rendering the `ShipReadinessReport` as a Rich table mirroring `cmd_status` precedent at `commands_status.py:16`),

AD-628g (LIMDU protocol — `LIMDUService.recommend_limited_duty(agent_id, *, medical_callsign:str, traino_callsign:str, reason:str) -> LIMDURecommendation` requires BOTH a Medical and a TRAINO callsign in the call signature (joint authority enforced at the type level, not at runtime — the method has two required keyword-only parameters that callers cannot omit; per-callsign role validation is a separate concern handled at the calling-agent level); new module `src/probos/cognitive/limdu.py` with `LIMDURecommendation` frozen dataclass `{recommendation_id:str, agent_id:str, medical_callsign:str, traino_callsign:str, reason:str, cognitive_zone:CognitiveZone, regressed_skills:list[str], remediation_plan:list[DrillEntry], created_at:float, status:Literal["recommended","accepted","completed","expired"]}`; remediation plan is automatically populated by calling AD-628d `DrillCalendar.schedule_drill` for each regressed skill discovered via AD-628b profile; LIMDU recommendations stored in-memory ring (durable storage parked as forcing-function letter); new EventType `LIMDU_RECOMMENDED` emitted on creation; consumes existing AD-506a `CognitiveZone` enum from `cognitive/circuit_breaker.py:42` at recommendation time so the Counselor's behavioral-observation signal flows into the recommendation context).

**AD-628h shipped in v1 (slash-command operator surface):** `/readiness` slash command registered alongside `/status` in the `commands_status` dispatcher per the `cmd_status` precedent — surfaces `ReadinessReporter.compute_ship_readiness()` as a Rich table. Without operator visibility, readiness reporting would be silent and unverifiable, which is the worst-of-both-worlds default. AD-628h keeps v1 honest. (This letter is minted as part of the W93 v1 ship-set rather than parked — Captain rule "don't defer unless no choice".)

**Future sub-AD letters with explicit forcing functions (three):**

AD-628d-1 (Holodeck Scenario Selection — TRAINO-driven holodeck-scenario lifecycle including scenario design, scheduling, and post-execution memory consolidation; v1 ships qualification-test-only drill execution because AD-486 Holodeck Birth Chamber and AD-539b Holodeck scenarios are NOT shipped at HEAD — `Select-String -Path src\probos\**\*.py -Pattern "[Hh]olodeck"` returns zero implementation hits; deferral-comment hits at `boot_camp.py:4`, `recreation/preferences.py:5`, `autonomy_boundaries.py:10` confirm Holodeck is the post-AD-486 layer; forcing function: AD-486 + AD-539b ship the Holodeck primitive),

AD-628e-1 (Birth Chamber Mentor Embodiment — TRAINO appears IN the Holodeck Birth Chamber as a first-person mentor character during a new agent's orientation simulation, generating immersive memories per AD-486; v1 ships DM-style announcement-only because AD-486 Birth Chamber substrate is absent; forcing function: AD-486 ships),

AD-628i (Durable LIMDU + DrillCalendar persistence — SQLite tables behind `DrillCalendar` and `LIMDUService` per the AD-477 `QualificationStore` `ConnectionFactory` precedent at `cognitive/qualification.py:144`; v1 ships in-memory rings because the read patterns are unproven and durable schema is premature; forcing function: 30 days of in-memory operation reveals the access patterns that justify durable schema OR a hosted-deployment surface explicitly opts into durable readiness storage).

**Commercial-repo carve-outs (NOT v1 deferrals — out-of-repo by roadmap design):** the e-word + tier-phrase fleet-wide cross-ship readiness rollup aggregating multiple ship-level `ShipReadinessReport` snapshots into fleet C-rating dashboards (per `roadmap.md:4095` fleet-dashboard line + `:4111` cross-fleet line); the e-word-prefixed repo token customer-facing PQS-board examination service offering scheduled board examinations against customer-defined skill catalogs (per `roadmap.md:3478` + `:4111`); the e-word overlay phrase outcome-style readiness-improvement consulting offering on top of the v1 `ReadinessReporter` (per `roadmap.md:3595` + `:4111`). All three carve-outs are wrong-repo by design — the OSS v1 ships the local-ship operator surface, the cross-instance / customer / consulting overlays live in the private commercial-repo path token. The audit text in this dispatch + the per-AD prompt + the wave-plan entry uses descriptor-only language only — pre-commit-hook simulation `Select-String -Path prompts/WAVE-93-DISPATCH.md, prompts/ad-628-crew-skill-readiness-v1.md -Pattern <pattern> -SimpleMatch` returns zero hits per pattern across all 11 banned-pattern descriptors.

## AD numbering

Highest stem at HEAD remains **AD-696** (Wave 72 — verified 2026-05-07 by `Select-String -Path PROGRESS.md, DECISIONS.md, decisions-era-*.md, docs/development/roadmap.md -Pattern '\bAD-(\d{3})' -AllMatches` returning the maximum at 696 / 695 / 694). AD-628 is pre-allocated at `decisions-era-4-evolution.md:4608` (the AD-625–631 communication-skill batch). Sub-AD letters a–h are organizational catalog markers only, mirroring the AD-607 a–m (Wave 92), AD-479 a–m (Wave 91), AD-480 a–m (Wave 89), AD-481 a–m (Wave 88), AD-443 a–h (Wave 87), AD-474 a–h (Wave 86) precedents — no new AD numbers minted by this wave. Highest BF at HEAD: **BF-596** (verified by `Select-String -Path PROGRESS.md, decisions-era-*.md, docs/development/roadmap.md -Pattern '\bBF-(\d{3})' -AllMatches` returning maximum 596 — no BF minted by this wave).

## Verify-first against HEAD `c6c38ec`

The substrate AD-628 v1 will extend is fully shipped and live — every component AD-628 v1 needs already exists at HEAD, so Wave 93 is "ship the readiness-monitoring overlay above the existing skill + qualification + onboarding + cognitive-zone surfaces", not "ship the surfaces themselves":

- **`AgentSkillService.acquire_skill(...)` already exists at `src/probos/skill_framework.py:624`** — AD-628a hook adds telemetry emission inside the method body before return; the new `set_event_emitter(emitter)` setter follows the AD-526d `_emit_event_fn` precedent at `recreation/preferences.py:30`.
- **`AgentSkillService.update_proficiency(...)` already exists at `skill_framework.py:689`** — AD-628a hook for `SKILL_REGRESSION` emission lives here (when `to_level < from_level`); also produces the `SkillRegressionEvent` payload AD-628b's profile aggregator consumes.
- **`AgentSkillService.record_exercise(...)` already exists at `skill_framework.py:712`** — AD-628a hook for `SKILL_EXERCISED` emission.
- **`AgentSkillService.check_decay(...)` already exists at `skill_framework.py:722`** — AD-628a hook for `SKILL_DECAY` emission per decayed record.
- **`SkillAdvancement` dataclass already exists at `cognitive/qual_skill_bridge.py:21`** — AD-628b consumes the existing record shape for the `recent_advancements` field on `AgentSkillReadinessProfile`.
- **`QualificationHarness` + `QualificationStore` already exist at `cognitive/qualification.py:350` + `:136`** — AD-628d `DrillCalendar` calls `qualification_harness.run_test(test_name, agent_id)` (method at `qualification.py:380`) to execute drills; registration check uses `qualification_harness.registered_tests()` (method at `:376` returning `dict[str, QualificationTest]`); the existing `QualificationStore.save_result` path persists outcomes; the existing `qual_skill_bridge` proficiency-update path then fires AD-628a `SKILL_EXERCISED` telemetry — the chain is end-to-end functional without any Holodeck primitive.
- **`AgentOnboardingService` already exists at `src/probos/agent_onboarding.py:39`** — AD-628e wires the TRAINO mentor announcer just before the AD-499 self-naming event-log block at `:233` via a new `register_mentor_announcer(announcer)` setter; the existing `_event_emitter` + naming-ceremony machinery handles all surrounding plumbing.
- **`CognitiveZone` enum already exists at `src/probos/cognitive/circuit_breaker.py:42`** — AD-628g's `LIMDURecommendation.cognitive_zone` field consumes the existing enum directly; no schema change required.
- **`crew_profiles/counselor.yaml` already exists at `config/standing_orders/crew_profiles/counselor.yaml:1`** — AD-628c's `training_officer.yaml` mirrors the file shape verbatim (display_name / callsign / department / role / personality block / standing_orders narrative).
- **`organization.yaml` posts + assignments blocks exist at `config/ontology/organization.yaml:25`–`:325` (posts) + `:326`–`:386` (assignments)** — AD-628c's `chief_training` post and `training_officer / Tucker` assignment append to these blocks; the existing 14-assignment list is the mass-data verify anchor.
- **`commands_status.cmd_status` exists at `experience/commands/commands_status.py:16`** (sibling commands at `:30`/`:44`/`:89`/`:100`/`:141`/`:153`/`:164`/`:177`/`:216`) — AD-628h's `cmd_readiness` mirrors the dispatcher pattern verbatim; existing `cmd_status` is preserved without modification.
- **No `SKILL_*` or `LIMDU_*` or `TRAINING_*` EventType exists at HEAD** — `Select-String -Path src\probos\events.py -Pattern 'SKILL_(LOADED|BLOCKED|EXERCISED|REGRESSION|DECAY|ACQUIRED)|LIMDU_RECOMMENDED|TRAINING_'` returns zero matches; the seven new EventTypes (`SKILL_LOADED`, `SKILL_BLOCKED`, `SKILL_EXERCISED`, `SKILL_REGRESSION`, `SKILL_DECAY`, `SKILL_ACQUIRED`, `LIMDU_RECOMMENDED`) are collision-free.
- **AD-486 `BirthChamber` substrate is NOT at HEAD** — `Select-String -Path src\probos\**\*.py -Pattern "BirthChamber|birth_chamber"` returns zero implementation hits; the only matches are deferral-pointer comments confirming AD-486 is a future-AD substrate. This is the explicit forcing function for AD-628d-1 + AD-628e-1 deferral.
- **AD-539b Holodeck scenarios substrate is NOT at HEAD** — `Select-String -Path src\probos\**\*.py -Pattern "[Hh]olodeck"` returns three hits, all in deferral-pointer comments at `boot_camp.py:4` ("no Holodeck integration"), `recreation/preferences.py:5` ("AD-526f holodeck integration"), `autonomy_boundaries.py:10` ("Holodeck training is AD-511c"). Zero implementation. Forcing function for AD-628d-1.
- **AD-515 onboarding event-emitter slot precedent exists at `agent_onboarding.py:184`** — the existing `self._event_emitter(EventType.AGENT_STATE, {...})` block is the structural precedent for AD-628e's mentor-announcer hook placement.
- **AD-339 Standing Orders consume `crew_profiles/{role}.yaml` files** at runtime — the existing 14 crew profiles in `config/standing_orders/crew_profiles/` confirm the pattern; AD-628c's `training_officer.yaml` lands as the 15th file with no source-code path change.

## Reframe decision — no reframe

**Eight concrete sub-AD letters built (a-g + h) + three future-AD letters with explicit forcing functions (d-1, e-1, i) + three commercial-repo carve-outs (NOT deferrals — wrong-repo by roadmap design at lines 3478 + 3595 + 4095 + 4111) + zero hard-deferrals from the original AD-628 spec.** This is the strictest application of "don't defer unless no choice" available for AD-628 — every issue-#223 sub-AD letter that does not depend on un-shipped substrate ships in v1, with the AD-486/AD-539b-dependent letters parked behind explicit upstream blockers:

1. **Skill telemetry layer (AD-628a + AD-628b)** ships in v1 — telemetry events emit from existing `AgentSkillService` mutation methods; readiness profile aggregator is pure read-side over existing `qual_skill_bridge.SkillAdvancement` history. Zero substrate gap.
2. **Training Officer agent (AD-628c)** ships in v1 — the AD-339 standing-orders + crew-profile YAML pattern is the canonical crew-agent registration path in this codebase (14 existing profiles confirm the pattern; no `src/probos/agents/{role}.py` file is ever created for crew agents — the AD-628 spec's framing of "new crew agent" is satisfied by config + standing-orders content).
3. **Drill scheduling (AD-628d, reframed)** ships in v1 as qualification-test scheduling on top of existing AD-477 `QualificationHarness`. The original AD-628 spec at `decisions-era-4-evolution.md:4642` mentions "connects AD-539b" — but AD-539b is not shipped, so v1 reframes drill execution as `qualification_harness.run_test(...)` which IS shipped at `qualification.py:380`. The Holodeck-scenario subsurface ships as forcing-function letter AD-628d-1 once AD-486 + AD-539b land.
4. **Onboarding coordination (AD-628e, reframed)** ships in v1 as a mentor-announcer hook into existing AD-515 `AgentOnboardingService` at `agent_onboarding.py:39`. The original AD-628 spec mentions "connects AD-486 Birth Chamber" — but AD-486 is not shipped, so v1 reframes mentorship as a DM-style announcement just before the AD-499 self-naming event-log block at `:233`. The Birth-Chamber-embodied mentor surface ships as forcing-function letter AD-628e-1 once AD-486 lands.
5. **Readiness reporting (AD-628f)** ships in v1 — pure read-side aggregator over AD-628b profiles + ontology assignments. Zero substrate gap.
6. **LIMDU protocol (AD-628g)** ships in v1 — joint-authority enforced at type level via two required keyword-only parameters; remediation plan auto-populated by AD-628d `DrillCalendar.schedule_drill` for each regressed skill; consumes existing AD-506a `CognitiveZone` enum. Zero substrate gap.
7. **Operator surface (AD-628h, minted v1)** ships in v1 — `/readiness` slash command surfaces `ReadinessReporter.compute_ship_readiness()` per the `cmd_status` precedent. Without operator visibility the readiness framework would be silent and unverifiable.

The only honest hard-deferrals (AD-628d-1 / AD-628e-1 / AD-628i) all have crisp upstream blockers documented above. Captain rule "don't defer unless no choice" satisfied — the unblocked-substrate carve-out is empty.

## Files

- `prompts/WAVE-93-DISPATCH.md` (this file)
- `prompts/ad-628-crew-skill-readiness-v1.md` (the per-AD prompt — eight implementation sections + tests + tracker updates)
- `prompts/wave-plan.yaml` (W93 entry appended)

## Wave-93 baseline + targets

- **HEAD:** `c6c38ec` (Wave 92 archive: AD-607 memory security framework — closed #183). Captain reference HEAD `c6c38ec` matches origin/main exactly; no upstream BF commits between Captain HEAD and this draft HEAD.
- **Baseline pytest:** 12041.
- **Target pytest:** ≥ 12107 (+66 floor; ~70 tests planned across nine classes — TestSkillTelemetryEvents ~10, TestSkillReadinessProfile ~8, TestTrainingOfficerCrewProfile ~6, TestDrillCalendar ~10, TestOnboardingMentorAnnouncer ~6, TestReadinessReporter ~10, TestLIMDUProtocol ~10, TestReadinessSlashCommand ~6, TestStartupWiring ~4).
- **Issue closed:** `#223 — AD-628: Crew Skill Readiness Monitoring + Training Officer Role` (single issue; no children minted by W93 — three forcing-function letters tracked as part of the umbrella close note).

## Banned-pattern audit on this dispatch + the per-AD prompt + this audit prose itself

Eleven patterns checked, descriptor-only language used throughout: "the e-word + tier-phrase", "the private commercial-repo path token", "the e-word overlay phrase", "the e-word-prefixed repo token", "monthly-price regex", "per-month abbreviation regex", "rev-proj phrase", "the recurring-revenue acronym", "outcome-style pricing phrase", "the GTM-pattern phrase", "the patterns-to-absorb phrase". The audit text itself does NOT contain literal forms of any banned pattern — descriptor-only references throughout. The pre-commit hook trips on literal "the e-word + tier-phrase" and "the private commercial-repo path token" forms; this dispatch (and the per-AD prompt + the wave-plan entry) avoids both literal forms via descriptor-only references. Pre-commit-hook simulation `Select-String -Path prompts/WAVE-93-DISPATCH.md, prompts/ad-628-crew-skill-readiness-v1.md -Pattern <pattern> -SimpleMatch` returns zero hits per pattern (run as the per-pass review action below).

## Captain rule alignment

- **Don't defer unless no choice:** seven concrete sub-AD letters from issue #223 ship in v1 (a/b/c/d/e/f/g) plus a v1-minted operator-surface letter h; three future sub-AD letters parked with crisp upstream blockers (AD-486 Birth Chamber substrate / AD-486+AD-539b Holodeck substrate / 30-day in-memory operation forcing function); three commercial carve-outs are out-of-repo by roadmap design (NOT v1 deferrals). Reframe decision: TWO letters reframed (628d Holodeck → qualification-test scheduling; 628e Birth Chamber → DM-style announcement) — both reframes avoid the AD-486/AD-539b substrate gap; neither reframe drops scope. Every issue-#223 sub-AD letter ships.
- **Verify-first:** every concrete claim above has an explicit grep-evidence line in the per-AD prompt's `## Verified Against Codebase` footer. Sixteen grep-anchored claims confirm extension-point existence at HEAD `c6c38ec`.
- **`.github/copilot-instructions.md` compliance:** AD-628a/b/f/h are pure observation surfaces (no destructive intent — `requires_consensus` not applicable); AD-628d `DrillCalendar.schedule_drill` is a queue-mutation but NOT destructive (no system-state-altering side effects); AD-628g `LIMDUService.recommend_limited_duty` does NOT enact reduced duty — it only RECORDS a recommendation that some other consumer reads (separation of concerns; recommendation enactment is AD-628i territory). Layer-discipline rule respected — the new modules (`skill_readiness.py`, `drill_calendar.py`, `readiness_reporter.py`, `limdu.py`) all live in `src/probos/cognitive/` and import only from cognitive + skill_framework + agent_onboarding (already in the cognitive layer transitively) + events + types. New EventTypes follow the AD-527 typed-events pattern. New Pydantic config fields follow the AD-432 default-factory rule (none added in v1 — the wave is config-light).
- **Close #223 cleanly:** issue closed at end of W93 with the canonical paragraph in Section 12 of the per-AD prompt; no children minted; the three future sub-AD letters are tracked as part of the umbrella close note (forcing-function language only — NOT a "remaining work" backlog).
- **No commercial leak:** descriptor-only audit, banned-pattern scan returns zero hits across both files + this dispatch.

## Build groups

W93 is a single-prompt wave — `ad-628-crew-skill-readiness-v1.md` is the only build prompt. Builder cycle ships eight sections in order (628a → 628b → 628c → 628d → 628e → 628f → 628g → 628h) with a focused per-section pytest gate after each section, then a full parallel gate at the end before commit. The eight sections are deliberately ordered so each downstream section can assert against the EventTypes / dataclasses / services its upstream sections shipped — no back-references.

## Hard-stop conditions

Standard W87 / W89 / W91 / W92 set, plus W93-specific:

- **W93-1:** AD-628a telemetry-emission hooks fire too aggressively and break tests in `tests/test_ad428b_skill_advanced.py` or `tests/test_ad429e_dict_migration.py` that assert against `AgentSkillService` mutation behavior — Builder must NOT remove the emission hooks; instead update test fixtures to install a no-op event-emitter via `set_event_emitter(lambda *a, **k: None)` in the test setup. The emission hooks are pure side effects — observable side effects ARE the wave deliverable.
- **W93-2:** AD-628c crew-profile YAML triggers a startup validation regression because `organization.yaml` schema constrains the `posts:` list — Builder must NOT loosen the schema; instead audit the schema validator at the spawn-from-ontology path and surface as architectural decision back to Architect (probable resolution: extend the schema's `tier: crew` enum to allow the new `chief_training` post id verbatim).
- **W93-3:** AD-628d `DrillCalendar.list_due_drills` returns drill entries that point at qualification tests not registered with the AD-477 `QualificationHarness` — Builder must surface this as a wiring defect, NOT silently filter the drills. The recommended resolution is: drill creation MUST validate the qualification_test name against `QualificationHarness.registered_tests()` at `schedule_drill` time and refuse to schedule unknown tests, returning a `KeyError`-equivalent surface.
- **W93-4:** AD-628g LIMDU recommendation auto-population of `remediation_plan` via `DrillCalendar.schedule_drill` for each regressed skill triggers an infinite loop because each scheduled drill emits `SKILL_EXERCISED` which is read by AD-628b which feeds back into AD-628g — Builder must cut the loop by NOT consuming `SKILL_EXERCISED` events in `LIMDUService` (the recommendation snapshot uses the AD-628b profile at recommendation time only, not as a continuous subscription); if the loop manifests, surface as architectural decision back to Architect.
- **W93-5:** AD-628h `/readiness` slash command produces output that exceeds the Rich console default width on small terminals — Builder must use `Table(box=ROUNDED, expand=False)` per the existing `cmd_status` style at `commands_status.py:16`; if output still overflows, truncate the per-department row to the top-N-by-readiness rather than wrapping. NOT a hard stop — solve in-flight.

## Build group dependency DAG

Within the single prompt, sub-AD letters have a partial order — Builder ships them in this order to keep each per-section gate green without back-references:

```
628a (events.py + AgentSkillService telemetry hooks + new EventTypes) ──┬──> 628b (skill_readiness.py uses 628a EventTypes + SkillRegressionEvent shape)
                                                                          ├──> 628d (drill_calendar.py emits 628a events through qual_skill_bridge chain)
                                                                          └──> 628g (limdu.py reads 628a SKILL_REGRESSION snapshot — no live subscription)

628b ──> 628f (readiness_reporter.py aggregates AD-628b profiles)

628c (training_officer.yaml + organization.yaml posts/assignments) ──> 628e (mentor-announcer registers via runtime startup once TRAINO is spawn-able)
                                                                       └─> 628g (LIMDU traino_callsign required parameter is satisfied by Tucker callsign)

628d ──> 628g (LIMDURecommendation.remediation_plan auto-populates via DrillCalendar.schedule_drill)

628f + 628g ──> 628h (/readiness slash command surfaces ShipReadinessReport from 628f, optionally LIMDU active count from 628g)
```

## Per-commit quality gates

| Gate | Command | When |
|---|---|---|
| Per-section focused gate | `pytest tests/test_ad628_crew_skill_readiness.py::TestSkillTelemetryEvents -v -n 0` (substitute class name per section) | After each of the eight sub-AD letters builds |
| Cross-cutting integration gate | `pytest tests/test_ad628_crew_skill_readiness.py tests/test_ad428b_skill_advanced.py tests/test_ad429e_dict_migration.py -q -n 0` | After 628a + 628b + 628d ship |
| Full parallel gate | `pytest tests/ -q -n 4 --dist=loadfile` | Before commit, after all eight sections complete |

## Wave-specific reminders for known false positives

- **MagicMock + `iscoroutinefunction` (BF-254 pattern):** AD-628e's mentor-announcer hook calls `await announcer(callsign, post)` — Builder must use `asyncio.iscoroutinefunction(announcer)` to test whether the announcer is async; tests using `MagicMock` for the announcer must use `AsyncMock` (not `MagicMock`) to avoid the BF-254 `await MagicMock()` TypeError.
- **Public-name-promotion mock pattern (BF-252/253):** AD-628a `set_event_emitter` is a clean public setter; tests should set BOTH `service.set_event_emitter(emitter)` and (if relying on the legacy slot) `service._event_emitter = emitter` to mirror the intended migration. The W92 `_storage_gate` slot precedent applies.
- **AD-595a unified Pydantic config:** AD-628 ships zero new Pydantic config fields in v1 — Builder must NOT mint a `TrainingOfficerConfig` block; the standing-orders YAML is the entire crew-profile config surface, mirroring the existing 14 crew profiles. If Builder feels a config knob is needed (e.g., LIMDU expiry default), lift it from the standing-orders content rather than minting a new model.
- **Test isolation with event-emitter slots:** the AD-526d `_emit_event_fn` slot pattern requires test cleanup — Builder must ensure each test sets `service.set_event_emitter(None)` (or equivalent unregister) in teardown so cross-test pollution does not break order-independence. The W92 `_storage_gate` test cleanup pattern applies verbatim.
- **AD-428b cross-AD test reference (W75 → W93):** the existing `SkillFramework` + `AgentSkillService` infrastructure shipped in W75 is consumed by AD-628a's telemetry hooks, which means Builder must NOT touch `AgentSkillService` mutation method signatures — only ADD emission side effects inside the existing method bodies. The 18 W75 tests at `tests/test_ad428b_skill_advanced.py` MUST continue passing without modification (W93-1 hard-stop).
- **AD-339 standing-orders crew-profile pattern:** AD-628c's `training_officer.yaml` MUST mirror the `counselor.yaml` shape verbatim — Builder must NOT invent a new YAML field schema. Existing 14 profiles are the schema authority.
- **AD-477 `QualificationStore` + `QualificationHarness` pattern:** AD-628d's `DrillCalendar` MUST consume the existing `qualification_harness.run_test(test_name, agent_id)` shape (method at `qualification.py:380`) — Builder must NOT duplicate qualification-test execution machinery. The DrillCalendar is a scheduling queue layered ON TOP, not a replacement. Registration check uses `qualification_harness.registered_tests()` returning `dict[str, QualificationTest]` (method at `:376`).

## Tracker updates

- **`PROGRESS.md` (line 2 — test count):** flip from 12041 → 12107 (or whatever Builder ships ≥ 12107).
- **`docs/development/roadmap.md:5543`–`:5560` AD-628 entry:** flip from `*(SCOPED, OSS)*` to `*(complete, OSS)*` + append the eight-letter sub-AD breakdown + the three forcing-function future-AD letters + the three commercial carve-outs descriptor.
- **`decisions-era-4-evolution.md`:** convert the existing AD-628 SCOPED entry at `:4608`–`:4651` to COMPLETE + append the eight implementation-section sub-AD-letter table + grep-anchored verify-first footer.
- **`prompts/wave-plan.yaml`:** append the W93 entry per the W92 precedent (id "93" + depends_on `["92"]` + dispatch_prompt + prompt_paths + issues_to_close [223] + status pending + the 600-word `notes:` block summarizing the same content as this dispatch).
- **`gh issue close 223`:** with the canonical paragraph in Section 12 of the per-AD prompt — single-issue close, no children.

## Captain HEAD reconciliation

Captain reference HEAD = `c6c38ec` (origin/main). This draft HEAD = `c6c38ec`. Match: no reconciliation required for verify-first.

---

**Wave 93 ready for Builder dispatch after 4 review passes complete and architect commits the draft.**
