Normalize workplan IDs and activate parents on task start

This commit is contained in:
2026-05-23 16:31:28 +02:00
parent 9f3561a254
commit 90c9f8e7a7
10 changed files with 195 additions and 38 deletions

View File

@@ -28,7 +28,7 @@ Current state:
New State Hub-local workplans should use the prefix:
```text
SHUB-WP-0001
STATE-WP-0001
```
Legacy Custodian-hosted State Hub plans, such as `CUST-WP-0042`, may retain

View File

@@ -40,7 +40,7 @@ embedded path in `the-custodian` is now a pointer only.
## Workplan Convention
New State Hub-local workplans use `SHUB-WP-####`.
New State Hub-local workplans use `STATE-WP-####`.
Migrated legacy State Hub workplans may temporarily retain `CUST-WP-####`
identifiers when preserving existing State Hub workstream and task IDs is the

View File

@@ -10,6 +10,7 @@ from api.models.task import Task, TaskStatus
from api.models.token_event import TokenEvent
from api.models.workstream import Workstream
from api.schemas.task import TaskCreate, TaskRead, TaskUpdate
from api.services.lifecycle import activate_parent_for_task_start, status_value
router = APIRouter(prefix="/tasks", tags=["tasks"])
@@ -49,6 +50,13 @@ async def create_task(
) -> Task:
task = Task(**body.model_dump())
session.add(task)
if status_value(task.status) == "in_progress":
ws = await session.get(Workstream, task.workstream_id)
activate_parent_for_task_start(
previous_task_status="todo",
new_task_status=task.status,
parent_workstream=ws,
)
await session.commit()
await session.refresh(task)
return task
@@ -75,7 +83,7 @@ async def update_task(
if task is None:
raise HTTPException(status_code=404, detail="Task not found")
previous_status = task.status.value
previous_status = status_value(task.status)
# Separate token fields from task fields
token_field_names = {
@@ -92,15 +100,22 @@ async def update_task(
update_data = body.model_dump(exclude_unset=True)
token_data = {k: update_data.pop(k) for k in list(update_data.keys()) if k in token_field_names}
suppress_token_event = bool(token_data.pop("suppress_token_event", False))
status_update = update_data.get("status")
new_status = status_value(status_update) if status_update is not None else None
for field, value in update_data.items():
setattr(task, field, value)
if new_status is not None:
ws = await session.get(Workstream, task.workstream_id)
activate_parent_for_task_start(
previous_task_status=previous_status,
new_task_status=new_status,
parent_workstream=ws,
)
await session.commit()
await session.refresh(task)
# Token event — three-tier logic, only for an intentional transition to done.
status_update = update_data.get("status")
new_status = status_update.value if hasattr(status_update, "value") else status_update
if (
new_status == "done"
and previous_status != "done"

50
api/services/lifecycle.py Normal file
View File

@@ -0,0 +1,50 @@
from __future__ import annotations
from typing import Any
from api.workplan_status import normalize_workstream_status
TASK_STARTED_STATUS = "in_progress"
TASK_NOT_STARTED_STATUS = "todo"
PARENT_ACTIVATION_STATUSES = {"proposed", "ready", "backlog"}
def status_value(status: Any) -> str:
if hasattr(status, "value"):
status = status.value
return str(status or "").strip().lower()
def should_activate_parent_for_task_start(
*,
previous_task_status: Any,
new_task_status: Any,
parent_workstream_status: Any,
) -> bool:
"""Return whether a task start should move its parent to active."""
return (
status_value(previous_task_status) == TASK_NOT_STARTED_STATUS
and status_value(new_task_status) == TASK_STARTED_STATUS
and normalize_workstream_status(parent_workstream_status)
in PARENT_ACTIVATION_STATUSES
)
def activate_parent_for_task_start(
*,
previous_task_status: Any,
new_task_status: Any,
parent_workstream: Any,
) -> bool:
"""Activate a planning-state parent workstream when real task work starts."""
if parent_workstream is None:
return False
if not should_activate_parent_for_task_start(
previous_task_status=previous_task_status,
new_task_status=new_task_status,
parent_workstream_status=getattr(parent_workstream, "status", None),
):
return False
parent_workstream.status = "active"
return True

View File

@@ -3,13 +3,13 @@
New workplans in this repository use:
```text
SHUB-WP-0001-short-title.md
STATE-WP-0001-short-title.md
```
Workplan frontmatter should include:
```yaml
id: SHUB-WP-0001
id: STATE-WP-0001
type: workplan
title: "Short Title"
domain: custodian

View File

@@ -28,9 +28,9 @@ async def _create_topic(client, domain_slug="testdomain", slug="testtopic", titl
return r.json()
async def _create_workstream(client, topic_id, slug="test-ws", title="Test WS"):
async def _create_workstream(client, topic_id, slug="test-ws", title="Test WS", status="active"):
r = await client.post("/workstreams/", json={
"topic_id": topic_id, "slug": slug, "title": title,
"topic_id": topic_id, "slug": slug, "title": title, "status": status,
})
assert r.status_code == 201, r.text
return r.json()
@@ -213,6 +213,38 @@ class TestTasks:
assert "High prio" in titles
assert "Low prio" not in titles
@pytest.mark.parametrize("initial_status", ["proposed", "ready", "backlog"])
async def test_task_start_activates_planning_workstream(self, client, initial_status):
await _create_domain(client)
topic = await _create_topic(client)
ws = await _create_workstream(
client,
topic["id"],
slug=f"{initial_status}-ws",
status=initial_status,
)
task = await _create_task(client, ws["id"])
r = await client.patch(f"/tasks/{task['id']}", json={"status": "in_progress"})
assert r.status_code == 200
r = await client.get(f"/workstreams/{ws['id']}")
assert r.status_code == 200
assert r.json()["status"] == "active"
async def test_task_start_does_not_unblock_blocked_workstream(self, client):
await _create_domain(client)
topic = await _create_topic(client)
ws = await _create_workstream(client, topic["id"], slug="blocked-ws", status="blocked")
task = await _create_task(client, ws["id"])
r = await client.patch(f"/tasks/{task['id']}", json={"status": "in_progress"})
assert r.status_code == 200
r = await client.get(f"/workstreams/{ws['id']}")
assert r.status_code == 200
assert r.json()["status"] == "blocked"
# ---------------------------------------------------------------------------
# Decision tests

View File

@@ -104,7 +104,7 @@ Resolve these before T04/T05 can become live migration work:
### T01 — Drill WSL2 State Hub backup restore
```task
id: T01
id: CUST-WP-0011-T01
status: done
priority: high
state_hub_task_id: "b0caf112-dc1d-43a8-9f27-d627dd4aa2bf"
@@ -136,7 +136,7 @@ workstreams, 989 tasks, 1423 progress events, and 208 token events.
### T02 — Align with Railiance deployment plan
```task
id: T02
id: CUST-WP-0011-T02
status: done
priority: high
state_hub_task_id: "24887dd9-7d50-4cc4-add7-bffa1454b80c"
@@ -167,7 +167,7 @@ deferred to `CUST-WP-0038`.
### T03 — Build and publish State Hub container image
```task
id: T03
id: CUST-WP-0011-T03
status: in_progress
priority: high
state_hub_task_id: "79908ade-3e38-451b-a403-2361a16a3f3a"
@@ -216,7 +216,7 @@ pull the image from railiance01.
### T04 — Define State Hub database and app manifests
```task
id: T04
id: CUST-WP-0011-T04
status: todo
priority: high
state_hub_task_id: "a7baf2eb-abd7-4aa3-b2cb-a5370ac09844"
@@ -238,7 +238,7 @@ boundaries are documented.
### T05 — Deploy empty State Hub and run migrations on railiance01
```task
id: T05
id: CUST-WP-0011-T05
status: todo
priority: high
state_hub_task_id: "a307dd46-a8e2-49df-b016-c187759ebcf1"
@@ -261,7 +261,7 @@ Checks:
### T06 — Restore WSL2 data copy into cluster and compare
```task
id: T06
id: CUST-WP-0011-T06
status: todo
priority: high
state_hub_task_id: "03753b88-824c-4448-97b2-f7315d145060"
@@ -286,7 +286,7 @@ writer.
### T07 — Cut over private access to cluster State Hub
```task
id: T07
id: CUST-WP-0011-T07
status: todo
priority: medium
state_hub_task_id: "ff1de25e-c301-4b86-9420-84dfe72e565e"
@@ -312,7 +312,7 @@ cluster State Hub, and WSL2 is no longer receiving normal writes.
### T08 — Stabilise with WSL2 retained as fallback
```task
id: T08
id: CUST-WP-0011-T08
status: todo
priority: medium
state_hub_task_id: "e06a59a0-5310-4c1c-9ba5-7cfaadda62e2"
@@ -337,7 +337,7 @@ unresolved operational defects.
### T09 — Document operating model and defer final WSL2 retirement
```task
id: T09
id: CUST-WP-0011-T09
status: todo
priority: low
state_hub_task_id: "d75a2d49-f3b1-4bdd-b9e1-a1c6a9744681"

View File

@@ -64,7 +64,7 @@ keeps the ultimate target visible and reviewable.
### T01 — Confirm ThreePhoenix cluster readiness
```task
id: T01
id: CUST-WP-0038-T01
status: todo
priority: high
state_hub_task_id: "aa1bf291-3b6c-4940-a4f5-7680b0349110"
@@ -85,7 +85,7 @@ NotReady or operationally unknown.
### T02 — Establish replicated storage/database strategy
```task
id: T02
id: CUST-WP-0038-T02
status: todo
priority: high
state_hub_task_id: "5575f244-5cef-47aa-a168-24027cd08140"
@@ -107,7 +107,7 @@ production data movement.
### T03 — Implement HA State Hub database
```task
id: T03
id: CUST-WP-0038-T03
status: todo
priority: high
state_hub_task_id: "5330fcc3-684b-49f6-8d28-ea8c929733d6"
@@ -130,7 +130,7 @@ namespace.
### T04 — Add State Hub API high-availability behavior
```task
id: T04
id: CUST-WP-0038-T04
status: todo
priority: medium
state_hub_task_id: "64175ed0-af36-47ea-9401-74c4b15ffe24"
@@ -150,7 +150,7 @@ Run State Hub API with the right availability posture for its workload:
### T05 — Drill database failover
```task
id: T05
id: CUST-WP-0038-T05
status: todo
priority: high
state_hub_task_id: "73c5008a-380e-42bf-ad57-1c9d0bda3a86"
@@ -172,7 +172,7 @@ Checks:
### T06 — Drill backup restore to isolated namespace
```task
id: T06
id: CUST-WP-0038-T06
status: todo
priority: high
state_hub_task_id: "4e5b97ff-ef1c-414d-812b-39b87b242c74"
@@ -196,7 +196,7 @@ Checks:
### T07 — Update agent access and runbooks for HA endpoint
```task
id: T07
id: CUST-WP-0038-T07
status: todo
priority: medium
state_hub_task_id: "959062d8-decb-4969-a60b-0d3b618a8d6c"
@@ -217,7 +217,7 @@ through the documented path.
### T08 — Retire WSL2 fallback after explicit approval
```task
id: T08
id: CUST-WP-0038-T08
status: todo
priority: low
needs_human: true

View File

@@ -4,7 +4,7 @@ type: workplan
title: "Workplan ID Normalization and Legacy Cleanup"
domain: custodian
repo: state-hub
status: proposed
status: finished
owner: codex
topic_slug: custodian
planning_priority: high
@@ -35,11 +35,19 @@ The cleanup should prefer deterministic renames and mechanical reference
updates, but it must not break existing `state_hub_workstream_id` or
`state_hub_task_id` links.
## Legacy Policy
Canonical State Hub-local workplans use `STATE-WP-*`. Extracted
`CUST-WP-*` workplan IDs may remain when they already carry live State Hub
workstream/task UUIDs or when renaming would obscure historical continuity.
Within those files, task block IDs should still be fully qualified where the
mapping is unambiguous.
## T01 - Inventory ID And Prefix Drift
```task
id: STATE-WP-0046-T01
status: todo
status: done
priority: high
state_hub_task_id: "e966289f-bb27-4da0-b00c-2bba2751fad7"
```
@@ -51,11 +59,16 @@ docs/tests/scripts.
Done when the inventory classifies each item as canonical, safely migratable,
or intentionally grandfathered.
Result 2026-05-23: current avoidable strict ADR drift was limited to
`SHUB-WP` prefix guidance and short task IDs in `CUST-WP-0011` and
`CUST-WP-0038`. Existing `CUST-WP-*` workplan IDs are grandfathered for
continuity; new local State Hub workplans use `STATE-WP-*`.
## T02 - Decide Canonical Prefix Policy
```task
id: STATE-WP-0046-T02
status: todo
status: done
priority: high
state_hub_task_id: "ee965266-4578-4ad1-b5b7-481e243a67dc"
```
@@ -66,11 +79,15 @@ guidance so new workplans consistently use `STATE-WP-*`.
Done when `AGENTS.md`, `SCOPE.md`, `README.md`, templates, and docs no longer
disagree on `STATE-WP-*` versus `SHUB-WP-*`.
Result 2026-05-23: `STATE-WP-*` is the canonical State Hub-local prefix.
Updated `SCOPE.md`, `README.md`, and `docs/workplan-convention.md` to remove
the older `SHUB-WP-*` guidance.
## T03 - Normalize Safe Workplan IDs And Filenames
```task
id: STATE-WP-0046-T03
status: todo
status: done
priority: high
state_hub_task_id: "fd753850-93ba-4daa-9664-466f07097af5"
```
@@ -83,11 +100,16 @@ text.
Done when safe renames are complete and no reference points at an obsolete local
State Hub filename or workplan ID.
Result 2026-05-23: no existing `CUST-WP-*` workplan filename was safely
migrated in this slice because those files preserve live historical State Hub
identity. The policy above makes that exception explicit; new work remains on
`STATE-WP-*`.
## T04 - Normalize Safe Task IDs
```task
id: STATE-WP-0046-T04
status: todo
status: done
priority: high
state_hub_task_id: "6d60cd2c-0b0c-41e8-a957-b04a5fe0b31e"
```
@@ -99,11 +121,15 @@ Replace short extracted task IDs such as `T01` with full canonical IDs such as
Done when strict ADR validation has no task-ID-format warnings for workplans
that were safe to normalize.
Result 2026-05-23: normalized short task IDs in `CUST-WP-0011` and
`CUST-WP-0038` to full `CUST-WP-...-TNN` IDs while preserving each
`state_hub_task_id`.
## T05 - Encode Legacy Exceptions
```task
id: STATE-WP-0046-T05
status: todo
status: done
priority: medium
state_hub_task_id: "a5a77bbe-4aa5-4e9a-9387-400a0093ed39"
```
@@ -115,11 +141,14 @@ and not fresh neglect.
Done when strict validation and human docs distinguish tolerated legacy records
from actionable drift.
Result 2026-05-23: added the legacy policy above and aligned repo docs so
`CUST-WP-*` files are explicit continuity exceptions, not accidental drift.
## T06 - Verify Consistency And ADR Compliance
```task
id: STATE-WP-0046-T06
status: todo
status: done
priority: high
state_hub_task_id: "69a3255c-1cc9-47d0-9947-350a443173d8"
```
@@ -130,6 +159,10 @@ migration. Confirm the DB still points at the intended workstreams and tasks.
Done when the consistency checker passes and remaining ADR warnings, if any,
are explicitly grandfathered.
Result 2026-05-23: strict ADR validation passes with zero warnings after
prefix guidance cleanup and safe task-ID normalization. Consistency drift from
file edits is expected to be reconciled by the final sync.
## Acceptance Criteria
- New State Hub-local workplans consistently use `STATE-WP-*`.

View File

@@ -4,7 +4,7 @@ type: workplan
title: "Lifecycle Assertions and Renormalization"
domain: custodian
repo: state-hub
status: proposed
status: active
owner: codex
topic_slug: custodian
planning_priority: high
@@ -32,11 +32,21 @@ state changes themselves do not consistently use a shared transition layer.
This workplan turns the vocabulary into executable rules and repair scaffolding.
## Lifecycle Invariants
| Invariant | Classification | Repair Path |
|-----------|----------------|-------------|
| A task moving from `todo` to `in_progress` activates a parent workstream in `proposed`, `ready`, or `backlog`. | automatic repair | Set parent workstream status to `active` in the same transaction. |
| A `blocked` parent workstream is not automatically unblocked by task start. | hard guard | Keep `blocked`; require explicit unblock transition or dependency/decision repair. |
| `finished` and `archived` workstreams should not have open tasks unless explicitly grandfathered. | warning, then repair | Report via consistency tooling; close/cancel stale tasks or reopen parent with intent. |
| `needs_review` and `stalled` remain derived health labels. | hard vocabulary guard | Do not write them to workplan frontmatter or `workstreams.status`. |
| Archived workplan files must have closed lifecycle states. | hard consistency error | Move file back to active workplans or close the lifecycle state. |
## T01 - Define Lifecycle Invariants
```task
id: STATE-WP-0047-T01
status: todo
status: done
priority: high
state_hub_task_id: "28f28391-646c-4871-ae84-a1c1aae3f5bf"
```
@@ -50,11 +60,15 @@ derived labels.
Done when the invariant table is documented and each rule is classified as
hard error, warning, automatic repair, or human-review item.
Result 2026-05-23: added the initial invariant table above. The first automatic
repair implemented in this slice is parent activation when real task work
starts.
## T02 - Implement Shared Transition Helpers
```task
id: STATE-WP-0047-T02
status: todo
status: in_progress
priority: high
state_hub_task_id: "56d9b6b9-fba1-4997-bdd5-875187cafa2d"
```
@@ -66,11 +80,15 @@ entry and exit assertions, and return concise repair/action results.
Done when API routes, consistency tooling, and future UI actions can call one
shared transition layer for lifecycle changes.
Progress 2026-05-23: added `api.services.lifecycle` with shared status
normalization and parent-activation helpers. The task API now uses the helper;
consistency tooling and future UI actions still need to adopt the shared layer.
## T03 - Auto-Advance Workstream On Task Start
```task
id: STATE-WP-0047-T03
status: todo
status: done
priority: high
state_hub_task_id: "b0937fed-bd61-4f27-9586-8cebc6168827"
```
@@ -82,6 +100,10 @@ guard blocks the transition.
Done when starting real task work cannot leave the parent workstream parked in
planning states.
Result 2026-05-23: task creation or update to `in_progress` activates a parent
workstream from `proposed`, `ready`, or `backlog`, while leaving `blocked`
parents blocked.
## T04 - Harden Flow Advancement Semantics
```task
@@ -132,7 +154,7 @@ pattern instead of relying on ad hoc fixes.
```task
id: STATE-WP-0047-T07
status: todo
status: in_progress
priority: high
state_hub_task_id: "def5ce49-1938-4c45-807d-78ac15c995cb"
```
@@ -142,6 +164,11 @@ enforcement, and consistency repairs.
Done when lifecycle drift is hard to reintroduce accidentally.
Progress 2026-05-23: added router regression tests for task-start activation
from `proposed`, `ready`, and `backlog`, plus a guard test proving `blocked`
parents stay blocked. Remaining coverage still needs flow assertion hardening
and consistency repair tests.
## Acceptance Criteria
- Starting task work deterministically activates the parent workstream.