From 26ae592e90ab3559b7b11964cac62ce10f275046 Mon Sep 17 00:00:00 2001 From: harrymunro Date: Fri, 8 May 2026 13:49:16 -0300 Subject: [PATCH 1/2] feat: restore TaskCreate captain-misuse enforcement via admiral session marker PR #116 removed the PreToolUse:TaskCreate mode-check hook because it blanket-rejected the admiral's own Ctrl+T visibility tracking. This reinstates enforcement properly by discriminating admiral from captain via the hook payload's transcript_path (each subagent gets its own). - session-init (SessionStart): records admiral transcript_path to .nelson/admiral.session - session-check (PreToolUse:TaskCreate): rejects with wrong-ensign only when mode is subagents/single-session and the payload transcript_path does not match the recorded admiral marker - preflight: opportunistically backfills the marker if init ran after SessionStart fired - stand-down: best-effort marker cleanup - All paths fail-open on uncertainty (missing marker, empty transcript, no mission), so non-Nelson projects and edge cases are unaffected --- README.md | 2 + hooks/hooks.json | 19 +++ hooks/nelson_hooks.py | 116 ++++++++++++++ hooks/test_nelson_hooks.py | 150 ++++++++++++++++++ .../nelson/scripts/nelson_data_lifecycle.py | 9 ++ skills/nelson/scripts/test_nelson_data.py | 36 +++++ 6 files changed, 332 insertions(+) diff --git a/README.md b/README.md index 326564b..7cedd4e 100644 --- a/README.md +++ b/README.md @@ -210,9 +210,11 @@ Nelson is not purely advisory. A set of Claude Code hooks (`hooks/nelson_hooks.p | Event | Hook | What it enforces | |---|---|---| | `PreToolUse` on `Agent` | `preflight` | Station tier gate, file ownership conflicts, mode-tool consistency | +| `PreToolUse` on `TaskCreate` | `session-check` | Captain TaskCreate gate (admiral exception via session marker) | | `PostToolUse` on `Write`/`Edit` | `brief-validate` | Turnover brief quality gate | | `TaskCompleted` | `task-complete` | Validation evidence and station controls | | `TeammateIdle` | `idle-ship` | Paid-off standing order advisory | +| `SessionStart` | `session-init` | Records admiral `transcript_path` for the TaskCreate gate | Plugin installs auto-discover `hooks/hooks.json` and wire these up with no user action. Hooks degrade gracefully: if no active Nelson mission is found, they exit cleanly and do not interfere with non-Nelson workflows. See [Installation](#installation) for manual-install caveats. diff --git a/hooks/hooks.json b/hooks/hooks.json index 27df8ef..04ca036 100644 --- a/hooks/hooks.json +++ b/hooks/hooks.json @@ -1,5 +1,15 @@ { "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "python3 \"${CLAUDE_PLUGIN_ROOT}/hooks/nelson_hooks.py\" session-init" + } + ] + } + ], "PreToolUse": [ { "matcher": "Agent", @@ -9,6 +19,15 @@ "command": "python3 \"${CLAUDE_PLUGIN_ROOT}/hooks/nelson_hooks.py\" preflight" } ] + }, + { + "matcher": "TaskCreate", + "hooks": [ + { + "type": "command", + "command": "python3 \"${CLAUDE_PLUGIN_ROOT}/hooks/nelson_hooks.py\" session-check" + } + ] } ], "PostToolUse": [ diff --git a/hooks/nelson_hooks.py b/hooks/nelson_hooks.py index 8866799..e673a06 100644 --- a/hooks/nelson_hooks.py +++ b/hooks/nelson_hooks.py @@ -10,6 +10,11 @@ brief-validate — PostToolUse on Write/Edit: turnover brief quality gate task-complete — TaskCompleted: validation evidence and station controls idle-ship — TeammateIdle: paid-off standing order advisory + session-init — SessionStart: record admiral transcript_path for the + TaskCreate captain-misuse gate + session-check — PreToolUse on TaskCreate: reject captain TaskCreate calls + in subagents/single-session mode (admiral exception via + the marker written by session-init) Exit codes: 0 — allow (action proceeds) @@ -36,6 +41,9 @@ # --------------------------------------------------------------------------- +ADMIRAL_SESSION_MARKER = "admiral.session" + + def _read_stdin() -> dict[str, Any]: """Parse JSON from stdin (Claude Code hook payload).""" try: @@ -116,6 +124,25 @@ def _load_mission_context( return mission_dir, bp +def _write_admiral_marker(nelson_dir: Path, transcript_path: str) -> bool: + """Write the admiral session marker. Returns True on success, False on failure. + + The marker stores the admiral's transcript_path so cmd_session_check can + distinguish admiral calls (match) from captain subagent calls (mismatch). + """ + if not nelson_dir.is_dir(): + return False + if not transcript_path.strip(): + return False + try: + (nelson_dir / ADMIRAL_SESSION_MARKER).write_text( + transcript_path.strip() + "\n", encoding="utf-8", + ) + return True + except OSError: + return False + + # --------------------------------------------------------------------------- # Preflight helpers # --------------------------------------------------------------------------- @@ -209,6 +236,14 @@ def cmd_preflight(args: argparse.Namespace) -> None: tasks = _get_tasks(battle_plan) tool_input = payload.get("tool_input", {}) + # Opportunistic admiral marker backfill: if init ran after SessionStart, + # the marker won't have been written. The admiral always fires PreToolUse + # on Agent before spawning captains, so this is a safe write point. + nelson_dir = Path(payload.get("cwd", ".")) / ".nelson" + marker = nelson_dir / ADMIRAL_SESSION_MARKER + if not marker.is_file(): + _write_admiral_marker(nelson_dir, payload.get("transcript_path", "")) + for check in ( lambda: _check_station_tiers(tasks), lambda: _check_file_ownership(tasks), @@ -627,6 +662,77 @@ def _clear_idle_tracker(mission_dir: Path, ship_name: str) -> None: clear_idle_tracker(mission_dir, ship_name) +# --------------------------------------------------------------------------- +# Subcommand: session-init (SessionStart) +# --------------------------------------------------------------------------- + + +def cmd_session_init(args: argparse.Namespace) -> None: + """SessionStart event: record admiral identity for TaskCreate enforcement. + + Writes the payload's transcript_path to .nelson/admiral.session so the + PreToolUse:TaskCreate hook can distinguish admiral (match) from captain + subagents (mismatch). No-op when .nelson/ does not yet exist — non-Nelson + projects are unaffected, and Nelson projects whose mission has not been + initialised will be backfilled by cmd_preflight on the first Agent spawn. + """ + payload = _read_stdin() + cwd = Path(payload.get("cwd", ".")) + _write_admiral_marker(cwd / ".nelson", payload.get("transcript_path", "")) + _allow() + + +# --------------------------------------------------------------------------- +# Subcommand: session-check (PreToolUse on TaskCreate) +# --------------------------------------------------------------------------- + + +def cmd_session_check(args: argparse.Namespace) -> None: + """PreToolUse:TaskCreate gate using admiral session marker. + + Allows TaskCreate when: + - no active Nelson mission (graceful degradation) + - mode is agent-team (TaskCreate is the shared coordination surface) + - admiral.session marker missing (fail-open, never had a chance to record) + - payload transcript_path matches the marker (admiral) + - payload transcript_path missing (defensive fail-open) + + Rejects with wrong-ensign violation only when mode is subagents or + single-session AND the payload transcript_path does not match the + recorded admiral transcript (i.e. captain subagent context). + """ + payload = _read_stdin() + ctx = _load_mission_context(payload) + if ctx is None: + _allow() + + _, battle_plan = ctx + mode = _get_mode(battle_plan) + if mode == "agent-team": + _allow() + + nelson_dir = Path(payload.get("cwd", ".")) / ".nelson" + marker = nelson_dir / ADMIRAL_SESSION_MARKER + if not marker.is_file(): + _allow() + + try: + admiral_transcript = marker.read_text(encoding="utf-8").strip() + except OSError: + _allow() + + payload_transcript = payload.get("transcript_path", "").strip() + if payload_transcript and payload_transcript != admiral_transcript: + _reject( + "Standing order violation (wrong-ensign): " + f"TaskCreate is reserved for the admiral in {mode} mode. " + "Captains report progress via Agent return value, not the task list. " + "See references/tool-mapping.md." + ) + + _allow() + + # --------------------------------------------------------------------------- # CLI entry point # --------------------------------------------------------------------------- @@ -655,6 +761,14 @@ def main() -> None: "idle-ship", help="Idle ship advisory (TeammateIdle)", ) + subparsers.add_parser( + "session-init", + help="Record admiral transcript_path on session start", + ) + subparsers.add_parser( + "session-check", + help="Captain TaskCreate gate (PreToolUse on TaskCreate)", + ) args = parser.parse_args() @@ -663,6 +777,8 @@ def main() -> None: "brief-validate": cmd_brief_validate, "task-complete": cmd_task_complete, "idle-ship": cmd_idle_ship, + "session-init": cmd_session_init, + "session-check": cmd_session_check, } handler = dispatch.get(args.command) diff --git a/hooks/test_nelson_hooks.py b/hooks/test_nelson_hooks.py index 9cb6670..43e05a6 100644 --- a/hooks/test_nelson_hooks.py +++ b/hooks/test_nelson_hooks.py @@ -24,6 +24,7 @@ from conftest import VALID_FLAGSHIP_BRIEF, VALID_STANDARD_BRIEF # noqa: E402 from nelson_hooks import ( # noqa: E402 + ADMIRAL_SESSION_MARKER, ROLLBACK_PATTERNS, VALIDATION_EVIDENCE_PATTERNS, _check_running_plot_nonempty, @@ -35,6 +36,8 @@ cmd_brief_validate, cmd_idle_ship, cmd_preflight, + cmd_session_check, + cmd_session_init, cmd_task_complete, ) @@ -502,3 +505,150 @@ def test_unknown_ship_advises_check( _make_mission(tmp_path, fleet_status={"squadron": []}) _run(cmd_idle_ship, {"teammate_name": "HMS Unknown"}, str(tmp_path)) assert "not found" in capsys.readouterr().err.lower() + + +# --------------------------------------------------------------------------- +# Session-init (SessionStart) +# --------------------------------------------------------------------------- + + +class TestSessionInit: + def test_no_nelson_dir_allows_and_no_write(self, tmp_path: Path) -> None: + """Non-Nelson project: no .nelson/ exists, hook is a no-op (allow).""" + code = _run( + cmd_session_init, + {"transcript_path": "/tmp/x.jsonl"}, + cwd=str(tmp_path), + ) + assert code == 0 + assert not (tmp_path / ".nelson" / ADMIRAL_SESSION_MARKER).exists() + + def test_writes_admiral_session_marker(self, tmp_path: Path) -> None: + (tmp_path / ".nelson").mkdir() + code = _run( + cmd_session_init, + {"transcript_path": "/transcripts/admiral.jsonl"}, + cwd=str(tmp_path), + ) + assert code == 0 + marker = tmp_path / ".nelson" / ADMIRAL_SESSION_MARKER + assert marker.is_file() + assert marker.read_text(encoding="utf-8").strip() == "/transcripts/admiral.jsonl" + + def test_overwrites_existing_marker_on_session_resume( + self, tmp_path: Path, + ) -> None: + (tmp_path / ".nelson").mkdir() + marker = tmp_path / ".nelson" / ADMIRAL_SESSION_MARKER + marker.write_text("/old/transcript.jsonl\n", encoding="utf-8") + code = _run( + cmd_session_init, + {"transcript_path": "/new/transcript.jsonl"}, + cwd=str(tmp_path), + ) + assert code == 0 + assert marker.read_text(encoding="utf-8").strip() == "/new/transcript.jsonl" + + def test_missing_transcript_path_is_no_op(self, tmp_path: Path) -> None: + (tmp_path / ".nelson").mkdir() + code = _run(cmd_session_init, {}, cwd=str(tmp_path)) + assert code == 0 + assert not (tmp_path / ".nelson" / ADMIRAL_SESSION_MARKER).exists() + + +# --------------------------------------------------------------------------- +# Session-check (PreToolUse on TaskCreate) +# --------------------------------------------------------------------------- + + +def _write_marker(tmp_path: Path, transcript: str) -> None: + """Helper: write the admiral session marker for tests.""" + nelson_dir = tmp_path / ".nelson" + nelson_dir.mkdir(exist_ok=True) + (nelson_dir / ADMIRAL_SESSION_MARKER).write_text( + transcript + "\n", encoding="utf-8", + ) + + +class TestSessionCheck: + def test_no_mission_allows(self, tmp_path: Path) -> None: + _write_marker(tmp_path, "/admiral.jsonl") + code = _run( + cmd_session_check, + {"transcript_path": "/captain.jsonl"}, + cwd=str(tmp_path), + ) + assert code == 0 + + def test_agent_team_mode_allows(self, tmp_path: Path) -> None: + _make_mission(tmp_path, mode="agent-team") + _write_marker(tmp_path, "/admiral.jsonl") + code = _run( + cmd_session_check, + {"transcript_path": "/captain.jsonl"}, + cwd=str(tmp_path), + ) + assert code == 0 + + def test_marker_missing_allows(self, tmp_path: Path) -> None: + """Graceful degradation: missing marker means allow.""" + _make_mission(tmp_path, mode="subagents") + code = _run( + cmd_session_check, + {"transcript_path": "/anyone.jsonl"}, + cwd=str(tmp_path), + ) + assert code == 0 + + def test_admiral_match_allows_subagents_mode(self, tmp_path: Path) -> None: + _make_mission(tmp_path, mode="subagents") + _write_marker(tmp_path, "/admiral.jsonl") + code = _run( + cmd_session_check, + {"transcript_path": "/admiral.jsonl"}, + cwd=str(tmp_path), + ) + assert code == 0 + + def test_admiral_match_allows_single_session_mode( + self, tmp_path: Path, + ) -> None: + _make_mission(tmp_path, mode="single-session") + _write_marker(tmp_path, "/admiral.jsonl") + code = _run( + cmd_session_check, + {"transcript_path": "/admiral.jsonl"}, + cwd=str(tmp_path), + ) + assert code == 0 + + def test_captain_mismatch_rejects_subagents_mode( + self, tmp_path: Path, + ) -> None: + _make_mission(tmp_path, mode="subagents") + _write_marker(tmp_path, "/admiral.jsonl") + code = _run( + cmd_session_check, + {"transcript_path": "/captain.jsonl"}, + cwd=str(tmp_path), + ) + assert code == 2 + + def test_captain_mismatch_rejects_single_session_mode( + self, tmp_path: Path, + ) -> None: + _make_mission(tmp_path, mode="single-session") + _write_marker(tmp_path, "/admiral.jsonl") + code = _run( + cmd_session_check, + {"transcript_path": "/captain.jsonl"}, + cwd=str(tmp_path), + ) + assert code == 2 + + def test_missing_transcript_path_allows(self, tmp_path: Path) -> None: + """Defensive fail-open: payload with no transcript_path is allowed.""" + _make_mission(tmp_path, mode="subagents") + _write_marker(tmp_path, "/admiral.jsonl") + code = _run(cmd_session_check, {}, cwd=str(tmp_path)) + assert code == 0 diff --git a/skills/nelson/scripts/nelson_data_lifecycle.py b/skills/nelson/scripts/nelson_data_lifecycle.py index 039db22..83a7759 100644 --- a/skills/nelson/scripts/nelson_data_lifecycle.py +++ b/skills/nelson/scripts/nelson_data_lifecycle.py @@ -1029,6 +1029,15 @@ def cmd_stand_down(args: argparse.Namespace) -> None: except Exception as exc: _err(f"Warning: failed to update memory store: {exc}") + # Best-effort cleanup of admiral session marker (mission-scoped lifecycle). + # Marker lives at .nelson/admiral.session, two levels up from mission_dir. + try: + (mission_dir.parent.parent / "admiral.session").unlink() + except FileNotFoundError: + pass + except OSError: + pass + # Print mission summary achieved = "ACHIEVED" if args.outcome_achieved else "NOT ACHIEVED" print( diff --git a/skills/nelson/scripts/test_nelson_data.py b/skills/nelson/scripts/test_nelson_data.py index f9d8b34..38e47c3 100644 --- a/skills/nelson/scripts/test_nelson_data.py +++ b/skills/nelson/scripts/test_nelson_data.py @@ -702,6 +702,42 @@ def test_writes_final_fleet_status(self, tmp_path: Path) -> None: fs = read_json(mission_dir / "fleet-status.json") assert fs["mission"]["status"] == "complete" + def test_removes_admiral_session_marker(self, tmp_path: Path) -> None: + """Admiral session marker is cleaned up at stand-down.""" + mission_dir = init_mission(tmp_path) + add_squadron(mission_dir) + add_task(mission_dir) + run("plan-approved", "--mission-dir", str(mission_dir)) + marker = tmp_path / ".nelson" / "admiral.session" + marker.write_text("/transcripts/admiral.jsonl\n", encoding="utf-8") + assert marker.exists() + run( + "stand-down", + "--mission-dir", str(mission_dir), + "--outcome-achieved", + "--actual-outcome", "Done", + "--metric-result", "Pass", + ) + assert not marker.exists() + + def test_stand_down_succeeds_without_admiral_session_marker( + self, tmp_path: Path, + ) -> None: + """Cleanup is best-effort: missing marker must not fail stand-down.""" + mission_dir = init_mission(tmp_path) + add_squadron(mission_dir) + add_task(mission_dir) + run("plan-approved", "--mission-dir", str(mission_dir)) + marker = tmp_path / ".nelson" / "admiral.session" + assert not marker.exists() + run( + "stand-down", + "--mission-dir", str(mission_dir), + "--outcome-achieved", + "--actual-outcome", "Done", + "--metric-result", "Pass", + ) + # --------------------------------------------------------------------------- # Status From 7a1117c0eb76b9fb1a8dda5f337d77c174ec2b99 Mon Sep 17 00:00:00 2001 From: harrymunro Date: Fri, 8 May 2026 14:50:55 -0300 Subject: [PATCH 2/2] fix: tighten admiral session marker fail-open posture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address PR #119 review feedback on cmd_session_check: - Mode gate now uses an explicit CAPTAIN_GATED_MODES allow-list (subagents, single-session). Previously any non-agent-team mode would reject on transcript mismatch, contradicting the docstring and pre-emptively gating future modes. - Empty admiral.session marker now fails open. Previously a partially written or interrupted marker would mismatch every payload and reject everything — including the admiral. - Share ADMIRAL_SESSION_MARKER constant via nelson_data_utils so hooks/ and skills/scripts/ reference one filename, not two. - Add regression tests for both fail-open paths and two unit tests for the preflight backfill path (previously untested). Tests: 64/64 hook tests, 280/280 lifecycle tests, references green. --- hooks/nelson_hooks.py | 29 ++++--- hooks/test_nelson_hooks.py | 83 +++++++++++++++++++ .../nelson/scripts/nelson_data_lifecycle.py | 6 +- skills/nelson/scripts/nelson_data_utils.py | 4 + 4 files changed, 110 insertions(+), 12 deletions(-) diff --git a/hooks/nelson_hooks.py b/hooks/nelson_hooks.py index e673a06..f0815db 100644 --- a/hooks/nelson_hooks.py +++ b/hooks/nelson_hooks.py @@ -42,6 +42,8 @@ ADMIRAL_SESSION_MARKER = "admiral.session" +# NOTE: must stay in sync with +# skills/nelson/scripts/nelson_data_utils.py:ADMIRAL_SESSION_MARKER. def _read_stdin() -> dict[str, Any]: @@ -687,19 +689,23 @@ def cmd_session_init(args: argparse.Namespace) -> None: # --------------------------------------------------------------------------- +CAPTAIN_GATED_MODES = frozenset({"subagents", "single-session"}) + + def cmd_session_check(args: argparse.Namespace) -> None: """PreToolUse:TaskCreate gate using admiral session marker. - Allows TaskCreate when: + Rejects with wrong-ensign violation only when mode is in + CAPTAIN_GATED_MODES (subagents, single-session) AND the payload + transcript_path does not match the recorded admiral transcript + (i.e. captain subagent context). + + Fails open in every other case: - no active Nelson mission (graceful degradation) - - mode is agent-team (TaskCreate is the shared coordination surface) - - admiral.session marker missing (fail-open, never had a chance to record) - - payload transcript_path matches the marker (admiral) - - payload transcript_path missing (defensive fail-open) - - Rejects with wrong-ensign violation only when mode is subagents or - single-session AND the payload transcript_path does not match the - recorded admiral transcript (i.e. captain subagent context). + - mode not in CAPTAIN_GATED_MODES (agent-team, future modes) + - admiral.session marker missing (never had a chance to record) + - admiral.session marker empty (interrupted write) + - payload transcript_path missing (defensive) """ payload = _read_stdin() ctx = _load_mission_context(payload) @@ -708,7 +714,7 @@ def cmd_session_check(args: argparse.Namespace) -> None: _, battle_plan = ctx mode = _get_mode(battle_plan) - if mode == "agent-team": + if mode not in CAPTAIN_GATED_MODES: _allow() nelson_dir = Path(payload.get("cwd", ".")) / ".nelson" @@ -721,6 +727,9 @@ def cmd_session_check(args: argparse.Namespace) -> None: except OSError: _allow() + if not admiral_transcript: + _allow() + payload_transcript = payload.get("transcript_path", "").strip() if payload_transcript and payload_transcript != admiral_transcript: _reject( diff --git a/hooks/test_nelson_hooks.py b/hooks/test_nelson_hooks.py index 43e05a6..86b981a 100644 --- a/hooks/test_nelson_hooks.py +++ b/hooks/test_nelson_hooks.py @@ -652,3 +652,86 @@ def test_missing_transcript_path_allows(self, tmp_path: Path) -> None: _write_marker(tmp_path, "/admiral.jsonl") code = _run(cmd_session_check, {}, cwd=str(tmp_path)) assert code == 0 + + def test_empty_marker_allows(self, tmp_path: Path) -> None: + """Empty marker (interrupted write) must fail-open — never reject.""" + _make_mission(tmp_path, mode="subagents") + nelson_dir = tmp_path / ".nelson" + (nelson_dir / ADMIRAL_SESSION_MARKER).write_text("", encoding="utf-8") + code = _run( + cmd_session_check, + {"transcript_path": "/admiral.jsonl"}, + cwd=str(tmp_path), + ) + assert code == 0 + + def test_unknown_mode_allows(self, tmp_path: Path) -> None: + """Unknown/future modes must fail-open: only explicit captain modes gate.""" + _make_mission(tmp_path, mode="future-mode-xyz") + _write_marker(tmp_path, "/admiral.jsonl") + code = _run( + cmd_session_check, + {"transcript_path": "/captain.jsonl"}, + cwd=str(tmp_path), + ) + assert code == 0 + + +# --------------------------------------------------------------------------- +# Preflight admiral-marker backfill +# --------------------------------------------------------------------------- + + +class TestPreflightAdmiralMarkerBackfill: + def test_backfill_writes_marker_when_missing(self, tmp_path: Path) -> None: + """Preflight backfills admiral marker when SessionStart missed it.""" + _make_mission(tmp_path, tasks=[ + { + "id": "t1", + "name": "Task 1", + "owner": "HMS Daring", + "station_tier": 1, + "file_ownership": ["src/auth.py"], + }, + ]) + marker = tmp_path / ".nelson" / ADMIRAL_SESSION_MARKER + assert not marker.exists() + code = _run( + cmd_preflight, + { + "tool_name": "Agent", + "tool_input": {"subagent_type": "general-purpose"}, + "transcript_path": "/transcripts/admiral.jsonl", + }, + cwd=str(tmp_path), + ) + assert code == 0 + assert marker.is_file() + assert marker.read_text(encoding="utf-8").strip() == "/transcripts/admiral.jsonl" + + def test_backfill_does_not_overwrite_existing_marker( + self, tmp_path: Path, + ) -> None: + """Existing marker is preserved — only writes when missing.""" + _make_mission(tmp_path, tasks=[ + { + "id": "t1", + "name": "Task 1", + "owner": "HMS Daring", + "station_tier": 1, + "file_ownership": ["src/auth.py"], + }, + ]) + marker = tmp_path / ".nelson" / ADMIRAL_SESSION_MARKER + marker.write_text("/original/admiral.jsonl\n", encoding="utf-8") + code = _run( + cmd_preflight, + { + "tool_name": "Agent", + "tool_input": {"subagent_type": "general-purpose"}, + "transcript_path": "/different/path.jsonl", + }, + cwd=str(tmp_path), + ) + assert code == 0 + assert marker.read_text(encoding="utf-8").strip() == "/original/admiral.jsonl" diff --git a/skills/nelson/scripts/nelson_data_lifecycle.py b/skills/nelson/scripts/nelson_data_lifecycle.py index 83a7759..9894534 100644 --- a/skills/nelson/scripts/nelson_data_lifecycle.py +++ b/skills/nelson/scripts/nelson_data_lifecycle.py @@ -26,6 +26,7 @@ ) from nelson_data_memory import _update_patterns_store, _update_standing_order_stats from nelson_data_utils import ( + ADMIRAL_SESSION_MARKER, FLEET_STATUS_EVENT_TYPES, FLEET_STATUS_STALENESS_THRESHOLD_SECONDS, JSON_INDENT, @@ -1030,9 +1031,10 @@ def cmd_stand_down(args: argparse.Namespace) -> None: _err(f"Warning: failed to update memory store: {exc}") # Best-effort cleanup of admiral session marker (mission-scoped lifecycle). - # Marker lives at .nelson/admiral.session, two levels up from mission_dir. + # Marker lives at .nelson/, two levels up from + # mission_dir (.nelson/missions//). try: - (mission_dir.parent.parent / "admiral.session").unlink() + (mission_dir.parent.parent / ADMIRAL_SESSION_MARKER).unlink() except FileNotFoundError: pass except OSError: diff --git a/skills/nelson/scripts/nelson_data_utils.py b/skills/nelson/scripts/nelson_data_utils.py index 5472f3b..ed66b2b 100644 --- a/skills/nelson/scripts/nelson_data_utils.py +++ b/skills/nelson/scripts/nelson_data_utils.py @@ -83,6 +83,10 @@ FLEET_STATUS_STALENESS_THRESHOLD_SECONDS = 600 +# Filename of the admiral session marker, written under .nelson/. +# Must stay in sync with hooks/nelson_hooks.py:ADMIRAL_SESSION_MARKER. +ADMIRAL_SESSION_MARKER = "admiral.session" + # --------------------------------------------------------------------------- # Helpers — pure functions (no side effects)