Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion skills/nelson/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ When The Estimate has been conducted, the Battle Plan inherits the analytical wo
- For each task, consciously mark `admiralty-action-required: yes` or `no`.
- Keep one task in progress per agent unless the mission explicitly requires multitasking.

Reference `references/admiralty-templates/battle-plan.md` for the battle plan template and `references/admiralty-templates/ship-manifest.md` for the ship manifest.
Reference `references/admiralty-templates/battle-plan.md` for the schema of each captain's brief and `references/admiralty-templates/ship-manifest.md` for the ship manifest.

**Battle Plan Gate — Standing Order Check:** You MUST NOT finalize task assignments until each question below is answered in writing and any triggered standing order remedy has been applied. Show your reasoning — a bare yes/no is not sufficient.
- `becalmed-fleet.md`: Should this mission use single-session instead of multi-agent? If yes, skip Step 4 — single-session has no squadron to form.
Expand All @@ -124,6 +124,8 @@ Reference `references/admiralty-templates/battle-plan.md` for the battle plan te

If any answer triggers a standing order, you MUST apply the corrective action and re-answer the question before proceeding. For situations not covered by this gate, consult the Standing Orders table below.

**Persist the drafted plan.** Before proceeding to Step 4, write the complete battle plan to `{mission-dir}/battle-plan.md` using the template at `references/admiralty-templates/battle-plan.md`. Include the commander's intent verbatim from Estimate §2, each task brief in template form, and the Standing Order Check answers. **This is a safe compaction point — admiral state is now fully on disk.**

**Structured Data Capture:** Task registration requires owners, which are assigned in Step 4. No `nelson-data.py` script calls at this step.

## 4. Form the Squadron
Expand Down Expand Up @@ -367,6 +369,7 @@ Consult the specific procedure that matches the situation.
## Admiralty Doctrine

- Include this instruction in any admiral's compaction summary: Re-read the quarterdeck report at the mission directory path to recover `{mission-dir}`. If the path is unknown, read `.nelson/.active-{SESSION_ID}` if you know the SESSION_ID, otherwise list `.nelson/missions/` and present the options to the user for selection. Then re-read `references/standing-orders/admiral-at-the-helm.md` to confirm you are in coordination role.
- Treat `/compact` as safe at any phase boundary (after Step 1, 2, 3, 4, and at every quarterdeck checkpoint in Step 6). The narrow unsafe window is inside Step 5 — between user approval and the admiral's `permission_granted` / phase advance / agent spawn turn.
- Optimize for mission throughput, not equal work distribution.
- Prefer replacing stalled agents over waiting on undefined blockers.
- Recognise strong performance; motivation compounds across missions.
Expand Down
2 changes: 2 additions & 0 deletions skills/nelson/references/admiralty-templates/battle-plan.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Battle Plan Template

The rendered plan lives at `{mission-dir}/battle-plan.md` and is the prose authority for the mission — commander's intent and per-task briefs. The structured form at `{mission-dir}/battle-plan.json` is the execution-data authority (owners, dependencies, station tiers, file ownership). Keep them aligned: edit one, mirror the change in the other.

Every captain's brief opens with the commander's intent from the Estimate (§2) — one paragraph, verbatim. This is how each ship sails under a shared understanding of purpose.

```text
Expand Down
2 changes: 2 additions & 0 deletions skills/nelson/references/damage-control/session-resumption.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,5 @@ Use when a session is interrupted (context limit, crash, timeout) and work must
7. Re-issue sailing orders with the original mission outcome and updated scope reflecting completed work.
8. Re-form the squadron at the minimum size needed for remaining tasks.
9. Resume quarterdeck rhythm from the next scheduled checkpoint.

**Safe compaction windows.** State is fully persisted at every phase boundary: after Sailing Orders (Step 1), after the Estimate (Step 2), after the Battle Plan is drafted to disk (Step 3), after Formation (Step 4), and at every quarterdeck checkpoint (Step 6). The one unsafe window is *inside* Step 5: between the user granting permission and the admiral logging `permission_granted` + advancing to UNDERWAY + spawning agents — that whole sequence is a single tightly coupled turn.
85 changes: 75 additions & 10 deletions skills/nelson/scripts/nelson_data_lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,48 @@
_CONFLICT_SCAN_SCRIPT = Path(__file__).resolve().parent / "nelson_conflict_scan.py"


# Per-phase recovery guidance for `cmd_recover`. UNDERWAY is intentionally
# omitted: when the mission is underway, recovery uses handoff-packet-derived
# actions instead of static guidance.
PHASE_RECOVERY_GUIDANCE: dict[str, list[str]] = {
"SAILING_ORDERS": [
"You are in SAILING_ORDERS phase (Step 1).",
"Read sailing-orders.json for outcome, metric, deadline, and constraints.",
"Decide with the user whether to conduct The Estimate before drafting the Battle Plan.",
],
"ESTIMATE": [
"You are in ESTIMATE phase (Step 2).",
"Read sailing-orders.json and any partial estimate.md to resume the seven-question Maritime Tactical Estimate.",
"Continue from the next unanswered question; honour the Q1 and Q3 checkpoints with the user.",
],
"BATTLE_PLAN": [
"You are in BATTLE_PLAN phase (Step 3).",
"Read estimate.md for commander's intent and effects, and battle-plan.md for any drafted plan.",
"Complete the Standing Order Check, then persist the final plan to {mission-dir}/battle-plan.md before advancing to Step 4.",
],
"FORMATION": [
"You are in FORMATION phase (Step 4).",
"Read battle-plan.json for tasks and squadron, and battle-plan.md for the prose plan.",
"Confirm formation orders with the user before advancing to PERMISSION.",
],
"PERMISSION": [
"You are in PERMISSION phase (Step 5).",
"Read battle-plan.json and battle-plan.md, then re-display the complete battle plan and squadron formation.",
"Wait for explicit user permission before logging permission_granted and advancing to UNDERWAY.",
],
"STAND_DOWN": [
"You are in STAND_DOWN phase (Step 8).",
"Read stand-down.json for the recorded mission summary.",
"Verify {mission-dir}/captains-log.md exists and remove .nelson/.active-{SESSION_ID} if still present.",
],
}


BATTLE_PLAN_MD_REQUIRED_PHASES: frozenset[str] = frozenset(
{"BATTLE_PLAN", "FORMATION", "PERMISSION"}
)


# ---------------------------------------------------------------------------
# Internal helper: _do_init (used by cmd_init and cmd_headless)
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -1717,17 +1759,38 @@ def _build_recovery_briefing(
}
)

current_phase = (
fleet_status.get("mission", {}).get("phase", "unknown")
if fleet_status
else "unknown"
)

recommended_actions: list[str] = []
for pkt in handoff_packets:
ship = pkt.get("ship_name", "unknown")
task_id = pkt.get("task_id")
recommended_actions.append(
f"Resume task {task_id} from handoff packet ({ship})"
)
if not recommended_actions:
recommended_actions.append(
"No handoff packets found — review fleet-status.json for current state"
)
# Phase guidance takes precedence over handoff packets when both exist —
# an anomalous mid-flight state, but if it occurs the phase wins because
# it reflects the admiral's last recorded position in the workflow.
if current_phase in PHASE_RECOVERY_GUIDANCE:
recommended_actions = list(PHASE_RECOVERY_GUIDANCE[current_phase])
if (
current_phase in BATTLE_PLAN_MD_REQUIRED_PHASES
and not (mission_dir / "battle-plan.md").is_file()
):
recommended_actions.insert(
0,
"WARNING: battle-plan.md is missing — rebuild from estimate.md",
)
else:
# UNDERWAY (and any unknown phase): derive actions from handoff packets.
for pkt in handoff_packets:
ship = pkt.get("ship_name", "unknown")
task_id = pkt.get("task_id")
recommended_actions.append(
f"Resume task {task_id} from handoff packet ({ship})"
)
if not recommended_actions:
recommended_actions.append(
"No handoff packets found — review fleet-status.json for current state"
)

return {
"mission_dir": str(mission_dir),
Expand All @@ -1736,6 +1799,7 @@ def _build_recovery_briefing(
if fleet_status
else "unknown"
),
"current_phase": current_phase,
"fleet_status": fleet_status,
"handoff_packets": handoff_packets,
"pending_tasks": pending_tasks,
Expand All @@ -1748,6 +1812,7 @@ def _format_recovery_text(briefing: dict) -> str:
lines: list[str] = []
lines.append(f"[nelson-data] Recovery briefing for {briefing['mission_dir']}")
lines.append(f" Status: {briefing['mission_status']}")
lines.append(f" Phase: {briefing.get('current_phase', 'unknown')}")
lines.append("")

fs = briefing.get("fleet_status")
Expand Down
56 changes: 56 additions & 0 deletions skills/nelson/scripts/test_nelson_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
import subprocess
from pathlib import Path

import pytest

from conftest import (
add_squadron,
add_task,
Expand All @@ -19,6 +21,18 @@
read_json,
run,
)
from nelson_data_lifecycle import (
BATTLE_PLAN_MD_REQUIRED_PHASES,
PHASE_RECOVERY_GUIDANCE,
)


def _set_mission_phase(mission_dir: Path, phase: str) -> None:
"""Test helper: rewrite fleet-status.json with the requested mission.phase."""
fs_path = mission_dir / "fleet-status.json"
fs = json.loads(fs_path.read_text(encoding="utf-8"))
fs.setdefault("mission", {})["phase"] = phase
fs_path.write_text(json.dumps(fs, indent=2), encoding="utf-8")


# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -1428,6 +1442,44 @@ def test_recover_text_output(self, tmp_path: Path) -> None:
result = run("recover", "--mission-dir", str(mission_dir), "--format", "text")
assert "[nelson-data] Recovery briefing" in result.stdout
assert "HMS Argyll" in result.stdout
assert "Phase: " in result.stdout

def test_recover_includes_phase_in_briefing(self, tmp_path: Path) -> None:
mission_dir = setup_mission_with_task(tmp_path)
result = run("recover", "--mission-dir", str(mission_dir))
briefing = json.loads(result.stdout)
assert "current_phase" in briefing
assert briefing["current_phase"]

@pytest.mark.parametrize("phase", sorted(PHASE_RECOVERY_GUIDANCE.keys()))
def test_recover_phase_specific_actions(
self, tmp_path: Path, phase: str
) -> None:
mission_dir = setup_mission_with_task(tmp_path)
# Pre-create battle-plan.md so phases that check for it don't add a warning.
(mission_dir / "battle-plan.md").write_text("# Battle Plan\n", encoding="utf-8")
_set_mission_phase(mission_dir, phase)
result = run("recover", "--mission-dir", str(mission_dir))
briefing = json.loads(result.stdout)
assert briefing["current_phase"] == phase
assert briefing["recommended_actions"] == PHASE_RECOVERY_GUIDANCE[phase]

@pytest.mark.parametrize("phase", sorted(BATTLE_PLAN_MD_REQUIRED_PHASES))
def test_recover_warns_when_battle_plan_md_missing(
self, tmp_path: Path, phase: str
) -> None:
mission_dir = setup_mission_with_task(tmp_path)
_set_mission_phase(mission_dir, phase)
# Ensure battle-plan.md is absent
bp_md = mission_dir / "battle-plan.md"
if bp_md.exists():
bp_md.unlink()
result = run("recover", "--mission-dir", str(mission_dir))
briefing = json.loads(result.stdout)
assert any(
"battle-plan.md is missing" in action
for action in briefing["recommended_actions"]
), briefing["recommended_actions"]

def test_recover_no_active_mission_silent(self, tmp_path: Path) -> None:
missions_dir = tmp_path / ".nelson" / "missions"
Expand Down Expand Up @@ -1474,6 +1526,10 @@ def test_full_handoff_lifecycle(self, tmp_path: Path) -> None:
],
)

# Handoffs happen during UNDERWAY — recovery should fall back to
# handoff-packet-derived actions in that phase.
_set_mission_phase(mission_dir, "UNDERWAY")

# Recover
result = run("recover", "--mission-dir", str(mission_dir))
briefing = json.loads(result.stdout)
Expand Down
Loading