orchestrata_ultimate.session.models
Reference › orchestrata_ultimate › session › models
The Pydantic data models of the session package: the documents that ContextFileManager serialises into context.json and renders into context.md, and that the controllers, managers and CLI commands pass around.
For a single project: WorkItem (a deferred item with a TaskState, priority and notes; mark_done, mark_todo and mark_deferred change its state), LogEntry (an entry in the session work log), FileArtifact (a file created or changed), TechChoice (a decision and its reason), HandoffData (decisions, blockers, active work, key files, commands and alerts for the next session), SessionRecord (one session, with session_id and header_line helpers), ProjectState (current state of the project) and ContextData, the full document with its session history, deferred work and state; it can return the latest session, the next session number and the pending work, and append a session.
For several projects: PortfolioProjectRef, ProjectSessionLink, PortfolioSessionRecord (which can link a project session), PortfolioState and PortfolioData, managed by the PortfolioController.
For goals (SPEC-023): GoalCriterion (an exit criterion with a status and an optional verify command), GoalTask, GoalProgress and Goal, used by GoalMixin. default_ai_model resolves the current AI-model label at call time.
At a glance
| Source | src/orchestrata_ultimate/session/models.py |
| Lines of code | 435 |
| Classes | 18 |
| Functions | 1 |
| Imports (internal) | orchestrata_ultimate.core.base.traces, orchestrata_ultimate.core.domain.goal |
| Imported by | orchestrata_ultimate.context.assembler, orchestrata_ultimate.context.providers.mission_context, orchestrata_ultimate.context.providers.plan_context, orchestrata_ultimate.kernel.pipeline_kg, orchestrata_ultimate.session, orchestrata_ultimate.session.archive, orchestrata_ultimate.session.carryover, orchestrata_ultimate.session.cli.goals, orchestrata_ultimate.session.cli.lifecycle, orchestrata_ultimate.session.cli.tasks, orchestrata_ultimate.session.context, orchestrata_ultimate.session.controller._changelog, orchestrata_ultimate.session.controller._core, orchestrata_ultimate.session.controller._git, orchestrata_ultimate.session.controller._goals, orchestrata_ultimate.session.facts, orchestrata_ultimate.session.handover, orchestrata_ultimate.session.hooks.session_auto_start_hook, orchestrata_ultimate.session.portfolio, orchestrata_ultimate.session.session |
| Tests that import it | tests/agents/volks/pm/test_goal_wiring.py, tests/context/providers/test_mission_context.py, tests/context/test_assembler.py, tests/factories/session.py, tests/kernel/test_pipeline_kg.py, tests/session/cli/test_lifecycle.py, tests/session/cli/test_overview.py, tests/session/controller/test_git.py, tests/session/hooks/test_session_auto_start_hook.py, tests/session/test_archive.py, tests/session/test_archive_coverage.py, tests/session/test_carryover_coverage.py, tests/session/test_context_coverage.py, tests/session/test_context_durability.py, tests/session/test_context_extended.py, tests/session/test_context_window.py, tests/session/test_event_emitting_session_controller.py, tests/session/test_facts.py, tests/session/test_facts_coverage.py, tests/session/test_goal_evaluate.py, tests/session/test_goal_models.py, tests/session/test_goal_step_tracking.py, tests/session/test_goal_stop_gate.py, tests/session/test_handover_coverage.py, tests/session/test_session_archive.py … and 18 more |
Class diagram
Dependency diagram
API
Classes
TaskState (enum)
Bases: StrEnum · Line 47
State of a work item.
| Member | Value |
|---|---|
TODO |
'todo' |
ACTIVE |
'active' |
DONE |
'done' |
BLOCKED |
'blocked' |
DEFERRED |
'deferred' |
WorkItem (pydantic)
Bases: BaseModel · Decorators: traces(spec='020', story='US-006', reqs=['REQ-020-174']) · Line 58
Single work item to track.
| Attribute | Type | Default |
|---|---|---|
text |
str |
|
state |
TaskState |
TaskState.TODO |
priority |
str | None |
None |
notes |
str | None |
None |
timestamp |
datetime |
Field(default_factory=datetime.now) |
done_at |
datetime | None |
None |
| Method | Signature | Summary |
|---|---|---|
mark_done |
mark_done() -> None |
Mark item as done. |
mark_todo |
mark_todo() -> None |
Return item to TODO, clearing any completion stamp. |
mark_deferred |
mark_deferred() -> None |
Mark item as deferred. |
LogEntry (pydantic)
Bases: BaseModel · Decorators: traces(spec='006', story='FR-012', reqs=['REQ-006-FR12-002', 'REQ-006-FR12-005']) · Line 92
Entry in session work log.
| Attribute | Type | Default |
|---|---|---|
desc |
str |
|
details |
list[str] |
Field(default_factory=list) |
at |
datetime |
Field(default_factory=datetime.now) |
FileArtifact (pydantic)
Bases: BaseModel · Line 100
File created during session.
| Attribute | Type | Default |
|---|---|---|
filepath |
str |
|
line_count |
int | None |
None |
summary |
str | None |
None |
TechChoice (pydantic)
Bases: BaseModel · Line 108
Technical decision made.
| Attribute | Type | Default |
|---|---|---|
choice |
str |
|
reason |
str |
|
at |
datetime |
Field(default_factory=datetime.now) |
HandoffData (pydantic)
Bases: BaseModel · Line 116
Data for session handoff.
| Attribute | Type | Default |
|---|---|---|
decisions |
list[str] |
Field(default_factory=list) |
blockers |
list[str] |
Field(default_factory=list) |
active_work |
list[str] |
Field(default_factory=list) |
key_files |
list[str] |
Field(default_factory=list) |
cmds |
list[str] |
Field(default_factory=list) |
alerts |
list[str] |
Field(default_factory=list) |
SessionRecord (pydantic)
Bases: BaseModel · Decorators: traces(spec='006', story='US-001', reqs=['REQ-006-FR10-004', 'REQ-006-FR12-002', 'REQ-006-FR2-002', 'REQ-006-US-001-003… · Line 139
Single session record.
| Attribute | Type | Default |
|---|---|---|
seq |
int |
|
when |
datetime |
|
ai_model |
str |
|
purpose |
str |
|
length |
str | None |
None |
objectives |
list[str] |
Field(default_factory=list) |
log |
list[LogEntry] |
Field(default_factory=list) |
artifacts |
list[FileArtifact] |
Field(default_factory=list) |
choices |
list[TechChoice] |
Field(default_factory=list) |
achievements |
list[str] |
Field(default_factory=list) |
upcoming |
list[str] |
Field(default_factory=list) |
handoff |
HandoffData | None |
None |
| Method | Signature | Summary |
|---|---|---|
session_id property |
session_id() -> str |
Get formatted session ID. |
header_line property |
header_line() -> str |
Get formatted header line. |
ProjectState (pydantic)
Bases: BaseModel · Decorators: traces(spec='020', story='US-006', reqs=['REQ-020-173']) · Line 169
Current project state.
| Attribute | Type | Default |
|---|---|---|
active_session |
int | None |
None |
version |
str |
'0.1.0' |
git_branch |
str |
'master' |
test_state |
str | None |
None |
status_line |
str |
'Initial setup' |
goal |
Goal | None |
None |
portfolio_goals |
list[PortfolioGoal] |
Field(default_factory=list) |
ContextData (pydantic)
Bases: BaseModel · Decorators: traces(spec='006', story='FR-014', reqs=['REQ-006-FR14-006']) · Line 188
Full context document data.
| Attribute | Type | Default |
|---|---|---|
state |
ProjectState |
Field(default_factory=ProjectState) |
deferred_work |
list[WorkItem] |
Field(default_factory=list) |
session_history |
list[SessionRecord] |
Field(default_factory=list) |
| Method | Signature | Summary |
|---|---|---|
latest_session |
latest_session() -> SessionRecord | None |
Get most recent session. |
next_session_num |
next_session_num() -> int |
Get next session number. |
append_session |
append_session(session: SessionRecord) -> None |
Add session to history. |
pending_work |
pending_work() -> list[WorkItem] |
Get pending work items. |
PortfolioProjectRef (pydantic)
Bases: BaseModel · Line 221
Reference to project in portfolio.
| Attribute | Type | Default |
|---|---|---|
name |
str |
|
path |
str |
|
summary |
str | None |
None |
is_active |
bool |
True |
| Method | Signature | Summary |
|---|---|---|
absolute_path |
absolute_path(base: Path) -> Path |
Get absolute path. |
ProjectSessionLink (pydantic)
Bases: BaseModel · Line 237
Link to project session from portfolio.
| Attribute | Type | Default |
|---|---|---|
project |
str |
|
session_num |
int |
|
focus |
str | None |
None |
PortfolioSessionRecord (pydantic)
Bases: BaseModel · Line 245
Portfolio-level session.
| Attribute | Type | Default |
|---|---|---|
seq |
int |
|
when |
datetime |
Field(default_factory=datetime.now) |
ai_model |
str |
Field(default_factory=default_ai_model) |
purpose |
str |
|
length |
str | None |
None |
objectives |
list[str] |
Field(default_factory=list) |
linked |
list[ProjectSessionLink] |
Field(default_factory=list) |
log |
list[LogEntry] |
Field(default_factory=list) |
achievements |
list[str] |
Field(default_factory=list) |
upcoming |
list[str] |
Field(default_factory=list) |
handoff |
HandoffData | None |
None |
| Method | Signature | Summary |
|---|---|---|
session_id property |
session_id() -> str |
Get formatted session ID. |
link_session |
link_session(project: str, session_num: int, focus: str | None = None) -> None |
Link a project session. |
PortfolioState (pydantic)
Bases: BaseModel · Line 274
Portfolio state.
| Attribute | Type | Default |
|---|---|---|
active_session |
int | None |
None |
status |
str | None |
None |
modified_at |
datetime |
Field(default_factory=datetime.now) |
PortfolioData (pydantic)
Bases: BaseModel · Line 282
Full portfolio data.
| Attribute | Type | Default |
|---|---|---|
name |
str |
|
summary |
str | None |
None |
created |
datetime |
Field(default_factory=datetime.now) |
state |
PortfolioState |
Field(default_factory=PortfolioState) |
projects |
list[PortfolioProjectRef] |
Field(default_factory=list) |
sessions |
list[PortfolioSessionRecord] |
Field(default_factory=list) |
work_items |
list[WorkItem] |
Field(default_factory=list) |
| Method | Signature | Summary |
|---|---|---|
add_project |
add_project(name: str, path: str, summary: str | None = None) -> PortfolioProjectRef |
Add project to portfolio. |
get_project |
get_project(name: str) -> PortfolioProjectRef | None |
Get project by name. |
drop_project |
drop_project(name: str) -> bool |
Remove project by name. |
add_session |
add_session(session: PortfolioSessionRecord) -> None |
Add session to portfolio. |
latest_session |
latest_session() -> PortfolioSessionRecord | None |
Get most recent session. |
next_session_num |
next_session_num() -> int |
Get next session number. |
pending_work |
pending_work() -> list[WorkItem] |
Get pending work items. |
add_work |
add_work(text: str, priority: str | None = None) -> WorkItem |
Add work item. |
GoalCriterion (pydantic)
Bases: BaseModel · Line 355
Exit criterion for goal achievement.
| Attribute | Type | Default |
|---|---|---|
id |
str |
|
description |
str |
|
status |
str |
Field(default='pending', description='pending, met, or fail… |
verify_command |
str | None |
None |
verify_pattern |
str | None |
None |
met_at |
str | None |
None |
GoalTask (pydantic)
Bases: BaseModel · Line 369
Task that contributes to goal achievement.
| Attribute | Type | Default |
|---|---|---|
id |
str |
|
description |
str |
|
criteria |
list[str] |
Field(default_factory=list) |
status |
str |
Field(default='pending', description='pending, in_progress,… |
created_at |
str |
Field(default_factory=lambda: datetime.now().isoformat()) |
done_at |
str | None |
None |
GoalProgress (pydantic)
Bases: BaseModel · Line 385
Logged progress entry for a goal.
| Attribute | Type | Default |
|---|---|---|
at |
str |
Field(default_factory=lambda: datetime.now().isoformat()) |
action |
str |
|
note |
str |
|
criteria_changed |
dict[str, str] |
Field(default_factory=dict) |
Goal (pydantic)
Bases: BaseModel · Line 397
A persistent goal with measurable exit criteria.
| Attribute | Type | Default |
|---|---|---|
id |
str |
Field(default_factory=lambda: f"goal-{datetime.now().strfti… |
statement |
str |
|
status |
str |
Field(default='active', description='active, achieved, bloc… |
set_at |
str |
Field(default_factory=lambda: datetime.now().isoformat()) |
achieved_at |
str | None |
None |
abandoned_at |
str | None |
None |
criteria |
list[GoalCriterion] |
Field(default_factory=list) |
tasks |
list[GoalTask] |
Field(default_factory=list) |
current_step |
int |
Field(default=0, ge=0, description='Index of the current ta… |
progress |
list[GoalProgress] |
Field(default_factory=list) |
blockers |
list[str] |
Field(default_factory=list) |
retry_counts |
dict[str, int] |
Field(default_factory=dict) |
Functions
| Function | Signature | Summary |
|---|---|---|
default_ai_model |
default_ai_model() -> str |
Current AI-model label, resolved at call time. |
Source
"""Fresh implementation of session models.
Pydantic models for session management system.
All models use strict validation.
"""
# Spec: 006 - Session Management
import os
from datetime import datetime
from enum import StrEnum
from pathlib import Path
from pydantic import BaseModel, Field
from orchestrata_ultimate.core.base.traces import traces
# Portfolio goal domain types now live in core (layer 1) so the agents layer
# (U:PM GoalRegistry) can import them without an upward cross-layer import.
# Re-exported here (redundant ``as`` alias = explicit re-export) for backward
# compatibility; PortfolioGoal is also used below by ProjectState.
from orchestrata_ultimate.core.domain.goal import (
PortfolioGoal as PortfolioGoal,
)
from orchestrata_ultimate.core.domain.goal import (
PortfolioGoalPriority as PortfolioGoalPriority,
)
from orchestrata_ultimate.core.domain.goal import (
PortfolioGoalStatus as PortfolioGoalStatus,
)
from orchestrata_ultimate.core.domain.goal import (
PortfolioSuccessCriteria as PortfolioSuccessCriteria,
)
_DEFAULT_AI_MODEL = "Claude Opus 4.8"
def default_ai_model() -> str:
"""Current AI-model label, resolved at call time.
Reads ``ORCHESTRATA_AI_MODEL`` so the model recorded on session records isn't
frozen to a stale hardcoded default (previously "Claude Opus 4.5" in 5 places).
"""
return os.environ.get("ORCHESTRATA_AI_MODEL", _DEFAULT_AI_MODEL)
class TaskState(StrEnum):
"""State of a work item."""
TODO = "todo"
ACTIVE = "active"
DONE = "done"
BLOCKED = "blocked"
DEFERRED = "deferred"
@traces(spec="020", story="US-006", reqs=["REQ-020-174"])
class WorkItem(BaseModel):
"""Single work item to track."""
text: str
state: TaskState = TaskState.TODO
priority: str | None = None
notes: str | None = None
timestamp: datetime = Field(default_factory=datetime.now)
done_at: datetime | None = None
@traces(spec="020", story="US-006", reqs=["REQ-020-174"])
def mark_done(self) -> None:
"""Mark item as done."""
self.state = TaskState.DONE
self.done_at = datetime.now()
@traces(spec="020", story="US-006", reqs=["REQ-020-174"])
def mark_todo(self) -> None:
"""Return item to TODO, clearing any completion stamp.
The inverse of :meth:`mark_done`. It lives here rather than in the
caller because reopening means clearing ``done_at`` as well as
``state`` -- a caller that reassigns only ``state`` leaves an item
that is open but carries a completion timestamp.
"""
self.state = TaskState.TODO
self.done_at = None
def mark_deferred(self) -> None:
"""Mark item as deferred."""
self.state = TaskState.DEFERRED
@traces(spec="006", story="FR-012", reqs=["REQ-006-FR12-002", "REQ-006-FR12-005"])
class LogEntry(BaseModel):
"""Entry in session work log."""
desc: str
details: list[str] = Field(default_factory=list)
at: datetime = Field(default_factory=datetime.now)
class FileArtifact(BaseModel):
"""File created during session."""
filepath: str
line_count: int | None = None
summary: str | None = None
class TechChoice(BaseModel):
"""Technical decision made."""
choice: str
reason: str
at: datetime = Field(default_factory=datetime.now)
class HandoffData(BaseModel):
"""Data for session handoff."""
decisions: list[str] = Field(default_factory=list)
blockers: list[str] = Field(default_factory=list)
active_work: list[str] = Field(default_factory=list)
key_files: list[str] = Field(default_factory=list)
cmds: list[str] = Field(default_factory=list)
alerts: list[str] = Field(default_factory=list)
@traces(
spec="006",
story="US-001",
reqs=[
"REQ-006-FR10-004",
"REQ-006-FR12-002",
"REQ-006-FR2-002",
"REQ-006-US-001-003",
"REQ-006-US-003-003",
"REQ-006-US-005-002",
],
)
class SessionRecord(BaseModel):
"""Single session record."""
seq: int
when: datetime
ai_model: str
purpose: str
length: str | None = None
objectives: list[str] = Field(default_factory=list)
log: list[LogEntry] = Field(default_factory=list)
artifacts: list[FileArtifact] = Field(default_factory=list)
choices: list[TechChoice] = Field(default_factory=list)
achievements: list[str] = Field(default_factory=list)
upcoming: list[str] = Field(default_factory=list)
handoff: HandoffData | None = None
@property
def session_id(self) -> str:
"""Get formatted session ID."""
return f"{self.seq:03d}"
@property
def header_line(self) -> str:
"""Get formatted header line."""
time_str = self.when.strftime("%Y-%m-%d %H:%M")
return f"Session {self.session_id} - {time_str} - {self.ai_model}"
@traces(spec="020", story="US-006", reqs=["REQ-020-173"])
class ProjectState(BaseModel):
"""Current project state.
Includes SPEC-023 goal system for persistent, measurable goals.
"""
active_session: int | None = None
version: str = "0.1.0"
git_branch: str = "master"
test_state: str | None = None
status_line: str = "Initial setup"
# SPEC-023: Goal system persistence
goal: Goal | None = None
# SPEC-012: Portfolio goal system persistence (distinct from the
# singular session goal above — see PortfolioGoal's docstring).
portfolio_goals: list[PortfolioGoal] = Field(default_factory=list)
@traces(spec="006", story="FR-014", reqs=["REQ-006-FR14-006"])
class ContextData(BaseModel):
"""Full context document data."""
state: ProjectState = Field(default_factory=ProjectState)
deferred_work: list[WorkItem] = Field(default_factory=list)
session_history: list[SessionRecord] = Field(default_factory=list)
@traces(spec="006", story="FR-010", reqs=["REQ-006-FR10-004"])
def latest_session(self) -> SessionRecord | None:
"""Get most recent session."""
if not self.session_history:
return None
return max(self.session_history, key=lambda s: s.seq)
@traces(spec="006", story="FR-014", reqs=["REQ-006-FR14-007"])
def next_session_num(self) -> int:
"""Get next session number."""
if not self.session_history:
return 0
return max(s.seq for s in self.session_history) + 1
@traces(spec="020", story="US-006", reqs=["REQ-020-172"])
def append_session(self, session: SessionRecord) -> None:
"""Add session to history."""
self.session_history.append(session)
self.state.active_session = session.seq
@traces(spec="006", story="US-006", reqs=["REQ-006-US-006-001"])
def pending_work(self) -> list[WorkItem]:
"""Get pending work items."""
return [w for w in self.deferred_work if w.state != TaskState.DONE]
class PortfolioProjectRef(BaseModel):
"""Reference to project in portfolio."""
name: str
path: str
summary: str | None = None
is_active: bool = True
def absolute_path(self, base: Path) -> Path:
"""Get absolute path."""
p = Path(self.path)
if p.is_absolute():
return p
return base / p
class ProjectSessionLink(BaseModel):
"""Link to project session from portfolio."""
project: str
session_num: int
focus: str | None = None
class PortfolioSessionRecord(BaseModel):
"""Portfolio-level session."""
seq: int
when: datetime = Field(default_factory=datetime.now)
ai_model: str = Field(default_factory=default_ai_model)
purpose: str
length: str | None = None
objectives: list[str] = Field(default_factory=list)
linked: list[ProjectSessionLink] = Field(default_factory=list)
log: list[LogEntry] = Field(default_factory=list)
achievements: list[str] = Field(default_factory=list)
upcoming: list[str] = Field(default_factory=list)
handoff: HandoffData | None = None
@property
def session_id(self) -> str:
"""Get formatted session ID."""
return f"{self.seq:03d}"
def link_session(
self, project: str, session_num: int, focus: str | None = None
) -> None:
"""Link a project session."""
self.linked.append(
ProjectSessionLink(project=project, session_num=session_num, focus=focus)
)
class PortfolioState(BaseModel):
"""Portfolio state."""
active_session: int | None = None
status: str | None = None
modified_at: datetime = Field(default_factory=datetime.now)
class PortfolioData(BaseModel):
"""Full portfolio data."""
name: str
summary: str | None = None
created: datetime = Field(default_factory=datetime.now)
state: PortfolioState = Field(default_factory=PortfolioState)
projects: list[PortfolioProjectRef] = Field(default_factory=list)
sessions: list[PortfolioSessionRecord] = Field(default_factory=list)
work_items: list[WorkItem] = Field(default_factory=list)
def add_project(
self, name: str, path: str, summary: str | None = None
) -> PortfolioProjectRef:
"""Add project to portfolio."""
proj = PortfolioProjectRef(name=name, path=path, summary=summary)
self.projects.append(proj)
return proj
def get_project(self, name: str) -> PortfolioProjectRef | None:
"""Get project by name."""
for p in self.projects:
if p.name == name:
return p
return None
def drop_project(self, name: str) -> bool:
"""Remove project by name."""
for i, p in enumerate(self.projects):
if p.name == name:
self.projects.pop(i)
return True
return False
def add_session(self, session: PortfolioSessionRecord) -> None:
"""Add session to portfolio."""
self.sessions.append(session)
self.state.active_session = session.seq
self.state.modified_at = datetime.now()
def latest_session(self) -> PortfolioSessionRecord | None:
"""Get most recent session."""
if not self.sessions:
return None
return max(self.sessions, key=lambda s: s.seq)
@traces(spec="006", story="FR-014", reqs=["REQ-006-FR14-007"])
def next_session_num(self) -> int:
"""Get next session number."""
if not self.sessions:
return 0
return max(s.seq for s in self.sessions) + 1
def pending_work(self) -> list[WorkItem]:
"""Get pending work items."""
return [
w
for w in self.work_items
if w.state in (TaskState.TODO, TaskState.DEFERRED)
]
def add_work(self, text: str, priority: str | None = None) -> WorkItem:
"""Add work item."""
item = WorkItem(text=text, priority=priority, state=TaskState.TODO)
self.work_items.append(item)
return item
# ============================================================================
# SPEC-023: Goal-Driven Session System Models
# ============================================================================
class GoalCriterion(BaseModel):
"""Exit criterion for goal achievement.
A boolean condition that must be true for the goal to be achieved.
"""
id: str
description: str
status: str = Field(default="pending", description="pending, met, or failed")
verify_command: str | None = None
verify_pattern: str | None = None
met_at: str | None = None
class GoalTask(BaseModel):
"""Task that contributes to goal achievement.
A unit of work that moves one or more exit criteria from pending to met.
"""
id: str
description: str
criteria: list[str] = Field(default_factory=list)
status: str = Field(
default="pending", description="pending, in_progress, done, or failed"
)
created_at: str = Field(default_factory=lambda: datetime.now().isoformat())
done_at: str | None = None
class GoalProgress(BaseModel):
"""Logged progress entry for a goal.
Records what changed toward the goal at a specific time.
"""
at: str = Field(default_factory=lambda: datetime.now().isoformat())
action: str
note: str
criteria_changed: dict[str, str] = Field(default_factory=dict)
class Goal(BaseModel):
"""A persistent goal with measurable exit criteria.
Persists in context.json under state.goal. The agent commits to a goal,
decomposes it into tasks, evaluates progress against exit criteria, and
continues until the goal is achieved or blocked.
"""
id: str = Field(
default_factory=lambda: f"goal-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
)
statement: str
status: str = Field(
default="active", description="active, achieved, blocked, or abandoned"
)
set_at: str = Field(default_factory=lambda: datetime.now().isoformat())
achieved_at: str | None = None
abandoned_at: str | None = None
criteria: list[GoalCriterion] = Field(default_factory=list)
tasks: list[GoalTask] = Field(default_factory=list)
current_step: int = Field(
default=0,
ge=0,
description="Index of the current task in ``tasks`` for plan-window rendering.",
)
progress: list[GoalProgress] = Field(default_factory=list)
blockers: list[str] = Field(default_factory=list)
retry_counts: dict[str, int] = Field(default_factory=dict)
# ============================================================================
# SPEC-012: Portfolio Goal System Models
# ============================================================================
# The portfolio goal domain types (PortfolioGoalStatus / PortfolioGoalPriority /
# PortfolioSuccessCriteria / PortfolioGoal) moved to
# ``orchestrata_ultimate.core.domain.goal`` (layer 1) so the agents layer
# (U:PM ``GoalRegistry``) can import them WITHOUT an upward cross-layer import.
# They are re-exported at the top of this module for backward compatibility, and
# ``ProjectState.portfolio_goals`` (above) references the re-exported name.