Remove Hub Self-Node — Implementation Plan
For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: Delete the inert hub self-node concept (from PR #202): Node.is_self, Node.ensure_self(), bootstrap_self_node, and the self-node branch in _resolve_node.
Architecture: Pure deletion. Keep PipelineRun.node + the origin enum; only the self-node mechanism goes. After removal, _resolve_node has a single label-based path and hub checker_generated runs resolve to node=None (already the production behavior, since ensure_self() no-ops when INSTANCE_ID is empty).
Tech Stack: Django ORM + migrations, pytest. uv run for everything.
Conventions (AGENTS.md): absolute imports; 100-char lines; Black + Ruff; 100% branch coverage on changed code; bandit clean. Design: docs/plans/2026-08-11-remove-hub-self-node-design.md.
Full reference inventory (verified):
apps/alerts/models.py:is_selffield (~340-342),ensure_self()(~383-393),import socket(line 5 — used ONLY by ensure_self).apps/alerts/admin.py:549-550:is_selfinlist_display+list_filter=["is_self"].apps/alerts/management/commands/bootstrap_self_node.py(+apps/alerts/_tests/management/test_bootstrap_self_node.py).apps/alerts/_tests/test_models.py:63-90: 5 tests (test_node_is_self_defaults_false,test_ensure_self_creates_self_node,test_ensure_self_is_idempotent,test_ensure_self_noop_when_instance_id_unset).apps/alerts/_tests/test_admin.py:10-12:test_nodeadmin_shows_is_self.apps/orchestration/orchestrator.py:_resolve_node(payload, origin)self-node branch (~202-203) + docstring (~189-198); call siteself._resolve_node(payload, origin)(~166).apps/orchestration/_tests/test_orchestrator.py:670-680:test_checker_generated_run_gets_self_node.- Help-text mentions of “hub self-node”:
apps/alerts/models.py(is_self help_text, removed with the field) andapps/orchestration/models.py:108(PipelineRun.nodehelp_text) +apps/orchestration/migrations/0006_*.py:20(historical migration — do NOT edit). apps/alerts/AGENTS.md: self-node documentation paragraph (added in #202).
Task 1: Remove Node.is_self + ensure_self() + migration
Files:
- Modify:
apps/alerts/models.py - Create:
apps/alerts/migrations/0009_remove_node_is_self.py(auto-generated) - Modify:
apps/alerts/_tests/test_models.py
Step 1: Delete the obsolete tests first (they define the behavior we’re removing). In apps/alerts/_tests/test_models.py, delete the 4 tests at lines ~63-90 (test_node_is_self_defaults_false, test_ensure_self_creates_self_node, test_ensure_self_is_idempotent, test_ensure_self_noop_when_instance_id_unset). Keep all other tests (the Alert/Incident model tests, Node.upsert tests). Remove any now-unused imports (e.g. override_settings) only if nothing else uses them.
Step 2: Remove the field + method from the model. In apps/alerts/models.py:
- Delete the
is_self = models.BooleanField(...)field. - Delete the entire
ensure_selfclassmethod. - Remove
import socket(line 5) — verify withgrep -n socket apps/alerts/models.pythat no other usage remains; only remove if grep shows none.
Step 3: Generate the migration.
Run: uv run python manage.py makemigrations alerts Expected: creates 0009_remove_node_is_self.py with a single RemoveField(model_name="node", name="is_self").
Step 4: Verify.
Run: uv run pytest apps/alerts/_tests/test_models.py -q Expected: PASS (no is_self/ensure_self references remain). Run: uv run python manage.py makemigrations --check --dry-run → No changes detected.
Step 5: Commit
git add apps/alerts/models.py apps/alerts/migrations/0009_remove_node_is_self.py apps/alerts/_tests/test_models.py
git commit -m "refactor(alerts): remove Node.is_self and ensure_self()"
Task 2: Remove bootstrap_self_node command + admin flag
Files:
- Delete:
apps/alerts/management/commands/bootstrap_self_node.py - Delete:
apps/alerts/_tests/management/test_bootstrap_self_node.py - Modify:
apps/alerts/admin.py - Modify:
apps/alerts/_tests/test_admin.py
Step 1: Delete the command and its test.
git rm apps/alerts/management/commands/bootstrap_self_node.py \
apps/alerts/_tests/management/test_bootstrap_self_node.py
If apps/alerts/_tests/management/ is left with only __init__.py and no other tests, leave the __init__.py (harmless) — do not remove the package.
Step 2: Remove is_self from NodeAdmin. In apps/alerts/admin.py:
- Remove
"is_self"fromNodeAdmin.list_display(line ~549). - Remove the
list_filter = ["is_self"]line entirely (line ~550) —is_selfwas its only entry; NodeAdmin had no other list_filter before #202.
Step 3: Remove the admin test. In apps/alerts/_tests/test_admin.py, delete test_nodeadmin_shows_is_self (lines ~10-12). If that leaves an unused NodeAdmin import, remove it only if nothing else in the file uses it.
Step 4: Verify.
Run: uv run pytest apps/alerts/ -q Expected: PASS. Run: uv run python manage.py check (ensure no missing-command / admin errors).
Step 5: Commit
git add -A apps/alerts/
git commit -m "refactor(alerts): drop bootstrap_self_node command and is_self admin column"
Task 3: Simplify _resolve_node (single resolution path)
Files:
- Modify:
apps/orchestration/orchestrator.py - Modify:
apps/orchestration/_tests/test_orchestrator.py - Modify:
apps/orchestration/models.py(help_text wording)
Step 1: Flip the orchestrator test first. In apps/orchestration/_tests/test_orchestrator.py, replace test_checker_generated_run_gets_self_node (~670-680) with:
@pytest.mark.django_db
def test_checker_generated_run_has_null_node():
run = PipelineOrchestrator().start_pipeline(
payload={"checks_only": True},
source="cli",
origin=PipelineOrigin.CHECKER_GENERATED,
)
assert run.origin == PipelineOrigin.CHECKER_GENERATED
assert run.node is None
(Drop the @override_settings(INSTANCE_ID="hub-1") decorator — no longer relevant.)
Step 2: Run to verify it fails
Run: uv run pytest apps/orchestration/_tests/test_orchestrator.py::test_checker_generated_run_has_null_node -q Expected: FAIL — currently returns the self-node (or None only because INSTANCE_ID unset in tests; the decorator removal + assertion change should still fail against the ensure_self branch if INSTANCE_ID is set by other config — if it unexpectedly passes, that’s fine, but proceed to Step 3 to remove the branch regardless).
Step 3: Remove the self-node branch + origin param. In apps/orchestration/orchestrator.py _resolve_node:
- Delete the
if origin == PipelineOrigin.CHECKER_GENERATED: return Node.ensure_self()branch. - Since
originis now unused inside_resolve_node, remove theoriginparameter from its signature and update the call site at ~line 166 fromself._resolve_node(payload, origin)toself._resolve_node(payload). - Update the docstring to describe the single label-based path for all origins (remove the “CHECKER_GENERATED runs concern the hub itself…” sentence).
- Remove the now-unused
from apps.alerts.models import Nodeimport inside_resolve_nodeonly ifNodeis no longer referenced there (theNode.objects.filter(...)lookup likely still uses it — keep if so).
Step 4: Fix help_text wording. In apps/orchestration/models.py:108, change the PipelineRun.node help_text from “Server this run concerns (agent node, or the hub self-node).” to “Server this run concerns (the agent node resolved from the alert’s instance_id).” Do NOT touch the historical migration 0006_*.py.
Step 5: Verify.
Run: uv run pytest apps/orchestration/ -q Expected: PASS (including the flipped test). Run: uv run python manage.py makemigrations --check --dry-run → No changes (help_text on a non-schema… note: help_text changes DO trigger a migration in Django. If makemigrations --check reports a change, generate it: uv run python manage.py makemigrations orchestration — it will be an AlterField with only help_text. Include it in the commit.)
Step 6: Commit
git add apps/orchestration/
git commit -m "refactor(orchestration): single node-resolution path, drop self-node branch"
Task 4: Docs + final gate
Files:
- Modify:
apps/alerts/AGENTS.md
Step 1: Update docs. In apps/alerts/AGENTS.md, remove/rewrite the self-node paragraph added in #202 so it no longer claims Node.is_self / ensure_self() / bootstrap_self_node exist. Keep the rest of the Node description accurate (agent registry keyed by instance_id).
Step 2: Full gate.
Run:
grep -rn "is_self\|ensure_self\|bootstrap_self_node" apps/ config/ bin/ # expect: no matches
uv run coverage run -m pytest && uv run coverage report
uv run black . --check
uv run ruff check .
uv run bandit -r apps/ config/ -c pyproject.toml
uv run python manage.py makemigrations --check --dry-run
Expected: no self-node references remain; full suite green; 100% branch coverage on changed lines; lint/format/bandit clean; no uncommitted migrations.
Step 3: Commit
git add apps/alerts/AGENTS.md
git commit -m "docs(alerts): drop self-node references"
Acceptance criteria
grep -rn "is_self\|ensure_self\|bootstrap_self_node" apps/ config/ bin/returns nothing.Nodehas nois_selfcolumn (migration0009applied);_resolve_nodetakes onlypayloadand has one resolution path.- Full CI green; coverage 100% on changed lines; deployed alert-grouping behavior unchanged.