STATE-WP-0070: identity headers, archive 0069, phase-2 workplan
Add X-StateHub-Component to fix-consistency and other State Hub REST callers for legacy-meter attribution. Archive STATE-WP-0069; open STATE-WP-0070 for meter-gated phase-2 retirement. Task POST bodies use workplan_id only.
This commit is contained in:
parent
2f06751d4f
commit
bb6bec9f10
10 changed files with 198 additions and 17 deletions
|
|
@ -25,6 +25,7 @@ from statehub_register import run_register as run_statehub_register
|
|||
|
||||
STATE_HUB_DIR = Path(__file__).resolve().parent
|
||||
API_BASE = os.environ.get("API_BASE", "http://127.0.0.1:8000")
|
||||
_LEGACY_METER_HEADERS = {"X-StateHub-Component": "state-hub.custodian-cli"}
|
||||
TEMPLATE = STATE_HUB_DIR / "scripts" / "project_claude_md.template"
|
||||
PATCH_CWD = STATE_HUB_DIR / "scripts" / "patch_mcp_cwd.py"
|
||||
|
||||
|
|
@ -81,7 +82,8 @@ _ONBOARDING_TASKS = [
|
|||
def _api_get(path: str) -> object:
|
||||
url = API_BASE.rstrip("/") + path
|
||||
try:
|
||||
with urllib.request.urlopen(url, timeout=10) as r:
|
||||
req = urllib.request.Request(url, headers=_LEGACY_METER_HEADERS)
|
||||
with urllib.request.urlopen(req, timeout=10) as r:
|
||||
return json.loads(r.read())
|
||||
except urllib.error.URLError as e:
|
||||
print(f"ERROR: Cannot reach API at {API_BASE}: {e}")
|
||||
|
|
@ -92,7 +94,11 @@ def _api_get(path: str) -> object:
|
|||
def _api_post(path: str, body: dict) -> object:
|
||||
url = API_BASE.rstrip("/") + path
|
||||
data = json.dumps({k: v for k, v in body.items() if v is not None}).encode()
|
||||
req = urllib.request.Request(url, data=data, headers={"Content-Type": "application/json"})
|
||||
req = urllib.request.Request(
|
||||
url,
|
||||
data=data,
|
||||
headers={"Content-Type": "application/json", **_LEGACY_METER_HEADERS},
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=10) as r:
|
||||
return json.loads(r.read())
|
||||
|
||||
|
|
@ -103,7 +109,7 @@ def _api_patch(path: str, body: dict) -> object:
|
|||
req = urllib.request.Request(
|
||||
url,
|
||||
data=data,
|
||||
headers={"Content-Type": "application/json"},
|
||||
headers={"Content-Type": "application/json", **_LEGACY_METER_HEADERS},
|
||||
method="PATCH",
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=10) as r:
|
||||
|
|
|
|||
|
|
@ -6,11 +6,13 @@ import urllib.error
|
|||
import urllib.request
|
||||
|
||||
API_BASE = os.environ.get("API_BASE", "http://127.0.0.1:8000").rstrip("/")
|
||||
_HEADERS = {"X-StateHub-Component": "state-hub.dashboard.token-summary"}
|
||||
|
||||
|
||||
def fetch(url: str):
|
||||
try:
|
||||
with urllib.request.urlopen(url, timeout=10) as resp:
|
||||
req = urllib.request.Request(url, headers=_HEADERS)
|
||||
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||
return json.loads(resp.read())
|
||||
except urllib.error.URLError:
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -6,9 +6,11 @@ import urllib.request
|
|||
import urllib.error
|
||||
|
||||
API_BASE = os.environ.get("API_BASE", "http://127.0.0.1:8000").rstrip("/")
|
||||
_HEADERS = {"X-StateHub-Component": "state-hub.dashboard.workstreams-list"}
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(f"{API_BASE}/workplans", timeout=10) as resp:
|
||||
req = urllib.request.Request(f"{API_BASE}/workplans", headers=_HEADERS)
|
||||
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||
data = json.loads(resp.read())
|
||||
print(json.dumps(data))
|
||||
except urllib.error.URLError as e:
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import urllib.request
|
|||
import urllib.error
|
||||
|
||||
API_BASE = os.environ.get("API_BASE", "http://127.0.0.1:8000").rstrip("/")
|
||||
_HEADERS = {"X-StateHub-Component": "state-hub.dashboard.workplan-detail"}
|
||||
|
||||
ws_id = sys.argv[1] if len(sys.argv) > 1 else ""
|
||||
|
||||
|
|
@ -15,7 +16,8 @@ if not ws_id:
|
|||
sys.exit(1)
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(f"{API_BASE}/workplans/{ws_id}", timeout=10) as resp:
|
||||
req = urllib.request.Request(f"{API_BASE}/workplans/{ws_id}", headers=_HEADERS)
|
||||
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||
data = json.loads(resp.read())
|
||||
print(json.dumps(data))
|
||||
except urllib.error.HTTPError as e:
|
||||
|
|
|
|||
|
|
@ -47,4 +47,22 @@ python scripts/capture_legacy_meter_evidence.py --days 7
|
|||
```
|
||||
|
||||
Expect `GET /workstreams/*` window counts to fall after deploy + one full
|
||||
fix-consistency sweep cycle on all hosts.
|
||||
fix-consistency sweep cycle on all hosts.
|
||||
|
||||
## Identity headers (2026-07-09)
|
||||
|
||||
Scripts now send `X-StateHub-Component` for legacy-meter attribution:
|
||||
|
||||
| Component key | Caller |
|
||||
| --- | --- |
|
||||
| `state-hub.fix-consistency` | `scripts/consistency_check.py` |
|
||||
| `state-hub.custodian-cli` | `custodian_cli.py` |
|
||||
| `state-hub.cleanup-stale-tasks` | `scripts/cleanup_stale_tasks.py` |
|
||||
| `state-hub.validate-repo-adr` | `scripts/validate_repo_adr.py` |
|
||||
| `state-hub.dashboard.workstreams-list` | dashboard data loader |
|
||||
| `state-hub.dashboard.workplan-detail` | dashboard detail loader |
|
||||
| `state-hub.dashboard.token-summary` | token summary loader |
|
||||
|
||||
**Remote deploy note:** railiance01 `~/state-hub` was at `050cbbc` (pre-migration);
|
||||
`git pull` failed with permission denied on `.git/FETCH_HEAD` — operator action
|
||||
required to update the sweep host.
|
||||
|
|
@ -32,13 +32,15 @@ except Exception: # pragma: no cover — event publishing is optional
|
|||
publish_event = None # type: ignore[assignment]
|
||||
shutdown_publisher = None # type: ignore[assignment]
|
||||
|
||||
API = "http://127.0.0.1:8000"
|
||||
API = os.environ.get("API_BASE", "http://127.0.0.1:8000").rstrip("/")
|
||||
_LEGACY_METER_HEADERS = {"X-StateHub-Component": "state-hub.cleanup-stale-tasks"}
|
||||
STALE_STATUSES = set(OPEN_TASK_STATUSES)
|
||||
CLOSED_WS_STATUS = set(CLOSED_WORKSTREAM_STATUSES)
|
||||
|
||||
|
||||
def get(path: str) -> list | dict:
|
||||
with urllib.request.urlopen(f"{API}{path}") as r:
|
||||
req = urllib.request.Request(f"{API}{path}", headers=_LEGACY_METER_HEADERS)
|
||||
with urllib.request.urlopen(req) as r:
|
||||
return json.loads(r.read())
|
||||
|
||||
|
||||
|
|
@ -49,7 +51,7 @@ def _request(method: str, url: str, payload: dict) -> dict:
|
|||
req = urllib.request.Request(
|
||||
url,
|
||||
data=data,
|
||||
headers={"Content-Type": "application/json"},
|
||||
headers={"Content-Type": "application/json", **_LEGACY_METER_HEADERS},
|
||||
method=method,
|
||||
)
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -122,6 +122,13 @@ except ImportError:
|
|||
# Constants
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_LEGACY_METER_COMPONENT = "state-hub.fix-consistency"
|
||||
|
||||
|
||||
def _legacy_meter_headers() -> dict[str, str]:
|
||||
return {"X-StateHub-Component": _LEGACY_METER_COMPONENT}
|
||||
|
||||
|
||||
_TASK_BLOCK_RE = re.compile(r"```task\s*\n(.*?)\n```", re.DOTALL)
|
||||
_HEADING_RE = re.compile(r"^(#{1,4})\s+(.+?)$", re.MULTILINE)
|
||||
_ARCHIVED_WP_RE = re.compile(r"^\d{6}-(.+\.md)$")
|
||||
|
|
@ -566,7 +573,12 @@ def _api_get(
|
|||
last_error: Exception | None = None
|
||||
for attempt in range(_API_GET_RETRIES):
|
||||
try:
|
||||
with _httpx.Client(base_url=api_base, timeout=10.0, follow_redirects=True) as c:
|
||||
with _httpx.Client(
|
||||
base_url=api_base,
|
||||
timeout=10.0,
|
||||
follow_redirects=True,
|
||||
headers=_legacy_meter_headers(),
|
||||
) as c:
|
||||
r = c.get(path, params=filtered if filtered else None)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
|
@ -604,7 +616,12 @@ def _api_patch(api_base: str, path: str, body: dict) -> Any:
|
|||
if not path.endswith("/"):
|
||||
path += "/"
|
||||
try:
|
||||
with _httpx.Client(base_url=api_base, timeout=10.0, follow_redirects=True) as c:
|
||||
with _httpx.Client(
|
||||
base_url=api_base,
|
||||
timeout=10.0,
|
||||
follow_redirects=True,
|
||||
headers=_legacy_meter_headers(),
|
||||
) as c:
|
||||
r = c.patch(path, json=body)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
|
@ -620,7 +637,12 @@ def _api_put(api_base: str, path: str, body: dict) -> Any:
|
|||
if not path.endswith("/"):
|
||||
path += "/"
|
||||
try:
|
||||
with _httpx.Client(base_url=api_base, timeout=30.0, follow_redirects=True) as c:
|
||||
with _httpx.Client(
|
||||
base_url=api_base,
|
||||
timeout=30.0,
|
||||
follow_redirects=True,
|
||||
headers=_legacy_meter_headers(),
|
||||
) as c:
|
||||
r = c.put(path, json=body)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
|
@ -634,7 +656,12 @@ def _api_post(api_base: str, path: str, body: dict) -> Any:
|
|||
if not path.endswith("/"):
|
||||
path += "/"
|
||||
try:
|
||||
with _httpx.Client(base_url=api_base, timeout=10.0, follow_redirects=True) as c:
|
||||
with _httpx.Client(
|
||||
base_url=api_base,
|
||||
timeout=10.0,
|
||||
follow_redirects=True,
|
||||
headers=_legacy_meter_headers(),
|
||||
) as c:
|
||||
r = c.post(path, json=body)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
|
@ -2461,7 +2488,7 @@ def fix_repo(
|
|||
if t_priority not in VALID_TASK_PRIORITIES:
|
||||
t_priority = "medium"
|
||||
t_data = _api_post(api_base, "/tasks", {
|
||||
"workstream_id": new_ws_id,
|
||||
"workplan_id": new_ws_id,
|
||||
"title": str(task.get("title", t_id)).strip() or t_id,
|
||||
"description": task.get("description") or None,
|
||||
"status": t_status,
|
||||
|
|
@ -2545,7 +2572,7 @@ def fix_repo(
|
|||
if t_priority not in VALID_TASK_PRIORITIES:
|
||||
t_priority = "medium"
|
||||
t_data = _api_post(api_base, "/tasks", {
|
||||
"workstream_id": ws_id,
|
||||
"workplan_id": ws_id,
|
||||
"title": str(task.get("title", t_id)).strip() or t_id,
|
||||
"description": task.get("description") or None,
|
||||
"status": t_status,
|
||||
|
|
|
|||
|
|
@ -301,13 +301,22 @@ def check_files(workplans_dir: Path, report: Report) -> list[dict]:
|
|||
# State-hub API checks
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _legacy_meter_headers() -> dict[str, str]:
|
||||
return {"X-StateHub-Component": "state-hub.validate-repo-adr"}
|
||||
|
||||
|
||||
def _api_get(api_base: str, path: str, params: dict | None = None) -> Any:
|
||||
if not _HAS_HTTPX:
|
||||
return None
|
||||
if not path.endswith("/"):
|
||||
path += "/"
|
||||
try:
|
||||
with _httpx.Client(base_url=api_base, timeout=10.0, follow_redirects=True) as c:
|
||||
with _httpx.Client(
|
||||
base_url=api_base,
|
||||
timeout=10.0,
|
||||
follow_redirects=True,
|
||||
headers=_legacy_meter_headers(),
|
||||
) as c:
|
||||
r = c.get(path, params={k: v for k, v in (params or {}).items() if v is not None})
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,113 @@
|
|||
---
|
||||
id: STATE-WP-0070
|
||||
type: workplan
|
||||
title: "Workplan terminology phase-2 legacy retirement"
|
||||
domain: infotech
|
||||
repo: state-hub
|
||||
status: active
|
||||
owner: codex
|
||||
topic_slug: custodian
|
||||
planning_priority: medium
|
||||
planning_order: 70
|
||||
created: "2026-07-09"
|
||||
updated: "2026-07-09"
|
||||
state_hub_workstream_id: "9aa92529-6ee8-4b3f-b573-78b0ef8d9788"
|
||||
---
|
||||
|
||||
# STATE-WP-0070 — Workplan terminology phase-2 legacy retirement
|
||||
|
||||
**Parent:** `STATE-WP-0069` (finished 2026-07-08, archived).
|
||||
|
||||
**Prerequisite:** Caller migrations shipped (`b05ca2c+`), identity headers on
|
||||
fix-consistency and dashboard loaders, activity-core progress dual-write stopped.
|
||||
|
||||
## Goal
|
||||
|
||||
Drive legacy-meter counts to zero per key, then retire remaining `/workstreams`
|
||||
routes, MCP aliases, and internal dual-keys. All removals remain
|
||||
**meter-gated** (seven consecutive zero-usage windows).
|
||||
|
||||
## Context
|
||||
|
||||
Live meter (2026-07-09) still shows ~150k weekly legacy GET calls — attributed
|
||||
to `unknown` until identity headers deploy and remote fix-consistency hosts pull
|
||||
latest `state-hub`. Evidence: `docs/evidence/legacy-meter-weekly-review-20260708.json`,
|
||||
caller inventory: `docs/evidence/workstream-caller-inventory-20260708.md`.
|
||||
|
||||
## Task: Deploy caller migrations and verify meter attribution
|
||||
|
||||
```task
|
||||
id: STATE-WP-0070-T01
|
||||
status: progress
|
||||
priority: high
|
||||
state_hub_task_id: "04cb3a3a-fe52-480b-816b-37c8f8fbe5ee"
|
||||
```
|
||||
|
||||
1. Pull `state-hub` ≥ `b05ca2c` on all fix-consistency hosts (railiance01 sweep,
|
||||
WSL workstation, cluster runners).
|
||||
2. Run full `statehub fix-consistency --all` (or scheduled sweep).
|
||||
3. Capture weekly review; confirm `X-StateHub-Component` buckets name
|
||||
`state-hub.fix-consistency` instead of `unknown`.
|
||||
4. Expect `GET /workstreams/*` window counts to fall sharply.
|
||||
|
||||
Done when a post-deploy capture shows attributed components and declining legacy
|
||||
REST counts.
|
||||
|
||||
## Task: Retire remaining REST `/workstreams` routes per key
|
||||
|
||||
```task
|
||||
id: STATE-WP-0070-T02
|
||||
status: todo
|
||||
priority: high
|
||||
state_hub_task_id: "13122195-3b7e-4e22-a5e8-4ebe7e9a093f"
|
||||
```
|
||||
|
||||
For each key in `docs/workplan-terminology-legacy-retirement-backlog.md` phase 1–2
|
||||
still showing usage:
|
||||
|
||||
- `GET/POST/PATCH /workstreams/*`
|
||||
- dependency and execution intent aliases
|
||||
|
||||
Return **410 Gone** (matching DELETE pattern) when legacy-meter shows seven
|
||||
consecutive zero-usage windows. Update tests and capture evidence per retirement.
|
||||
|
||||
## Task: MCP alias removal (Phase 2)
|
||||
|
||||
```task
|
||||
id: STATE-WP-0070-T03
|
||||
status: todo
|
||||
priority: medium
|
||||
state_hub_task_id: "e2b9015a-d157-4677-b700-165f1cf7e2af"
|
||||
```
|
||||
|
||||
Remove `create_workstream`, `update_workstream`, `list_workstreams`,
|
||||
`update_workstream_status`, and `state://workstreams/{topic_slug}` when MCP
|
||||
procedure keys hit zero usage for seven windows. Keep `TOOLS.md` migration notes.
|
||||
|
||||
## Task: Internal dual-key and param alias cleanup
|
||||
|
||||
```task
|
||||
id: STATE-WP-0070-T04
|
||||
status: todo
|
||||
priority: low
|
||||
state_hub_task_id: "18af98bf-c74e-47c8-948a-e713cff88bb8"
|
||||
```
|
||||
|
||||
- Drop `open_workstreams` from `/state/summary` when meter clears.
|
||||
- Remove `workstream_id` query/body aliases on preferred routes.
|
||||
- Retire `flows/workstream.yaml` when no callers remain.
|
||||
|
||||
## Task: Closeout and fleet gate
|
||||
|
||||
```task
|
||||
id: STATE-WP-0070-T05
|
||||
status: todo
|
||||
priority: medium
|
||||
state_hub_task_id: "dcc81665-ef84-4c0a-a88f-d8afc0d1bbb9"
|
||||
```
|
||||
|
||||
Re-run `scan_workstream_terminology.py`, grep budget check, full test suite,
|
||||
archive this workplan, notify CUST-WP-0055 fleet gate complete.
|
||||
|
||||
Done when legacy-meter weekly review shows no active legacy REST keys and MCP
|
||||
aliases are removed or marked retired.
|
||||
Loading…
Add table
Add a link
Reference in a new issue