planning package
Reviewed against commit 8a2cb42ad on 2026-10-23.
Reference: generated package page orchestrata_ultimate.planning (2 modules, 389 lines of code); key module plan_parser. The goal loop that consumes it is in session; the wider picture (specs, governance, work flow) is in the orchestrata overview.
The story
Long-horizon agent work in this project is steered by a human-authored execution plan — a markdown document like plans/execution-plan.md whose phases (## Phase 0: Foundation) contain numbered steps (### Step 1: Fix wrong import path in logging_config.py), each carrying bold-labeled fields: **Goal:**, **File:**, **Actions:**, **Verify:**, **Expect:**/**Expected:**, and **If it fails:**. Agents cannot afford to re-read and re-interpret that prose on every volk call, so orchestrata_ultimate.planning (SPEC-107 FR-001) exists to turn the plan into a compact, machine-readable step list once, up front. It is a deliberately small package — two modules, one of them the facade — and entirely pure: no LLM, no infrastructure, just markdown in and dataclasses out.
The single data type is PlanStep, a frozen dataclass in plan_parser.py holding the step number, title, containing phase name, goal text, target file, an ordered tuple of action items, and the verify/expected/fallback fields, with a to_dict() for serialization. Parsing flows top-down: parse_plan_text normalizes line endings, splits the document into (phase_name, phase_body) chunks at ## headings (a plan with no phase headings is treated as one anonymous phase), finds each ### Step N: title heading inside a phase, and hands the step body to a field extractor. That extractor locates every known bold tag in the body, captures the text between one tag and the next tag, the next --- horizontal rule, or the end of the body, and canonicalizes **Expect:** and **Expected:** into a single expected field, keeping the first non-empty occurrence when a tag appears twice. Action blocks are split into individual items — numbered (1. ...) or bulleted (- ...) entries, with unmarked continuation lines folded into the previous item and internal newlines collapsed for compactness. The result is sorted by step_num; anything that cannot be parsed as a step is simply skipped rather than raised, so a hand-edited plan degrades to fewer steps, never to an exception.
Getting the plan off disk is the only impure edge, and it follows the project's env-seam rule. parse_plan_file takes an explicit path, or resolves one from the ORCHESTRATA_PLAN_PATH environment variable (read at call time through an injectable env mapping, defaulting to plans/execution-plan.md), reads the file — raising FileNotFoundError when the resolved plan does not exist — and delegates to parse_plan_text. A small find_step(steps, step_num) helper completes the surface with a linear lookup by step number. Both parse entry points carry @traces decorators tying them to SPEC-107 REQ-107-003 and REQ-107-060, which is how the RTM tooling verifies coverage.
A later addition (REQ-107-060 and SPEC-007 US-002) gives the module a governance role as well. parse_plan_status(text) extracts a plan document's own **Status:** marker (or None, which is a fact about the document and not a failure), is_plan_active(status) treats a plan as in force unless it carries an explicit terminal marker (DONE, COMPLETE, SUPERSEDED, CLOSED, ARCHIVED, CANCELLED), and load_plan_governance_facts(plan_path=None, *, env=None) resolves, reads and parses the plan into a frozen PlanGovernanceFacts (plan id, active flag, status, steps). Python asserts these as ground facts; the Prolog rule plan_fidelity/2 in constitutional.pl only reasons over them. The parser is therefore used by the kernel dispatcher and the session goal loop alike.
The package's place in the system is as the write-side of the SPEC-023 goal loop. session/controller/_goals.py's GoalMixin calls parse_plan_file when a goal is set from a plan, converts the returned steps into the goal's task list, and resets current_step to zero; that goal state persists in context.json through the session ContextFileManager. From there the read-side takes over in a different package: context.providers.plan_context.render_plan_context renders the goal's current, recently completed, and upcoming tasks into the plan layer of every assembled volk prompt. So the division of labor is clean — planning parses the human plan into structure exactly once, the session layer owns the resulting goal state, and the context layer windows it back into prompts — which is precisely why this package can stay two modules with no dependencies beyond core.traceability.
Key responsibilities
- Model one plan step as an immutable, serializable record — number, title, phase, goal, file, actions, verify, expected, fallback (
plan_parser.py,PlanStep). - Parse execution-plan markdown into ordered
PlanStepobjects — phase splitting, step-heading recognition, bold-tag field capture withExpect/Expectedcanonicalization, and numbered/bulleted action-list parsing with continuation folding — skipping unparseable steps instead of raising (plan_parser.py,parse_plan_text). - Load the plan from disk with call-time
ORCHESTRATA_PLAN_PATHenv resolution and an injectableenvseam, raisingFileNotFoundErrorfor a missing file (plan_parser.py,parse_plan_file). - Look up a step by number for callers tracking a current position (
plan_parser.py,find_step). - Judge whether a plan is still in force for constitutional Article XII (plan fidelity, RUL-015): read a plan's own
**Status:**marker and package the parsed steps as ground facts (parse_plan_status,is_plan_active,PlanGovernanceFacts,load_plan_governance_factsinplan_parser.py). - Re-export the package facade (
__init__.py); at 8a2cb42ad the facade is 15 lines, so check the generated__init__page for the exact export list.
Class diagram
The package's one data type (generated from the real source):
Module inventory
| Module | Purpose |
|---|---|
planning/__init__.py |
Package facade — re-exports PlanStep, parse_plan_text, parse_plan_file, and find_step. |
planning/plan_parser.py |
The whole implementation (SPEC-107 FR-001, REQ-107-003/060; SPEC-007 US-002) — the PlanStep model, the phase/step/field markdown parser, env-resolved file loading, step lookup, and the plan-status / PlanGovernanceFacts helpers for Article XII. |
plan_parser.py
Everything lives here. PlanStep is the frozen record; parse_plan_text(text) is the pure parser — phases split at ## headings, steps matched by the ### Step N: title pattern, and each step body mined for the bold field tags (**Goal:**, **File:**, **Actions:**, **Verify:**, **Expect:**/**Expected:**, **If it fails:**). Field capture runs positionally: every tag occurrence is located, values extend to the next tag, the next --- rule, or the end of the body, and duplicate tags keep the first non-empty value with expect canonicalized to expected. _parse_actions turns the actions block into a tuple of items, recognizing numbered and bulleted markers and folding bare continuation lines into the preceding item. Steps come back sorted by step_num, with unparseable material silently dropped. parse_plan_file(plan_path=None, *, env=None) adds the disk edge — explicit path, else ORCHESTRATA_PLAN_PATH, else plans/execution-plan.md, with FileNotFoundError on a missing file — and find_step is the linear by-number lookup. The parse functions are @traces-decorated to SPEC-107 REQ-107-003/060; the sole consumer today is the session GoalMixin, which converts parsed steps into SPEC-023 goal tasks.
Class map (generated)
Generated from the live source AST by scripts/gen-module-class-diagrams.py — do not hand-edit. Abstract classes have a green border; members listed are those defined on the class itself (inherited members excluded). Solid empty-triangle arrows = in-package inheritance; dashed arrows into dashed boxes = external/stdlib/cross-package bases; filled diamond = composition (constructed in __init__); open diamond = aggregation (annotated/injected reference); diamond edge labels name the owning attribute.