# AD-477: Naval Organization Protocols — Captain's Log + Plan of the Day

**Status:** Drafted (Wave 12) — Revised 2026-05-03 (review pass-1 findings applied)
**Risk:** medium (cross-cutting reads from episodic memory, ward room, work item store)
**Depends on:** episodic_memory, work_item_store, ward_room (all shipped)
**Closes:** GitHub issue #71

---

## Solution Overview

AD-477 in roadmap.md (line 4207) lists 6 naval organization capabilities. Heavy overlap exists with already-shipped systems:

- **(1) Qualification Programs** — overlaps with AD-566 (Crew Qualification Battery), AD-539 (Gap → Qualification Pipeline), AD-595e (Qualification Gate Enforcement). Extending those is its own AD.
- **(4) 3M System** + **(5) Damage Control Organization** — large engineering systems each warranting their own AD.
- **(6) SORM** — Standing Orders Manual evolution; large governance scope.

**v1 ships 2 of 6 capabilities** (per convention #14 aggressive pre-deferral) — the truly NEW generative surfaces with no existing-system conflict:

1. **Captain's Log** — synthesized daily narrative aggregating episodic memory highlights + Ward Room activity + active work item summary. Three source aggregations. One Markdown document per day at `data/captains_log/YYYY-MM-DD.md`. Generated by background task at end-of-day or on-demand via runtime API.
2. **Plan of the Day** — auto-generated morning operations summary aggregating active WorkItems + Ward Room thread queue + alert conditions. Three source aggregations (scheduled duties deferred to AD-477f). One Markdown document per day at `data/plan_of_day/YYYY-MM-DD.md`.

**Deferred to grandchildren:**
- AD-477b: Qualification Programs (extends AD-566 with rank-transition requirements). Forcing function: when AD-566's qualification system gets concrete pass/fail criteria for cross-rank promotion.
- AD-477c: 3M System (planned preventive maintenance). Forcing function: when fleet operational scale demands it.
- AD-477d: Damage Control Organization (5-phase protocol). Forcing function: when alert-condition response needs structured procedure.
- AD-477e: SORM (Ship's Organization and Regulations Manual). Forcing function: governance evolution AD.
- AD-477f: Plan of the Day scheduled-duty integration. Forcing function: either runtime exposes a public DutyScheduleTracker accessor, OR AD-500a-1 ships and `WorkItem(work_type="duty")` becomes the canonical query path.
- AD-477g: Captain's Log dream-consolidation source. Forcing function: either runtime exposes `runtime.dreaming_engine` as a public attribute, OR `DreamScheduler` exposes a public `recent_consolidation_summaries(...)` accessor. v1 CaptainsLog omits dream summaries (mirrors duty-tracker negative-framing pattern).

## Dependencies

- `runtime.episodic_memory` — read-only consumer (`recent(k)` over-fetch + Python-side date/importance filter; verified at `cognitive/episodic.py:1832`).
- `runtime.work_item_store` — read-only consumer (`list_work_items` for active WorkItems; verified at `workforce.py:1066`).
- `runtime.ward_room.list_threads` — read-only consumer (verified at `ward_room/service.py:289` — kwargs `channel_id`, `limit=50`, `offset=0`, `sort="recent"`, `include_archived=False`).
- `runtime.config.alerts` (or equivalent) — read-only for alert conditions.

**NOTE on scheduled duties:** `DutyScheduleTracker` is currently private to `ProactiveCognitiveLoop` (`self._duty_tracker` at `proactive.py:181`); there is no `runtime.duty_schedule_tracker` public accessor. AD-477f deferred grandchild covers "include scheduled duties in Plan of the Day" via either (a) public accessor on runtime (scope expansion) or (b) AD-500a-1's `WorkItem(work_type="duty")` query path once that ships. v1 PlanOfDay omits scheduled duties.

**NOTE on dream consolidation:** `runtime.dreaming_engine` is NOT a public attribute (the `DreamingEngine` instance lives in `StartupResults.dreaming_engine` only); `runtime.dream_scheduler` exists but does not expose a recent-summaries accessor. Same negative-framing shape as `duty_schedule_tracker` above. AD-477g deferred grandchild covers "include dream consolidation summaries in Captain's Log" via either (a) runtime exposes `runtime.dreaming_engine` publicly, OR (b) `DreamScheduler` adds `recent_consolidation_summaries(...)`. v1 CaptainsLog omits dream summaries.

All reads; no writes to existing surfaces.

## Sections

### Section 0 — Section 0 EventTypes

- `CAPTAINS_LOG_GENERATED` — emitted when a daily Captain's Log document is written to disk.
- `PLAN_OF_DAY_GENERATED` — emitted when a daily Plan of the Day document is written to disk.

Verify no collision with existing events.py.

### Section 1 — Create `src/probos/naval/` package

New package with `__init__.py` re-exports. Path:
- `src/probos/naval/__init__.py`
- `src/probos/naval/captains_log.py`
- `src/probos/naval/plan_of_day.py`

Per AD-457/459/466/467/475/469/641 precedents — owns directory creation.

### Section 2 — `CaptainsLogService`

```python
class CaptainsLogService:
    """Synthesizes daily narrative from episodic memory + Ward Room + work items."""

    def __init__(self, runtime: Any, config: CaptainsLogConfig) -> None:
        self._runtime = runtime
        self._config = config
        self._task: asyncio.Task | None = None  # convention #17: instance state

    async def generate_for_date(self, date: datetime.date) -> str:
        """Generate Captain's Log Markdown for the given date.

        Aggregates THREE sources (dream consolidation deferred to AD-477g):
        - Top N episodes from episodic memory (importance >= threshold) for the date.
          Pattern: call ``await self._runtime.episodic_memory.recent(k=N*4)`` where
          N = ``self._config.top_episodes_count`` (over-fetch for headroom), then
          filter Python-side: keep episodes whose ``episode.timestamp`` falls within
          ``[date_start, date_end]`` (UTC midnight bounds) AND whose
          ``episode.importance >= self._config.importance_threshold``, then sort by
          importance descending and take the first ``top_episodes_count``.
          Rationale: no by-date query exists on EpisodicMemory; ``recent(k)`` is the
          only live entry point (verified at ``cognitive/episodic.py:1832``).
        - Ward Room thread highlights via
          ``await self._runtime.ward_room.list_threads(channel_id=None, limit=50,
          sort="recent")``, filter to threads with activity on ``date``, summarize
          activity count + top topics.
        - Active work item summary via
          ``await self._runtime.work_item_store.list_work_items(status="open")``
          (canonical ``WorkItemStatus.OPEN`` enum value; verified at
          ``workforce.py:44``).
        Returns the Markdown content as a string. Caller writes to disk.
        """

    async def write_to_disk(self, date: datetime.date) -> Path:
        """Generate + write to data/captains_log/YYYY-MM-DD.md. Emits CAPTAINS_LOG_GENERATED."""

    async def start(self) -> None:
        """Start background task that runs at end-of-day."""

    async def stop(self) -> None:
        """Cancel background task."""
```

### Section 3 — `PlanOfDayService`

```python
class PlanOfDayService:
    """Generates morning operations summary."""

    def __init__(self, runtime: Any, config: PlanOfDayConfig) -> None:
        self._runtime = runtime
        self._config = config
        self._task: asyncio.Task | None = None

    async def generate_for_date(self, date: datetime.date) -> str:
        """Generate Plan of the Day Markdown.

        Aggregates THREE sources (scheduled duties deferred to AD-477f):
        - Active WorkItems via
          ``await runtime.work_item_store.list_work_items(status="open")``
          (canonical ``WorkItemStatus.OPEN`` enum value; verified at
          ``workforce.py:44``).
        - Ward Room thread queue depth via
          ``await runtime.ward_room.list_threads(channel_id=None, limit=50,
          sort="recent")``.
        - Current alert conditions (read-only off ``runtime.config.alerts``
          when ``self._config.include_alert_conditions`` is True).
        Returns Markdown content. Caller writes to disk.
        """

    async def write_to_disk(self, date: datetime.date) -> Path:
        """Generate + write to data/plan_of_day/YYYY-MM-DD.md. Emits PLAN_OF_DAY_GENERATED."""

    async def start(self) -> None:
        """Start background task that runs at start-of-day."""

    async def stop(self) -> None:
        """Cancel background task."""
```

### Section 4 — Pydantic config

```python
class CaptainsLogConfig(BaseModel):
    enabled: bool = True
    output_dir: Path = Path("data/captains_log")
    end_of_day_hour: int = 23  # local hour to trigger generation
    top_episodes_count: int = 5
    importance_threshold: int = 5

class PlanOfDayConfig(BaseModel):
    enabled: bool = True
    output_dir: Path = Path("data/plan_of_day")
    start_of_day_hour: int = 8
    include_alert_conditions: bool = True

class NavalOrganizationConfig(BaseModel):
    captains_log: CaptainsLogConfig = Field(default_factory=CaptainsLogConfig)
    plan_of_day: PlanOfDayConfig = Field(default_factory=PlanOfDayConfig)
```

Use ``Field(default_factory=...)`` for nested-BaseModel defaults (Wave 5 anti-pattern: bare mutable defaults). Wire into `Config.naval_organization` field with the same factory pattern.

### Section 5 — Runtime wiring (finalize.py)

Add to `finalize_startup` (or appropriate startup phase):

```python
# AD-477: Naval Organization Protocols
naval_cfg = getattr(runtime.config, "naval_organization", None)
if naval_cfg:
    if naval_cfg.captains_log.enabled:
        runtime.captains_log_service = CaptainsLogService(runtime, naval_cfg.captains_log)
        runtime.captains_log_start_task = asyncio.create_task(runtime.captains_log_service.start())
        logger.info("AD-477: CaptainsLogService started (output_dir=%s)", naval_cfg.captains_log.output_dir)
    if naval_cfg.plan_of_day.enabled:
        runtime.plan_of_day_service = PlanOfDayService(runtime, naval_cfg.plan_of_day)
        runtime.plan_of_day_start_task = asyncio.create_task(runtime.plan_of_day_service.start())
        logger.info("AD-477: PlanOfDayService started (output_dir=%s)", naval_cfg.plan_of_day.output_dir)
```

Public attributes (no underscore — Wave 5 convention #1; applies to both services and their start-tasks):
- `runtime.captains_log_service`
- `runtime.captains_log_start_task`
- `runtime.plan_of_day_service`
- `runtime.plan_of_day_start_task`

## What This Does NOT Change

- AD-566 Crew Qualification Battery — untouched. AD-477b extends it.
- AD-539 Gap → Qualification Pipeline — untouched.
- AD-595e Qualification Gate Enforcement — untouched.
- Earned Agency / Rank system — read-only consumer; no writes.
- Episodic memory / dreaming engine / WorkItemStore / Ward Room — read-only consumers.
- Standing Orders — AD-477e (SORM) extends; v1 untouched.

## Test Plan

| # | Test | Purpose |
|---|---|---|
| 1 | `test_event_type_captains_log_generated_exists` | Section 0 surface |
| 2 | `test_event_type_plan_of_day_generated_exists` | Section 0 surface |
| 3 | `test_naval_organization_config_defaults` | Pydantic defaults |
| 4 | `test_captains_log_generate_with_no_episodes_returns_empty_template` | Empty-state behavior |
| 5 | `test_captains_log_aggregates_top_episodes_by_importance` | Episode sorting + threshold (over-fetch + Python-side date+importance filter) |
| 6 | `test_captains_log_includes_ward_room_summary` | Ward Room consumption via list_threads |
| 7 | `test_captains_log_write_to_disk_emits_event` | EventType emission + file write |
| 8 | `test_plan_of_day_aggregates_open_work_items` | WorkItemStore consumption with `status="open"` (canonical WorkItemStatus.OPEN) |
| 9 | `test_plan_of_day_includes_alert_conditions_when_enabled` | Config flag behavior |
| 10 | `test_plan_of_day_write_to_disk_emits_event` | EventType emission + file write |
| 11 | `test_start_creates_named_task` | Background task lifecycle |
| 12 | `test_stop_surfaces_cancellederror_to_caller_after_cleanup` | Async-discipline rule: long-running loop catches CancelledError, performs cleanup, re-raises. Test must `assert raises(CancelledError)` — must not allow silent swallow. |
| 13 | `test_runtime_attribute_set_when_enabled` | Public-attribute wiring |
| 14 | `test_runtime_attribute_none_when_disabled` | Disabled config skips wiring |

Total: ~14 tests. (Dream-consolidation source dropped from v1 → no test added or removed; deferred path covered by AD-477g when it ships.)

## Tracking

1. **PROGRESS.md:** prepend AD-477 entry.
2. **DECISIONS.md:** add entry under Era V:

```markdown
### AD-477: Naval Organization Protocols (v1: Captain's Log + Plan of the Day) (2026-05-03)

**Problem:** AD-477 originally bundled 6 naval-organization capabilities. Half overlap with already-shipped systems (Qualification Programs vs AD-566; SORM vs Standing Orders). Heavy bundling led to no-theater risk and integration ambiguity.

**Decision:** v1 ships ONLY the 2 truly NEW generative surfaces, each with 3 source aggregations:
- `CaptainsLogService` — synthesizes daily narrative from episodic memory (over-fetch + Python-side date/importance filter) + Ward Room activity (`list_threads`) + active work item summary (`status="open"`). Markdown output to `data/captains_log/YYYY-MM-DD.md`. Dream-consolidation source deferred to AD-477g (no public `runtime.dreaming_engine` accessor exists).
- `PlanOfDayService` — auto-generated morning operations summary aggregating active WorkItems (`status="open"`) + Ward Room thread queue + alert conditions. Markdown output to `data/plan_of_day/YYYY-MM-DD.md`. Scheduled-duties source deferred to AD-477f (no public `runtime.duty_schedule_tracker` accessor exists).

Both are read-only consumers of existing runtime surfaces. No writes. Public attributes (no underscore per Wave 5 convention #1).

**Why:** Generative narrative + plan documents are forcing-function-ready (Captain reads them daily). Qualification Programs (AD-477b) extends AD-566 — needs separate scope. 3M System (AD-477c), Damage Control (AD-477d), SORM (AD-477e) are larger systems each warranting their own AD. Dream-consolidation (AD-477g) and scheduled-duties (AD-477f) are deferred on accessor-availability forcing functions, mirroring Wave 9B's pre-deferral honesty pattern.

**Deferred:**
- AD-477b: Qualification Programs (rank-transition requirements; extends AD-566).
- AD-477c: 3M System (planned preventive maintenance).
- AD-477d: Damage Control Organization (5-phase protocol).
- AD-477e: SORM (Ship's Organization and Regulations Manual).
- AD-477f: Plan of the Day scheduled-duty integration (forcing function: public DutyScheduleTracker accessor or AD-500a-1 ships).
- AD-477g: Captain's Log dream-consolidation source (forcing function: public `runtime.dreaming_engine` OR `DreamScheduler.recent_consolidation_summaries(...)`).

**Cross-links:** AD-566 (Crew Qualification Battery), AD-539 (Gap → Qualification Pipeline), AD-471 (Watch Bill), Earned Agency, episodic memory, dreaming engine, WorkItemStore.
```

3. **docs/development/roadmap.md:** flip AD-477 status to `partial — v1 ships Captain's Log + Plan of the Day; Qualification Programs/3M/Damage Control/SORM deferred to AD-477b/c/d/e`.

## Verified Against Codebase (2026-05-03, refreshed during revision pass)

```
grep -n "class WorkItemStore\|def list_work_items" src/probos/workforce.py
  905: class WorkItemStore(EventEmitterMixin):
 1066: async def list_work_items(self, status, assigned_to, work_type, parent_id, priority, tags, limit, offset)

grep -n "class WorkItemStatus\|^    OPEN\|^    IN_PROGRESS" src/probos/workforce.py
   41: class WorkItemStatus(str, Enum):
   44:     OPEN = "open"
   46:     IN_PROGRESS = "in_progress"
  (no "PENDING" member — Required #3 fix: status="open" is canonical)

grep -n "async def recent" src/probos/cognitive/episodic.py
 1832: async def recent(self, k: int = 10) -> list[Episode]:
  (no by-date query exists — Required #2 fix: over-fetch + Python-side date+importance filter)

grep -n "async def list_threads" src/probos/ward_room/service.py
  289: async def list_threads(channel_id, limit=50, offset=0, sort="recent", include_archived=False)

grep -rn "runtime\.dreaming_engine\|self\.dreaming_engine" src/probos/
  (zero hits — Required #1 fix: dream-consolidation deferred to AD-477g; runtime carries
   only `runtime.dream_scheduler`, which has no recent-summaries accessor at
   `src/probos/cognitive/dreaming.py`)

grep -rn "runtime.episodic_memory" src/probos/ | head -3
  acm.py:335: if hasattr(runtime, 'episodic_memory') and runtime.episodic_memory:
  acm.py:336: if hasattr(runtime.episodic_memory, 'count_for_agent'):
  cognitive/anchor_quality.py:105: episodic_memory: Any,
```

**AD-685 kwarg-pre-check note:** the kwarg NAME `status` validates clean. The kwarg VALUE drift (`"pending"` → `"open"`) is a manual review-tier fix this pass. Banked as forcing-function for AD-685c/e tooling-hygiene candidate (extend pre-check to validate enum-member values against live `Enum` definitions).

## Acceptance Criteria

- `src/probos/naval/` package exists.
- `CaptainsLogService` + `PlanOfDayService` ship as described.
- Public attributes `runtime.captains_log_service`, `runtime.plan_of_day_service` (no underscore).
- 2 new EventTypes (`CAPTAINS_LOG_GENERATED`, `PLAN_OF_DAY_GENERATED`).
- `NavalOrganizationConfig` Pydantic class wired into `Config`.
- 14 tests pass.
- DECISIONS.md entry under Era V.
- GH issue #71 closes when commit lands.

## Hard-Stops

- `runtime.work_item_store.list_work_items` signature differs from AD-500/AD-501 verification (Wave 10) — surface; rare given recent verification but possible.
- `runtime.ward_room.list_threads` signature drift from `(channel_id, limit, offset, sort, include_archived)` — surface; verified at `ward_room/service.py:289` during revision pass.
- Background tasks conflict with existing scheduled-task infrastructure — surface for re-bundling.

## Revision (2026-05-03)

Second pass against `prompts/Reviews/ad-477-naval-organization-v1-review.md` — Verdict ⚠️ Conditional, 3 Required + 3 Recommended + 3 Nits. All Required addressed; Recommended #1-3 folded; Nit #1 folded; Nits #2-3 judged out-of-scope.

**Required #1 (resolution path: option (a) — defer dream-consolidation to AD-477g):**
- `runtime.dreaming_engine` confirmed phantom (zero hits in `src/probos/`). `runtime.dream_scheduler` exists but exposes no recent-summaries accessor.
- Solution Overview: CaptainsLog now lists 3 source aggregations (episodic + Ward Room + work item summary), dream-consolidation removed.
- Dependencies: dream entry replaced with negative-framing NOTE mirroring `duty_schedule_tracker` pattern. AD-477g grandchild added with explicit forcing function.
- Section 2 docstring: aggregation list rewritten to 3 sources, dream bullet removed.
- DECISIONS.md draft entry: deferral list extended to include AD-477g.
- No infra ask introduced (pre-deferral honesty preserved per Wave 9B convention #21).

**Required #2 (episodic by-date pattern specified):**
- Section 2 `generate_for_date` docstring now specifies the exact pattern: `recent(k=top_episodes_count*4)` over-fetch → Python-side `[date_start, date_end]` UTC-midnight filter + `importance >= threshold` filter → sort by importance desc → take top N. Rationale (no by-date query exists) included.
- Verified Against Codebase footer adds `episodic.py:1832` evidence and the explicit "no by-date query exists" call-out.

**Required #3 (status value enum drift):**
- `status="pending"` → `status="open"` in Section 3 docstring (was 1 site).
- Test #8 renamed `test_plan_of_day_aggregates_pending_work_items` → `test_plan_of_day_aggregates_open_work_items`.
- Section 2 docstring (newly added work-item bullet) uses `status="open"` from the start.
- Verified Against Codebase footer adds `WorkItemStatus` enum-member evidence and the AD-685c/e kwarg-value tooling-hygiene candidate note.

**Recommended #1 (Pydantic factory defaults):** `NavalOrganizationConfig` now uses `Field(default_factory=CaptainsLogConfig)` and `Field(default_factory=PlanOfDayConfig)` per Wave 5 anti-pattern guidance.

**Recommended #2 (verified-against-codebase grep audit):** Footer rewritten to include grep evidence for: `WorkItemStatus` members, `episodic.recent`, `ward_room.list_threads`, and the explicit zero-hit confirmation for `runtime.dreaming_engine`. Convention #16 mandatory pre-check audit trail satisfied.

**Recommended #3 (test #12 tightening):** Renamed `test_stop_cancels_task_cleanly` → `test_stop_surfaces_cancellederror_to_caller_after_cleanup` with explicit `raises(CancelledError)` requirement in the description. Prevents silent-swallow Builder regression.

**Nit #1:** Public-attribute convention #1 callout extended to start-task attributes in Section 5 wiring text.

**Nits #2-3 deliberately not applied:** Nit #2 (drop defensive `getattr` in finalize.py) is fine to keep for the staged rollout; Nit #3 (test count contingency) is moot under Required #1 path (a).

**Beyond-review structural defects:** None discovered during verify-first re-grep.

**Hard-Stop #1 retired:** Removed (replaced by AD-477g deferral). Remaining hard-stops are real build-time risks.
