forgex-cli 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. forgex/__init__.py +3 -0
  2. forgex/__main__.py +6 -0
  3. forgex/agents/__init__.py +36 -0
  4. forgex/agents/architect.py +233 -0
  5. forgex/agents/base.py +385 -0
  6. forgex/agents/builder.py +269 -0
  7. forgex/agents/ensemble.py +334 -0
  8. forgex/agents/expert_coder.py +283 -0
  9. forgex/agents/supervisor.py +303 -0
  10. forgex/cli/__init__.py +1 -0
  11. forgex/cli/app.py +232 -0
  12. forgex/cli/build_views.py +94 -0
  13. forgex/cli/commands/__init__.py +1 -0
  14. forgex/cli/commands/build.py +63 -0
  15. forgex/cli/commands/checkpoint.py +41 -0
  16. forgex/cli/commands/config.py +225 -0
  17. forgex/cli/commands/context.py +94 -0
  18. forgex/cli/commands/diff.py +32 -0
  19. forgex/cli/commands/do_cmd.py +50 -0
  20. forgex/cli/commands/doctor.py +175 -0
  21. forgex/cli/commands/fix.py +46 -0
  22. forgex/cli/commands/init.py +41 -0
  23. forgex/cli/commands/logs.py +102 -0
  24. forgex/cli/commands/models.py +144 -0
  25. forgex/cli/commands/plan.py +103 -0
  26. forgex/cli/commands/review.py +76 -0
  27. forgex/cli/commands/rollback.py +65 -0
  28. forgex/cli/commands/run.py +75 -0
  29. forgex/cli/commands/status.py +141 -0
  30. forgex/cli/commands/supervise.py +61 -0
  31. forgex/cli/commands/task.py +136 -0
  32. forgex/cli/commands/tools.py +116 -0
  33. forgex/cli/commands/verify.py +65 -0
  34. forgex/cli/context_memory.py +62 -0
  35. forgex/cli/intent.py +209 -0
  36. forgex/cli/progress.py +101 -0
  37. forgex/cli/repl.py +377 -0
  38. forgex/cli/runtime.py +233 -0
  39. forgex/cli/session.py +95 -0
  40. forgex/cli/ui.py +113 -0
  41. forgex/config/__init__.py +53 -0
  42. forgex/config/loader.py +189 -0
  43. forgex/config/schema.py +145 -0
  44. forgex/context/__init__.py +6 -0
  45. forgex/context/chunker.py +102 -0
  46. forgex/context/dependency_graph.py +110 -0
  47. forgex/context/indexer.py +161 -0
  48. forgex/context/manager.py +174 -0
  49. forgex/context/retriever.py +99 -0
  50. forgex/models/__init__.py +25 -0
  51. forgex/models/base.py +112 -0
  52. forgex/models/capabilities.py +21 -0
  53. forgex/models/factory.py +66 -0
  54. forgex/models/groq.py +13 -0
  55. forgex/models/local.py +10 -0
  56. forgex/models/openai_compatible.py +233 -0
  57. forgex/models/registry.py +81 -0
  58. forgex/models/router.py +332 -0
  59. forgex/orchestration/__init__.py +36 -0
  60. forgex/orchestration/checkpoints.py +117 -0
  61. forgex/orchestration/engine.py +573 -0
  62. forgex/orchestration/events.py +101 -0
  63. forgex/orchestration/lockfile.py +82 -0
  64. forgex/orchestration/retry.py +65 -0
  65. forgex/orchestration/state.py +89 -0
  66. forgex/orchestration/stores.py +90 -0
  67. forgex/orchestration/workflow.py +103 -0
  68. forgex/project.py +209 -0
  69. forgex/prompts/__init__.py +27 -0
  70. forgex/prompts/architect.md +18 -0
  71. forgex/prompts/builder.md +20 -0
  72. forgex/prompts/expert_coder.md +25 -0
  73. forgex/prompts/reviewer.md +11 -0
  74. forgex/prompts/supervisor.md +26 -0
  75. forgex/research/__init__.py +15 -0
  76. forgex/research/citations.py +16 -0
  77. forgex/research/engine.py +83 -0
  78. forgex/research/sources.py +154 -0
  79. forgex/sandbox/__init__.py +37 -0
  80. forgex/sandbox/executor.py +418 -0
  81. forgex/sandbox/permissions.py +67 -0
  82. forgex/sandbox/policies.py +172 -0
  83. forgex/schemas/__init__.py +101 -0
  84. forgex/schemas/agent.py +109 -0
  85. forgex/schemas/checkpoint.py +19 -0
  86. forgex/schemas/error.py +125 -0
  87. forgex/schemas/event.py +76 -0
  88. forgex/schemas/plan.py +84 -0
  89. forgex/schemas/project.py +92 -0
  90. forgex/schemas/research.py +25 -0
  91. forgex/schemas/result.py +68 -0
  92. forgex/schemas/task.py +118 -0
  93. forgex/storage/__init__.py +25 -0
  94. forgex/storage/database.py +40 -0
  95. forgex/storage/migrations.py +130 -0
  96. forgex/storage/repositories.py +492 -0
  97. forgex/tools/__init__.py +73 -0
  98. forgex/tools/base.py +212 -0
  99. forgex/tools/dependencies.py +137 -0
  100. forgex/tools/filesystem.py +322 -0
  101. forgex/tools/git.py +239 -0
  102. forgex/tools/project.py +39 -0
  103. forgex/tools/search.py +205 -0
  104. forgex/tools/terminal.py +81 -0
  105. forgex/tools/testing.py +100 -0
  106. forgex/tools/verification.py +174 -0
  107. forgex/utils/__init__.py +15 -0
  108. forgex/utils/logging.py +28 -0
  109. forgex/utils/paths.py +48 -0
  110. forgex/utils/redact.py +44 -0
  111. forgex_cli-0.1.0.dist-info/METADATA +189 -0
  112. forgex_cli-0.1.0.dist-info/RECORD +115 -0
  113. forgex_cli-0.1.0.dist-info/WHEEL +4 -0
  114. forgex_cli-0.1.0.dist-info/entry_points.txt +2 -0
  115. forgex_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,101 @@
1
+ """Pydantic schemas and the FORGEX error taxonomy."""
2
+
3
+ from forgex.schemas.agent import (
4
+ AgentRole,
5
+ BuilderResult,
6
+ ExpertCoderResult,
7
+ ReviewFinding,
8
+ ReviewFindings,
9
+ SupervisorJudgment,
10
+ SupervisorReport,
11
+ )
12
+ from forgex.schemas.checkpoint import CheckpointRecord
13
+ from forgex.schemas.error import (
14
+ AgentOutputError,
15
+ AuthenticationError,
16
+ BuildError,
17
+ CommandDeniedError,
18
+ ConfigError,
19
+ ErrorRecord,
20
+ FileSystemError,
21
+ ForgexError,
22
+ GitError,
23
+ PathViolationError,
24
+ ProjectError,
25
+ ProviderError,
26
+ RateLimitError,
27
+ SandboxError,
28
+ StorageError,
29
+ TaskGraphError,
30
+ TestError,
31
+ ToolError,
32
+ WorkflowError,
33
+ )
34
+ from forgex.schemas.event import Event, EventType
35
+ from forgex.schemas.plan import (
36
+ AnalysisResult,
37
+ ArchitectResult,
38
+ ArchitecturePlan,
39
+ TaskSpec,
40
+ specs_to_tasks,
41
+ )
42
+ from forgex.schemas.project import ProjectState, RunState, RunStatus, WorkflowStatus
43
+ from forgex.schemas.research import ResearchNote
44
+ from forgex.schemas.result import (
45
+ BuildResults,
46
+ CommandOutcome,
47
+ ModelUsageRecord,
48
+ TestResults,
49
+ VerificationOutcome,
50
+ )
51
+ from forgex.schemas.task import Task, TaskPriority, TaskStatus
52
+
53
+ __all__ = [
54
+ "AgentOutputError",
55
+ "AgentRole",
56
+ "AnalysisResult",
57
+ "ArchitectResult",
58
+ "ArchitecturePlan",
59
+ "AuthenticationError",
60
+ "BuildError",
61
+ "BuildResults",
62
+ "BuilderResult",
63
+ "CheckpointRecord",
64
+ "CommandDeniedError",
65
+ "CommandOutcome",
66
+ "ConfigError",
67
+ "ErrorRecord",
68
+ "Event",
69
+ "EventType",
70
+ "ExpertCoderResult",
71
+ "FileSystemError",
72
+ "ForgexError",
73
+ "GitError",
74
+ "ModelUsageRecord",
75
+ "PathViolationError",
76
+ "ProjectError",
77
+ "ProjectState",
78
+ "ProviderError",
79
+ "RateLimitError",
80
+ "ResearchNote",
81
+ "ReviewFinding",
82
+ "ReviewFindings",
83
+ "RunState",
84
+ "RunStatus",
85
+ "SandboxError",
86
+ "StorageError",
87
+ "SupervisorJudgment",
88
+ "SupervisorReport",
89
+ "Task",
90
+ "TaskGraphError",
91
+ "TaskPriority",
92
+ "TaskSpec",
93
+ "TaskStatus",
94
+ "TestError",
95
+ "TestResults",
96
+ "ToolError",
97
+ "VerificationOutcome",
98
+ "WorkflowError",
99
+ "WorkflowStatus",
100
+ "specs_to_tasks",
101
+ ]
@@ -0,0 +1,109 @@
1
+ """Agent identity, roles, and agent output schemas."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from enum import Enum
6
+ from typing import Any, Literal
7
+
8
+ from pydantic import BaseModel, ConfigDict, Field
9
+
10
+ from forgex.schemas.plan import ArchitectResult as ArchitectResult
11
+
12
+
13
+ class AgentRole(str, Enum):
14
+ ARCHITECT = "architect"
15
+ BUILDER = "builder"
16
+ EXPERT_CODER = "expert_coder"
17
+ SUPERVISOR = "supervisor"
18
+
19
+ @property
20
+ def display_name(self) -> str:
21
+ return self.value.replace("_", " ").title()
22
+
23
+
24
+ class BuilderResult(BaseModel):
25
+ """The Builder's structured self-report after executing one task (§9).
26
+
27
+ NOTE: 'completed' is the Builder's own claim — independent verification
28
+ is the Supervisor's responsibility (Phase 8) and the repair loop (Phase 9).
29
+ """
30
+
31
+ model_config = ConfigDict(extra="ignore")
32
+
33
+ status: Literal["completed", "failed"]
34
+ summary: str = ""
35
+ changed_files: list[str] = Field(default_factory=list)
36
+ issues: list[str] = Field(default_factory=list)
37
+
38
+
39
+ class ExpertCoderResult(BaseModel):
40
+ """The Expert Coder's structured review report (spec section 10).
41
+
42
+ Empty changed_files is a valid outcome: 'reviewed, nothing warranted
43
+ changes'. unfixed_issues are handed forward to the Supervisor (Phase 8).
44
+ """
45
+
46
+ model_config = ConfigDict(extra="ignore")
47
+
48
+ status: Literal["completed", "failed"]
49
+ summary: str = ""
50
+ changed_files: list[str] = Field(default_factory=list)
51
+ improvements: list[str] = Field(default_factory=list)
52
+ issues_found: list[str] = Field(default_factory=list)
53
+ unfixed_issues: list[str] = Field(default_factory=list)
54
+
55
+
56
+ class ReviewFinding(BaseModel):
57
+ """One issue found by an ensemble reviewer (spec section 15)."""
58
+
59
+ model_config = ConfigDict(extra="ignore")
60
+
61
+ issue: str
62
+ severity: Literal["critical", "major", "minor"] = "major"
63
+ file: str | None = None
64
+ suggestion: str | None = None
65
+
66
+
67
+ class ReviewFindings(BaseModel):
68
+ """One reviewer's read-only verdict on a diff."""
69
+
70
+ model_config = ConfigDict(extra="ignore")
71
+
72
+ verdict: Literal["ok", "needs_changes"]
73
+ summary: str = ""
74
+ findings: list[ReviewFinding] = Field(default_factory=list)
75
+
76
+
77
+ class SupervisorJudgment(BaseModel):
78
+ """The LLM Supervisor's verdict (spec section 11 output)."""
79
+
80
+ model_config = ConfigDict(extra="ignore")
81
+
82
+ status: Literal["pass", "fail", "needs_review"]
83
+ summary: str = ""
84
+ errors: list[str] = Field(default_factory=list)
85
+ warnings: list[str] = Field(default_factory=list)
86
+ required_changes: list[str] = Field(default_factory=list)
87
+ recommended_changes: list[str] = Field(default_factory=list)
88
+
89
+
90
+ class SupervisorReport(BaseModel):
91
+ """Final supervisor verdict: LLM judgment + deterministic quality gates.
92
+
93
+ Gates can only WORSEN a verdict, never improve it (spec sections 56, 58):
94
+ a red test suite forces 'fail' regardless of the model's opinion."""
95
+
96
+ model_config = ConfigDict(extra="ignore")
97
+
98
+ status: Literal["pass", "fail", "needs_review"]
99
+ summary: str = ""
100
+ errors: list[str] = Field(default_factory=list)
101
+ warnings: list[str] = Field(default_factory=list)
102
+ required_changes: list[str] = Field(default_factory=list)
103
+ recommended_changes: list[str] = Field(default_factory=list)
104
+ tests: Any | None = None
105
+ build: Any | None = None
106
+ lint: Any | None = None
107
+
108
+
109
+ SupervisorReport.model_rebuild()
@@ -0,0 +1,19 @@
1
+ """Checkpoint metadata (spec section 21)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import UTC, datetime
6
+
7
+ from pydantic import BaseModel, ConfigDict, Field
8
+
9
+
10
+ class CheckpointRecord(BaseModel):
11
+ model_config = ConfigDict(extra="forbid")
12
+
13
+ id: str
14
+ project_id: str
15
+ label: str
16
+ git_commit: str | None = None
17
+ description: str | None = None
18
+ created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
19
+ run_id: str | None = None
@@ -0,0 +1,125 @@
1
+ """FORGEX error taxonomy and the persisted error record (spec sections 5, 25, 28).
2
+
3
+ All controlled failures derive from :class:`ForgexError` so the CLI can
4
+ render friendly messages and keep raw stack traces behind ``--debug``.
5
+ Exit codes are stable per error class.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from datetime import UTC, datetime
11
+ from typing import Any
12
+
13
+ from pydantic import BaseModel, ConfigDict, Field
14
+
15
+ from typer._click import ClickException
16
+
17
+
18
+ class ForgexError(ClickException):
19
+ """Base class for all controlled FORGEX errors."""
20
+
21
+ exit_code: int = 1
22
+
23
+ def __init__(self, message: str = "") -> None:
24
+ super().__init__(message)
25
+
26
+
27
+ class ConfigError(ForgexError):
28
+ exit_code = 2
29
+
30
+
31
+ class FileSystemError(ForgexError):
32
+ exit_code = 3
33
+
34
+
35
+ class ProjectError(ForgexError):
36
+ exit_code = 4
37
+
38
+
39
+ class ProviderError(ForgexError):
40
+ """Provider-level failure. `retryable` drives the retry policy (§29);
41
+ `attempts` records how many attempts were made (§40)."""
42
+
43
+ exit_code = 5
44
+ default_retryable: bool = False
45
+ attempts: int = 1
46
+
47
+ def __init__(self, message: str, *, retryable: bool | None = None, attempts: int = 1) -> None:
48
+ super().__init__(message)
49
+ self.retryable = self.default_retryable if retryable is None else retryable
50
+ self.attempts = attempts
51
+
52
+
53
+ class AuthenticationError(ProviderError):
54
+ exit_code = 6
55
+ default_retryable = False
56
+
57
+
58
+ class RateLimitError(ProviderError):
59
+ exit_code = 7
60
+ default_retryable = True
61
+
62
+
63
+ class ToolError(ForgexError):
64
+ exit_code = 8
65
+
66
+
67
+ class PermissionDeniedError(ToolError):
68
+ """An agent attempted an action outside its permission set."""
69
+ exit_code = 8
70
+
71
+
72
+ class SandboxError(ForgexError):
73
+ exit_code = 9
74
+
75
+
76
+ class CommandDeniedError(SandboxError):
77
+ """A command was blocked by policy or not approved (spec sections 19, 33)."""
78
+ exit_code = 9
79
+
80
+
81
+ class GitError(ForgexError):
82
+ exit_code = 10
83
+
84
+
85
+ class BuildError(ForgexError):
86
+ exit_code = 11
87
+
88
+
89
+ class TestError(ForgexError):
90
+ exit_code = 12
91
+
92
+
93
+ class WorkflowError(ForgexError):
94
+ exit_code = 14
95
+
96
+
97
+ class TaskGraphError(ForgexError):
98
+ exit_code = 15
99
+
100
+
101
+ class StorageError(ForgexError):
102
+ exit_code = 16
103
+
104
+
105
+ class AgentOutputError(ForgexError):
106
+ """An agent could not produce valid structured output (spec sections 7, 28)."""
107
+ exit_code = 17
108
+
109
+
110
+ class PathViolationError(FileSystemError):
111
+ """A path attempted to escape the authorized workspace."""
112
+ exit_code = 3
113
+
114
+
115
+ class ErrorRecord(BaseModel):
116
+ """Persisted error record for state and history."""
117
+
118
+ model_config = ConfigDict(extra="forbid")
119
+
120
+ ts: datetime = Field(default_factory=lambda: datetime.now(UTC))
121
+ source: str
122
+ kind: str
123
+ message: str
124
+ run_id: str | None = None
125
+ detail: dict[str, Any] | None = None
@@ -0,0 +1,76 @@
1
+ """Event schema (spec section 24).
2
+
3
+ On-disk format (.forgex/events.jsonl), one JSON object per line:
4
+
5
+ {"ts": "<iso-8601 utc>", "type": "<EventType>", "run_id": null, "data": {...}}
6
+
7
+ Introduced in Phase 1; ``run_id`` added in Phase 2 (readers tolerate its
8
+ absence). This format is FROZEN — treat it as a public contract.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ from datetime import UTC, datetime
15
+ from enum import Enum
16
+ from typing import Any
17
+
18
+ from pydantic import BaseModel, ConfigDict, Field
19
+
20
+
21
+ class EventType(str, Enum):
22
+ PROJECT_INITIALIZED = "ProjectInitialized"
23
+ RUN_STARTED = "RunStarted"
24
+ RUN_COMPLETED = "RunCompleted"
25
+ RUN_FAILED = "RunFailed"
26
+ AGENT_STARTED = "AgentStarted"
27
+ AGENT_THINKING = "AgentThinking"
28
+ AGENT_COMPLETED = "AgentCompleted"
29
+ TOOL_STARTED = "ToolStarted"
30
+ TOOL_COMPLETED = "ToolCompleted"
31
+ FILE_CHANGED = "FileChanged"
32
+ TASK_STARTED = "TaskStarted"
33
+ TASK_COMPLETED = "TaskCompleted"
34
+ TEST_STARTED = "TestStarted"
35
+ TEST_COMPLETED = "TestCompleted"
36
+ SUPERVISOR_STARTED = "SupervisorStarted"
37
+ REPAIR_STARTED = "RepairStarted"
38
+ RESEARCH_COMPLETED = "ResearchCompleted"
39
+ PLAN_CREATED = "PlanCreated"
40
+ CHECKPOINT_CREATED = "CheckpointCreated"
41
+ CHECKPOINT_ROLLED_BACK = "CheckpointRolledBack"
42
+ ENSEMBLE_COMPLETED = "EnsembleCompleted"
43
+ INTERRUPTED = "Interrupted"
44
+
45
+
46
+ class Event(BaseModel):
47
+ """One immutable event in the FORGEX observability stream."""
48
+
49
+ model_config = ConfigDict(extra="forbid")
50
+
51
+ ts: datetime = Field(default_factory=lambda: datetime.now(UTC))
52
+ type: EventType
53
+ data: dict[str, Any] = Field(default_factory=dict)
54
+ run_id: str | None = None
55
+
56
+ def to_jsonl(self) -> str:
57
+ return json.dumps(
58
+ {
59
+ "ts": self.ts.isoformat(),
60
+ "type": self.type.value,
61
+ "run_id": self.run_id,
62
+ "data": self.data,
63
+ },
64
+ default=str,
65
+ )
66
+
67
+ @classmethod
68
+ def from_jsonl(cls, line: str) -> Event:
69
+ """Parse one JSONL line. Raises ValueError/KeyError on malformed input."""
70
+ raw = json.loads(line)
71
+ return cls(
72
+ ts=datetime.fromisoformat(raw["ts"]),
73
+ type=EventType(raw["type"]),
74
+ run_id=raw.get("run_id"),
75
+ data=raw.get("data") or {},
76
+ )
forgex/schemas/plan.py ADDED
@@ -0,0 +1,84 @@
1
+ """Planning schemas (spec sections 7, 8, 54).
2
+
3
+ The Architect's structured outputs. `AnalysisResult` is the early
4
+ understanding pass; `ArchitecturePlan` is the final plan persisted to
5
+ state.json. `TaskSpec` is the plan-side task; `specs_to_tasks()` converts
6
+ specs into runtime Task objects for the task store.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from datetime import UTC, datetime
12
+ from typing import Any
13
+
14
+ from pydantic import BaseModel, ConfigDict, Field
15
+
16
+ from forgex.schemas.research import ResearchNote
17
+ from forgex.schemas.task import Task, TaskPriority
18
+
19
+
20
+ class TaskSpec(BaseModel):
21
+ """A task as planned by the Architect (no runtime state)."""
22
+
23
+ model_config = ConfigDict(extra="forbid")
24
+
25
+ id: str
26
+ description: str
27
+ priority: TaskPriority = TaskPriority.MEDIUM
28
+ depends_on: list[str] = Field(default_factory=list)
29
+ files: list[str] = Field(default_factory=list)
30
+ acceptance_criteria: list[str] = Field(default_factory=list)
31
+
32
+
33
+ class AnalysisResult(BaseModel):
34
+ """Stage 1: the Architect's understanding before research."""
35
+
36
+ model_config = ConfigDict(extra="forbid")
37
+
38
+ goal: str
39
+ requirements: list[str] = Field(default_factory=list)
40
+ candidate_libraries: list[str] = Field(default_factory=list)
41
+ research_topics: list[str] = Field(default_factory=list)
42
+
43
+
44
+ class ArchitecturePlan(BaseModel):
45
+ """Stage 2: the final implementation plan (spec section 7)."""
46
+
47
+ model_config = ConfigDict(extra="forbid")
48
+
49
+ goal: str
50
+ requirements: list[str] = Field(default_factory=list)
51
+ tasks: list[TaskSpec] = Field(default_factory=list)
52
+ dependencies: list[str] = Field(default_factory=list) # external systems/services
53
+ libraries: list[str] = Field(default_factory=list) # language packages
54
+ architecture: dict[str, Any] = Field(default_factory=dict)
55
+ risks: list[str] = Field(default_factory=list)
56
+ acceptance_criteria: list[str] = Field(default_factory=list)
57
+ test_strategy: list[str] = Field(default_factory=list)
58
+
59
+
60
+ class ArchitectResult(BaseModel):
61
+ """Everything the Architect produces in one planning run."""
62
+
63
+ plan: ArchitecturePlan
64
+ research: list[ResearchNote] = Field(default_factory=list)
65
+ analysis: AnalysisResult | None = None
66
+ prompt_version: str = "architect:v1"
67
+
68
+
69
+ def specs_to_tasks(specs: list[TaskSpec]) -> list[Task]:
70
+ """Convert planned task specs into runtime Task objects (pending, attempt 0)."""
71
+ now = datetime.now(UTC)
72
+ return [
73
+ Task(
74
+ id=spec.id,
75
+ description=spec.description,
76
+ priority=spec.priority,
77
+ depends_on=list(spec.depends_on),
78
+ files=list(spec.files),
79
+ acceptance_criteria=list(spec.acceptance_criteria),
80
+ created_at=now,
81
+ updated_at=now,
82
+ )
83
+ for spec in specs
84
+ ]
@@ -0,0 +1,92 @@
1
+ """Project and run state (spec sections 5, 53).
2
+
3
+ ProjectState is THE central shared object: agents read and write
4
+ structured state instead of giant conversations (spec section 2.7).
5
+ WorkflowStatus is the phase machine from spec section 53; the transition
6
+ RULES live in forgex.orchestration.workflow.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from datetime import UTC, datetime
12
+ from enum import Enum
13
+ from typing import Literal
14
+
15
+ from pydantic import BaseModel, ConfigDict, Field
16
+
17
+ from forgex.schemas.agent import AgentRole
18
+ from forgex.schemas.error import ErrorRecord
19
+ from forgex.schemas.plan import ArchitecturePlan
20
+ from forgex.schemas.research import ResearchNote
21
+ from forgex.schemas.result import BuildResults, TestResults
22
+
23
+
24
+ class WorkflowStatus(str, Enum):
25
+ INITIALIZING = "initializing"
26
+ ANALYZING = "analyzing"
27
+ RESEARCHING = "researching"
28
+ PLANNING = "planning"
29
+ BUILDING = "building"
30
+ REVIEWING = "reviewing"
31
+ TESTING = "testing"
32
+ SUPERVISING = "supervising"
33
+ REPAIRING = "repairing"
34
+ COMPLETED = "completed"
35
+ FAILED = "failed"
36
+ CANCELLED = "cancelled"
37
+
38
+
39
+ class RunStatus(str, Enum):
40
+ RUNNING = "running"
41
+ COMPLETED = "completed"
42
+ FAILED = "failed"
43
+ CANCELLED = "cancelled"
44
+
45
+
46
+ class RunState(BaseModel):
47
+ """Execution run record persisted to database."""
48
+
49
+ model_config = ConfigDict(extra="forbid")
50
+
51
+ run_id: str
52
+ project_id: str
53
+ request: str
54
+ status: RunStatus = RunStatus.RUNNING
55
+ phase: WorkflowStatus = WorkflowStatus.INITIALIZING
56
+ iteration: int = 0
57
+ started_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
58
+ finished_at: datetime | None = None
59
+ summary: str | None = None
60
+ error: str | None = None
61
+
62
+
63
+ class ProjectState(BaseModel):
64
+ """Durable project state, persisted to .forgex/state.json (spec section 5)."""
65
+
66
+ model_config = ConfigDict(extra="forbid")
67
+
68
+ schema_version: int = 1
69
+ project_id: str
70
+ project_path: str
71
+ created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
72
+ updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
73
+
74
+ user_request: str | None = None
75
+ status: WorkflowStatus = WorkflowStatus.INITIALIZING
76
+ iteration: int = 0
77
+ current_run_id: str | None = None
78
+ current_agent: AgentRole | None = None
79
+ current_task: str | None = None
80
+ git_commit: str | None = None
81
+
82
+ architecture_plan: ArchitecturePlan | None = None
83
+ research: list[ResearchNote] = Field(default_factory=list)
84
+ changed_files: list[str] = Field(default_factory=list)
85
+ tasks: list[str] = Field(default_factory=list)
86
+ completed_tasks: list[str] = Field(default_factory=list)
87
+ failed_tasks: list[str] = Field(default_factory=list)
88
+ errors: list[ErrorRecord] = Field(default_factory=list)
89
+
90
+ test_results: TestResults | None = None
91
+ build_results: BuildResults | None = None
92
+ last_supervisor_status: Literal["pass", "fail", "needs_review"] | None = None
@@ -0,0 +1,25 @@
1
+ """Research metadata (spec section 8).
2
+
3
+ Research never invents facts: every note records where it came from and
4
+ when it was checked — or is explicitly marked unavailable.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from datetime import UTC, datetime
10
+ from typing import Literal
11
+
12
+ from pydantic import BaseModel, ConfigDict, Field
13
+
14
+
15
+ class ResearchNote(BaseModel):
16
+ model_config = ConfigDict(extra="forbid")
17
+
18
+ topic: str
19
+ source: str # "pypi" | "npm" | "crates" | "go" | "none"
20
+ summary: str
21
+ version: str | None = None
22
+ checked_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
23
+ confidence: Literal["high", "medium", "low"] = "medium"
24
+ available: bool = True
25
+ detail: str | None = None # registry URL, or the unavailability reason
@@ -0,0 +1,68 @@
1
+ """Result schemas — model usage records and command verification outcomes (spec sections 40, 41, 56).
2
+
3
+ Token counts are stored exactly as reported by the provider. When usage
4
+ is not reported, values stay None — never fabricated (spec section 41).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from datetime import UTC, datetime
10
+
11
+ from pydantic import BaseModel, ConfigDict, Field
12
+
13
+
14
+ class ModelUsageRecord(BaseModel):
15
+ model_config = ConfigDict(extra="forbid")
16
+
17
+ ts: datetime = Field(default_factory=lambda: datetime.now(UTC))
18
+ run_id: str | None = None
19
+ agent: str
20
+ provider: str
21
+ model: str
22
+ input_tokens: int | None = None
23
+ output_tokens: int | None = None
24
+ total_tokens: int | None = None
25
+ latency_ms: float | None = None
26
+ attempts: int = 1
27
+ success: bool = True
28
+ error: str | None = None
29
+
30
+
31
+ class CommandOutcome(BaseModel):
32
+ """One executed command's outcome (tests/build/lint). `output_tail` is the
33
+ last few KB — not the full stream (spec section 16)."""
34
+
35
+ model_config = ConfigDict(extra="forbid")
36
+
37
+ command: list[str] = Field(default_factory=list)
38
+ exit_code: int | None = None # None = never ran (denied by policy)
39
+ timed_out: bool = False
40
+ duration_ms: float | None = None
41
+ output_tail: str = ""
42
+
43
+ @property
44
+ def ok(self) -> bool:
45
+ return self.exit_code == 0 and not self.timed_out
46
+
47
+
48
+ class TestResults(CommandOutcome):
49
+ """Test suite outcome. Counts are parsed when the runner reports them;
50
+ None = unknown — never fabricated (spec section 41)."""
51
+
52
+ __test__ = False
53
+
54
+ passed: int | None = None
55
+ failed: int | None = None
56
+ errors: int | None = None
57
+
58
+
59
+ class BuildResults(CommandOutcome):
60
+ """Build outcome (compile/packaging step, where applicable)."""
61
+
62
+
63
+ class VerificationOutcome(BaseModel):
64
+ """Deterministic verification results (spec sections 11, 56)."""
65
+
66
+ tests: TestResults | None = None
67
+ build: BuildResults | None = None
68
+ lint: CommandOutcome | None = None