36 KiB
Structured Runtime Context Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Replace innermost-only foreach context with ancestry-derived, same-scope structured context available consistently to Python handlers, graph bindings, validation, schemas, and authoring helpers.
Architecture: Add one typed ForeachContext runtime value and derive the
active mapping from persisted frame ancestry. Make runtime value construction
and static context-schema construction the two canonical projections of that
model, then use the static projection for validation and authoring inventory.
Keep subgraph scopes isolated and retain current aliases as derived migration
sugar.
Tech Stack: Python 3.14, Pydantic workflow models, dataclass runtime state, JSON Schema, pytest, pytest-asyncio, Ruff, basedpyright, markdownlint-cli2.
Spec:
docs/superpowers/specs/2026-09-04-structured-runtime-context-design.md
Global Constraints
GraphSourcePathkeeps exactly theinput,state, andcontextroots; do not add anoutputroot.- Structured foreach lookup is keyed by the static
ForeachNode.id, never by the configured alias or a dynamic suffix. - Derive active foreach context from persisted frame ancestry; do not copy a flattened context snapshot into every frame.
- Stop ancestry traversal at
RuntimeScope; subgraphs receive caller values only through declared input bindings. - Preserve
context.loop_item,context.loop_index, and unambiguous aliases as derived migration conveniences. - Reject nested alias collisions and malformed persisted foreach metadata rather than shadowing or silently omitting values.
- Keep Python
RuntimeContext.foreachtyped while graph-visible context remains JSON-compatible. - Do not add host-provided
Runtime[ContextT], fork/gather context, scheduling context, run-step limits, time-machine behavior, or the broader Python DSL. - Add docstrings around ancestry traversal, scope stopping, literal path segment construction, and fail-closed metadata checks.
- Do not modify or commit the user's dirty
docs/AGENTS.md.
Task 1: Model And Derive Same-Scope Foreach Context
Files:
- Modify:
src/wf_core/run_state.py - Modify:
src/wf_core/context_contracts.py - Modify:
src/wf_core/runtime/ops/frames.py - Modify:
src/wf_core/runtime/scheduler.py - Modify:
src/wf_core/__init__.py - Create:
tests/core/test_structured_runtime_context.py
Interfaces:
-
Consumes: persisted
RunState.frames,ExecutionFrame.parent_frame_id,ExecutionFrame.scope_id, andForeachIterationMetadata.from_frame(...). -
Produces:
@dataclass(frozen=True, slots=True) class ForeachContext: node_id: str activation_id: str frame_id: str scope_id: str lineage_id: str index: int item: object @dataclass(slots=True) class RuntimeContext: # Existing fields stay unchanged. foreach: Mapping[str, ForeachContext] = field(default_factory=dict) @dataclass(frozen=True, slots=True) class FrameContextView: """Typed handler context and graph values from one ancestry walk.""" foreach: Mapping[str, ForeachContext] graph: Mapping[str, object | None] def frame_context_view( run: RunState, frame: ExecutionFrame, ) -> FrameContextView: ... -
FrameContextView.foreachcontains entries in outermost-to-innermost insertion order. Lookup does not rely on that order. -
FrameContextView.graphexposes a JSON-compatibleforeachmapping, all unique active aliases, and innermostloop_item/loop_indexvalues. -
Step 1: Write failing model and ancestry tests
Add focused helpers that construct a
RunStatewith explicit frames, then add tests named:test_root_frame_has_empty_structured_foreach_contexttest_nested_same_scope_frames_expose_outermost_to_innermost_contexttest_graph_context_values_keep_all_aliases_and_innermost_loop_keystest_context_ancestry_stops_at_runtime_scope_boundary
The central nested assertion is:
view = frame_context_view(run, run.frames["inner-item"]) contexts = view.foreach assert tuple(contexts) == ("customers", "orders") assert contexts["customers"] == ForeachContext( node_id="customers", activation_id="customers:activation:1", frame_id="outer-item", scope_id="root", lineage_id="customers:lineage:0", index=0, item={"name": "Ada"}, ) assert contexts["orders"].index == 2 assert contexts["orders"].item == {"sku": "A-17"}For the scope-boundary case, give the child root a scheduling parent in the caller's foreach frame and assert that the child context remains
{}. -
Step 2: Run the focused tests and confirm the missing API fails
Run:
uv run pytest -q tests/core/test_structured_runtime_context.pyExpected: collection or assertions fail because
ForeachContextand the ancestry-aware helper do not exist. -
Step 3: Add the typed context value and validate frame metadata once
Add
ForeachContextbesideRuntimeContextand export it throughwf_core.__init__. KeepForeachIterationMetadataas the typed decoder for persisted item metadata; add a conversion method so field names are not copied at call sites:def to_context(self, frame: ExecutionFrame) -> ForeachContext: return ForeachContext( node_id=self.foreach_node_id, activation_id=self.activation_id, frame_id=frame.id, scope_id=frame.scope_id, lineage_id=frame.lineage_id, index=self.loop_index, item=self.loop_item, ) -
Step 4: Implement fail-closed same-scope ancestry traversal
Walk from the selected frame through
parent_frame_idwhile scope ids match. Validate the full chain before returning materialized values:selected_scope_id = frame.scope_id current: ExecutionFrame | None = frame seen: set[str] = set() inner_to_outer: list[tuple[str, ForeachContext, str]] = [] while current is not None and current.scope_id == selected_scope_id: if current.id in seen: raise WorkflowExecutionError( f"cyclic execution frame ancestry at frame {current.id!r}" ) seen.add(current.id) metadata = ForeachIterationMetadata.from_frame(current) if metadata is not None: inner_to_outer.append( (metadata.foreach_node_id, metadata.to_context(current), metadata.loop_alias) ) if current.parent_frame_id is None: break parent = run.frames.get(current.parent_frame_id) if parent is None: raise WorkflowExecutionError( f"missing parent frame {current.parent_frame_id!r} " f"for frame {current.id!r}" ) current = parentReverse the collected entries and reject duplicate foreach node ids. Reject aliases that are empty, reserved, or duplicated in the active chain. Do not mutate
RunStatewhile reading context.Add
FOREACH_CONTEXT_KEY = "foreach"tocontext_contracts.pyand include it inRESERVED_CONTEXT_KEYS. Materialize graph values from the validated outer-to-inner entries:graph[FOREACH_CONTEXT_KEY] = { node_id: { "node_id": entry.node_id, "activation_id": entry.activation_id, "frame_id": entry.frame_id, "scope_id": entry.scope_id, "lineage_id": entry.lineage_id, "index": entry.index, "item": entry.item, } for node_id, entry in foreach.items() } for node_id, entry in foreach.items(): graph[alias_by_node_id[node_id]] = entry.item if foreach: innermost = next(reversed(foreach.values())) graph[LOOP_ITEM_CONTEXT_KEY] = innermost.item graph[LOOP_INDEX_CONTEXT_KEY] = innermost.index -
Step 5: Add corruption and collision regressions
Add:
test_structured_context_rejects_malformed_foreach_metadatatest_structured_context_rejects_missing_parent_frametest_structured_context_rejects_parent_cycletest_structured_context_rejects_duplicate_active_foreach_idtest_structured_context_rejects_duplicate_active_aliastest_context_read_does_not_mutate_run_state
Assert the diagnostic category plus the offending frame/node ids. Snapshot
run.to_dict()before the read-only test and assert it remains equal after deriving context. -
Step 6: Run and commit the focused model slice
Run:
uv run pytest -q tests/core/test_structured_runtime_context.py uv run ruff check src/wf_core/run_state.py src/wf_core/context_contracts.py \ src/wf_core/runtime/ops/frames.py \ src/wf_core/runtime/scheduler.py tests/core/test_structured_runtime_context.py uv run basedpyright --level error src/wf_core/run_state.py \ src/wf_core/runtime/ops/frames.py src/wf_core/runtime/scheduler.pyExpected: all commands pass.
git add src/wf_core/run_state.py src/wf_core/context_contracts.py \ src/wf_core/runtime/ops/frames.py \ src/wf_core/runtime/scheduler.py src/wf_core/__init__.py \ tests/core/test_structured_runtime_context.py git commit -m "feat: derive structured foreach runtime context"
Task 2: Use One Derived Context Across Every Runtime Consumer
Files:
- Modify:
src/wf_core/runtime/ops/nodes.py - Modify:
src/wf_core/runtime/ops/foreach.py - Modify:
src/wf_core/runtime/ops/handlers.py - Modify:
src/wf_core/runtime/subgraphs.py - Modify:
src/wf_core/runtime/ops/flow.py - Modify:
tests/core/test_structured_runtime_context.py - Modify:
tests/core/test_scheduler.py
Interfaces:
-
Consumes:
frame_context_view(run, frame)from Task 1. -
Produces: identical context values for node input bindings, foreach
overresolution, interrupt requests, subgraph input/output boundaries, workflow output projection, and Python node handlers. -
RuntimeContext.metadataremains a defensive copy of the selected frame's metadata;RuntimeContext.foreachis the canonical typed view. -
Step 1: Write failing end-to-end runtime tests
Extend
test_structured_runtime_context.pywith:test_nested_handler_receives_outer_and_inner_typed_entriestest_nested_graph_bindings_resolve_outer_and_inner_itemstest_inner_completion_restores_outer_contexttest_concurrent_items_receive_distinct_frame_and_lineage_contexttest_nested_foreach_over_resolves_structured_outer_item_path
In the handler test, capture only stable fields:
def record(_payload: dict[str, object], ctx: RuntimeContext) -> dict[str, object]: seen.append( ( tuple(ctx.foreach), ctx.foreach["customers"].item, ctx.foreach["orders"].item, ctx.foreach["orders"].index, ) ) return {"outcome": "ok", "output": {}} assert seen == [ (("customers", "orders"), {"name": "Ada"}, {"sku": "A-17"}, 0) ]Build the binding test with canonical
InputPathBindingvalues forcontext.foreach.customers.itemandcontext.foreach.orders.item. -
Step 2: Run the end-to-end tests and observe innermost-only behavior
Run:
uv run pytest -q tests/core/test_structured_runtime_context.py \ tests/core/test_scheduler.pyExpected: the new end-to-end tests fail because runtime consumers still use innermost frame-only context and handlers do not receive
.foreach. -
Step 3: Update all graph-visible context call sites together
Replace every old call and verify with search:
rg -n 'frame_context_values\(' \ src/wf_core/runtimeExpected after the edit: no matches. The required consumers are:
_resolve_node_executioninruntime/ops/nodes.py;- foreach source resolution in
runtime/ops/foreach.py; - interrupt request construction in
runtime/ops/handlers.py; - parent input and child output projection in
runtime/subgraphs.py; - root workflow output projection in
runtime/ops/flow.py.
Root workflow output receives
frame_context_view(run, root_frame).graphrather than an omitted context so standard root facts remain consistent. -
Step 4: Give Python handlers the same typed projection
In
_resolve_node_execution, materialize the view once and pass its two projections to their consumers:context_view = frame_context_view(run, frame) context_values = context_view.graph context = RuntimeContext( current_node_id=node.id, frame_id=frame.id, scope_id=frame.scope_id, lineage_id=frame.lineage_id, parent_lineage_id=frame.parent_lineage_id, prior_outcome=frame.prior_outcome, activated_incoming_edge=frame.activated_incoming_edge, metadata=dict(frame.metadata), foreach=context_view.foreach, platform=platform, )Resolve graph bindings against
context_view.graph. Do not reconstructForeachContextfrom the graph-visible dictionary. -
Step 5: Update old focused tests to pass
RunStateexplicitlyReplace direct frame-only context calls in scheduler/context tests with a run containing that frame. Keep assertions for existing standard and compatibility keys; add structured assertions rather than deleting old coverage.
-
Step 6: Run and commit the runtime integration slice
Run:
uv run pytest -q tests/core/test_structured_runtime_context.py \ tests/core/test_scheduler.py tests/core/test_context_scopes.py \ tests/core/test_concurrent_foreach.py tests/core/test_subgraph_step.py uv run ruff check src/wf_core/runtime tests/core/test_structured_runtime_context.py uv run basedpyright --level error src/wf_core/runtimeExpected: all commands pass.
git add src/wf_core/runtime/ops/nodes.py \ src/wf_core/runtime/ops/foreach.py \ src/wf_core/runtime/ops/handlers.py src/wf_core/runtime/subgraphs.py \ src/wf_core/runtime/ops/flow.py \ tests/core/test_structured_runtime_context.py tests/core/test_scheduler.py git commit -m "feat: expose structured context during execution"
Task 3: Expose Literal Structured Paths From Foreach References
Files:
- Modify:
src/wf_core/models/steps.py - Modify:
tests/authoring/test_builder.py - Modify:
tests/authoring/test_subgraph.py - Modify:
tests/core/test_canonical_node_bindings.py
Interfaces:
-
Consumes: existing
ForeachNodevalues returned byWorkflowBuilder.foreach()andGraphSourcePath. -
Produces two non-serialized computed properties:
@property def item(self) -> GraphSourcePath: return GraphSourcePath("context", ("foreach", self.id, "item")) @property def index(self) -> GraphSourcePath: return GraphSourcePath("context", ("foreach", self.id, "index")) -
The constructor uses literal tuple segments. It must not call
GraphSourcePath.context(self.id)because that helper parses dots as path separators. -
Step 1: Write failing path and serialization tests
Add:
test_foreach_reference_exposes_item_and_index_pathstest_foreach_reference_treats_dotted_id_as_one_literal_segmenttest_foreach_computed_paths_are_not_serialized_fields
Assert:
each = builder.foreach(id="orders.v2", over=state_path("orders"), as_="order") assert each.item == GraphSourcePath( "context", ("foreach", "orders.v2", "item") ) assert str(each.item) == 'context.foreach."orders.v2".item' assert str(each.index) == 'context.foreach."orders.v2".index' assert "item" not in each.model_dump(mode="json") assert "index" not in each.model_dump(mode="json") -
Step 2: Write failing node and subgraph binding tests
Use the computed ref directly in both authoring boundaries:
work = builder.use( capability, input=[input_from(each.item, "order")], ) child = builder.subgraph( child_workflow, input=[input_from(each.item, "order")], )Assert both compiled bindings serialize their path as
context.foreach.orders.item. -
Step 3: Implement only the two computed properties
Add the properties directly to
ForeachNode. Do not createForeachRef, a node-address type, or field-selection sugar beneath.item. -
Step 4: Run and commit the authoring slice
Run:
uv run pytest -q tests/authoring/test_builder.py \ tests/authoring/test_subgraph.py \ tests/core/test_canonical_node_bindings.py \ tests/core/test_path_values.py uv run ruff check src/wf_core/models/steps.py \ tests/authoring/test_builder.py tests/authoring/test_subgraph.py uv run basedpyright --level error src/wf_core/models/steps.pyExpected: all commands pass.
git add src/wf_core/models/steps.py tests/authoring/test_builder.py \ tests/authoring/test_subgraph.py tests/core/test_canonical_node_bindings.py git commit -m "feat: expose foreach context paths"
Task 4: Generate Full Static Context Schemas And Authoring Inventory
Files:
- Modify:
src/wf_core/context_contracts.py - Modify:
src/wf_core/analysis/context_scopes.py - Modify:
src/wf_core/analysis/__init__.py - Modify:
src/wf_api/authoring_contracts.py - Modify:
tests/core/test_context_scopes.py - Modify:
tests/wf_api/test_authoring_contracts.py
Interfaces:
-
Consumes: the complete owner stack from
analyze_control_regions(workflow).owner_stack_by_node. -
Produces the existing
context_fields_by_node(workflow)function returningdict[str, tuple[ContextFieldAvailability, ...]], now with a structuredforeachcontract and all active aliases. -
Produces:
def context_schema_for_node(workflow: Workflow, node_id: str) -> ContextSchema: """Return the complete graph-visible context object schema at one node.""" def context_schemas_by_node( workflow: Workflow, *, control_regions: ControlRegionAnalysis | None = None, ) -> dict[str, ContextSchema]: """Return schemas for all unambiguous, reachable program locations.""" def root_context_schema() -> ContextSchema: """Return standard fields plus an empty structured foreach map.""" -
The schema for an inner body contains:
{ "type": "object", "properties": { "foreach": { "type": "object", "properties": { "customers": { "type": "object", "properties": { "node_id": {"const": "customers"}, "activation_id": {"type": "string"}, "frame_id": {"type": "string"}, "scope_id": {"type": "string"}, "lineage_id": {"type": "string"}, "index": {"type": "integer"}, "item": customer_item_schema, }, }, "orders": { "type": "object", "properties": { "node_id": {"const": "orders"}, "activation_id": {"type": "string"}, "frame_id": {"type": "string"}, "scope_id": {"type": "string"}, "lineage_id": {"type": "string"}, "index": {"type": "integer"}, "item": order_item_schema, }, }, }, "required": ["customers", "orders"], "additionalProperties": False, }, "loop_item": order_item_schema, "loop_index": {"type": "integer"}, "customer": customer_item_schema, "order": order_item_schema, # Existing standard execution fields remain. }, "required": [ "foreach", "loop_item", "loop_index", "customer", "order", "scope_id", "lineage_id", ], "additionalProperties": False, }Keep entry schemas inline unless a measured schema-size problem requires
$defs; the observable field types and required paths are contractual. -
Step 1: Replace innermost-only tests with full-stack expectations
Update the existing nested test instead of adding contradictory coverage:
fields = _field_map(workflow, "inner_body") assert fields["outer_item"].schema == outer_item_schema assert fields["inner_item"].schema == inner_item_schema assert fields["loop_item"].schema == inner_item_schema foreach_schema = fields["foreach"].schema assert set(foreach_schema["properties"]) == {"outer", "inner"} assert foreach_schema["properties"]["outer"]["properties"]["item"] \ == outer_item_schema assert foreach_schema["properties"]["inner"]["properties"]["index"] \ == {"type": "integer"}Add
test_inner_completion_schema_restores_outer_structured_entryand assertafter_innercontains only the outer structured entry. -
Step 2: Run static analysis tests and confirm the old projection fails
Run:
uv run pytest -q tests/core/test_context_scopes.pyExpected: nested assertions fail because
_available_fieldsuses onlystack[-1]. -
Step 3: Build contracts from the whole owner stack
Change
_available_fieldsto accept the completeForeachOwnerStack. For every owner id, infer its item schema in the controller's own outer region, then build:- one required property beneath
foreach.<owner-id>; - that entry's string identity fields, integer
index, and inferreditem; - every active configured alias;
loop_itemandloop_indexfrom the final owner only.
The controller node itself uses its outer stack, so an inner controller may resolve
over=context.foreach.outer.item.childrenwithout claiming its own not-yet-active entry. - one required property beneath
-
Step 4: Keep context schema construction reusable and bounded
Implement
context_schemas_by_nodeby composing the returned contracts, not by running a second graph traversal.context_schema_for_nodeis the single-node convenience over that map. Preserve bounded local$refresolution for item schemas. A conflicted or unreachable node has no generated per-node schema; callers treat that absence as invalid, not as root context. -
Step 5: Expose structured paths through authoring inventory
Add tests showing
context_path_options_for_node(workflow, "inner_body")includes at least:paths = {option["path"] for option in options} assert "context.foreach.customers.item" in paths assert "context.foreach.customers.index" in paths assert "context.foreach.orders.item" in paths assert "context.foreach.orders.index" in pathsReuse the existing bounded schema-navigation helper to emit nested context properties. Preserve
origin="runtime_context",uses=["step_input"], availability, descriptions, and literal TOML path quoting. Do not hand-concatenate a dotted foreach id; format literal segments throughGraphSourcePath. -
Step 6: Run and commit the static projection slice
Run:
uv run pytest -q tests/core/test_context_scopes.py \ tests/wf_api/test_authoring_contracts.py uv run ruff check src/wf_core/context_contracts.py \ src/wf_core/analysis/context_scopes.py src/wf_api/authoring_contracts.py uv run basedpyright --level error src/wf_core/context_contracts.py \ src/wf_core/analysis/context_scopes.py src/wf_api/authoring_contracts.pyExpected: all commands pass.
git add src/wf_core/context_contracts.py \ src/wf_core/analysis/context_scopes.py src/wf_core/analysis/__init__.py \ src/wf_api/authoring_contracts.py tests/core/test_context_scopes.py \ tests/wf_api/test_authoring_contracts.py git commit -m "feat: describe structured foreach context"
Task 5: Validate Context Paths And Alias Ownership From One Schema
Files:
- Create:
src/wf_core/validation/context_paths.py - Modify:
src/wf_core/validation/core.py - Modify:
src/wf_core/validation/issues.py - Modify:
src/wf_core/validation/steps.py - Create:
tests/core/test_structured_context_validation.py - Modify:
tests/core/test_context_scopes.py
Interfaces:
-
Consumes:
context_schemas_by_node(workflow, control_regions=analysis)and one sharedControlRegionAnalysisfrom Tasks 1 and 4. -
Produces:
def validate_context_paths( workflow: Workflow, *, context_schemas: Mapping[str, ContextSchema], report: ValidationReport, ) -> None: ... -
Adds exact issue codes:
INVALID_CONTEXT_PATH = "invalid_context_path" FOREACH_CONTEXT_ALIAS_CONFLICT = "foreach_context_alias_conflict" -
Ordinary input/state validation remains where it is. The new pass owns the stronger, program-location-aware meaning of
context.*. -
Step 1: Write failing context-path validation tests
Add:
test_active_structured_foreach_item_path_is_validtest_nested_body_can_read_outer_and_inner_entriestest_inactive_foreach_entry_is_rejectedtest_missing_foreach_id_is_rejectedtest_unknown_foreach_entry_field_is_rejectedtest_unreachable_node_does_not_receive_root_context_fallbacktest_workflow_output_cannot_read_completed_foreach_entry
For failures, assert both code and model location:
issue = next( issue for issue in report.errors if issue.code == ValidationIssueCode.INVALID_CONTEXT_PATH ) assert issue.path == "nodes[3].input[0].path" assert "context.foreach.orders.item" in issue.message assert "work" in issue.message -
Step 2: Cover every model surface that can contain a graph path
Parameterize invalid
context.foreach.missing.itemreferences through:NodeUse.inputpath bindings and nested input expressions;SubgraphNode.input;ConditionNode.checkincluding nested conditions;ForeachNode.over;InterruptNode.requestpath bindings and expressions;- workflow output bindings.
Each case must assert the exact model path reported by validation. This test prevents a future path-bearing model from accidentally retaining the current permissive
allow_context=Truebehavior. -
Step 3: Add one bounded structural path walker
In
validation/context_paths.py, use small typed walkers for conditions and input expressions:def _expression_paths( expression: InputExpression, location: str, ) -> Iterator[tuple[str, GraphSourcePath]]: match expression: case PathExpression(path=path): yield location + ".path", path case ArrayExpression(items=items): for index, item in enumerate(items): yield from _expression_paths(item, f"{location}.items[{index}]") case ObjectExpression(fields=fields): for name, item in fields.items(): yield from _expression_paths(item, f"{location}.fields.{name}") case LiteralExpression(): returnMirror the same finite recursion for condition operands. Reuse these walkers for every step kind; do not duplicate context validation in each existing step validator.
-
Step 4: Validate context paths against the consuming location
For paths whose root is
context, walk their literalpartsthrough the consuming node's generated schema. A path is valid only if every segment is a declared object property. The wholecontextobject andcontext.foreachmap remain readable; unknown dynamic keys do not.Workflow output uses
root_context_schema(), which contains standard execution fields and an empty structured foreach map. This preservescontext.scope_idwhile rejecting a completed iteration value.Change
validate_foreach_nodeso context-rootedoverpaths reach this pass instead of being rejected by the old input/state-only check. -
Step 5: Write failing alias-collision tests
Add:
test_foreach_alias_cannot_use_reserved_context_nametest_nested_active_foreach_aliases_must_be_uniquetest_sibling_foreach_aliases_may_match_when_never_active_together
Reserved names are every standard context field plus
foreach,loop_item, andloop_index. The nested failure points to the inner foreach'sasfield. Siblings in separate control regions may reuse an alias. -
Step 6: Share control-region analysis during validation
In
validate_workflow, runanalyze_control_regions(workflow)once. Feed the result to context schema construction, translate its issues as today, then callvalidate_context_paths. Avoid a second traversal hidden insidecontext_fields_by_node; add an optional internalcontrol_regions=input if necessary while keeping the existing public call form valid. -
Step 7: Run and commit the validation slice
Run:
uv run pytest -q tests/core/test_structured_context_validation.py \ tests/core/test_context_scopes.py \ tests/core/test_foreach_control_regions.py \ tests/core/test_input_expressions.py tests/core/test_subgraph_step.py uv run ruff check src/wf_core/validation tests/core/test_structured_context_validation.py uv run basedpyright --level error src/wf_core/validationExpected: all commands pass.
git add src/wf_core/validation/context_paths.py \ src/wf_core/validation/core.py src/wf_core/validation/issues.py \ src/wf_core/validation/steps.py \ tests/core/test_structured_context_validation.py \ tests/core/test_context_scopes.py git commit -m "feat: validate structured context paths"
Task 6: Prove Resume And Subgraph Isolation, Then Publish The Contract
Files:
- Modify:
tests/core/test_structured_runtime_context.py - Modify:
tests/core/test_subgraph_step.py - Modify:
tests/core/test_concurrent_foreach_interrupts.py - Modify:
docs/wf_authoring_control_flow.md - Modify:
skills/wf-python/SKILL.md - Modify:
skills/wf-python/references/python-lifecycle.md - Modify:
docs/current_roadmap.md - Modify:
docs/superpowers/specs/2026-09-04-structured-runtime-context-design.md - Move after completion:
docs/superpowers/plans/2026-09-04-structured-runtime-context.mdtodocs/historical/superpowers/plans/2026-09-04-structured-runtime-context.md
Interfaces:
-
Consumes: the complete runtime, authoring, schema, and validation behavior from Tasks 1-5.
-
Produces a public example that prefers declared input bindings via
foreach_ref.item, while documentingRuntimeContext.foreachas the advanced handler escape hatch. -
Step 1: Write the interrupt-resume identity regression
Build
outer.loop -> inner.loop -> ask -> inner -> outer, interrupt one inner item, serialize it withdump_run_state, restore it withload_run_state, and resume. Capture context before and after:before = captured_before_interrupt[0] after = captured_after_resume[0] assert after["outer"].activation_id == before["outer"].activation_id assert after["inner"].activation_id == before["inner"].activation_id assert after["inner"].frame_id == before["inner"].frame_id assert after["inner"].lineage_id == before["inner"].lineage_id assert after["inner"].item == before["inner"].itemDo not reuse the original in-memory
RunState; the loaded value is the resume input so reconstruction is genuinely tested. -
Step 2: Write the complete subgraph scope-boundary test
Build a parent foreach and map
each.iteminto a saved child subgraph's declared input. Give both parent and child a foreach node with idorders. Assert:assert child_seen["input_order"] == {"sku": "A-17"} assert tuple(child_seen["context"].foreach) == ("orders",) assert child_seen["context"].foreach["orders"].item == "child-item" assert child_seen["context"].foreach["orders"].scope_id != parent_scope_idAlso execute a child node before its own foreach and assert
child_ctx.foreach == {}. This proves the caller entry was not inherited and the reused static id does not collide. -
Step 3: Run the persistence and scope pressure tests
Run:
uv run pytest -q tests/core/test_structured_runtime_context.py \ tests/core/test_subgraph_step.py tests/core/test_concurrent_foreach_interrupts.pyExpected: all commands pass.
-
Step 4: Document the preferred authoring and advanced Python forms
Add this shape to
docs/wf_authoring_control_flow.mdand the Python skill:orders = graph.foreach( id="orders", over=state_path("orders"), as_="order", ) charge = graph.use( charge_order, input=[input_from(orders.item, "order")], ) graph.set_route(orders, "loop", charge) graph.set_route(charge, "ok", orders)State explicitly:
- normal capabilities receive foreach values through declared inputs;
- advanced handlers may inspect
ctx.foreach["orders"].indexand stable runtime identities; - child workflows do not inherit caller context and must receive input;
loop_item,loop_index, and aliases are migration conveniences.
-
Step 5: Mark the implementation current and retire the live plan
Change the spec status from approved to implemented, replace the roadmap's proposed wording with a completed current-runtime statement, and move this fully checked plan under
docs/historical/superpowers/plans/. Search for the old live-plan path and update any links:rg -n -F 'superpowers/plans/2026-09-04-structured-runtime-context.md' \ docs skills -
Step 6: Run full verification
Run:
uv run pytest -q uv run ruff check uv run ruff format --check uv run basedpyright --level error pnpx markdownlint-cli2 \ 'docs/superpowers/specs/2026-09-04-structured-runtime-context-design.md' \ 'docs/wf_authoring_control_flow.md' \ 'skills/wf-python/SKILL.md' \ 'skills/wf-python/references/python-lifecycle.md' \ 'docs/current_roadmap.md' \ 'docs/historical/superpowers/plans/2026-09-04-structured-runtime-context.md' git diff --checkExpected: all commands pass. If the repository's known thesis-PDF baseline remains the only failure, record its exact failing test and verify it also fails at the plan's starting commit before treating it as baseline.
-
Step 7: Commit the integration and documentation slice
Stage exact paths so the user's
docs/AGENTS.mdedit remains untouched:git add tests/core/test_structured_runtime_context.py \ tests/core/test_subgraph_step.py \ tests/core/test_concurrent_foreach_interrupts.py \ docs/wf_authoring_control_flow.md skills/wf-python/SKILL.md \ skills/wf-python/references/python-lifecycle.md docs/current_roadmap.md \ docs/superpowers/specs/2026-09-04-structured-runtime-context-design.md \ docs/historical/superpowers/plans/2026-09-04-structured-runtime-context.md git commit -m "docs: publish structured runtime context"
Plan Self-Review Checklist
- Every required test group in the spec maps to Tasks 1-6.
- Runtime and static projections both consume the same static ids, item fields, and scope boundary.
- Validation covers every current model location that can embed a
GraphSourcePath. - Authoring properties and inventory both construct dotted ids as literal TOML segments.
- No task adds host runtime context, fork/gather behavior, step budgeting, or a new graph path root.
- The user's dirty
docs/AGENTS.mdis never staged.