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.
- forgex/__init__.py +3 -0
- forgex/__main__.py +6 -0
- forgex/agents/__init__.py +36 -0
- forgex/agents/architect.py +233 -0
- forgex/agents/base.py +385 -0
- forgex/agents/builder.py +269 -0
- forgex/agents/ensemble.py +334 -0
- forgex/agents/expert_coder.py +283 -0
- forgex/agents/supervisor.py +303 -0
- forgex/cli/__init__.py +1 -0
- forgex/cli/app.py +232 -0
- forgex/cli/build_views.py +94 -0
- forgex/cli/commands/__init__.py +1 -0
- forgex/cli/commands/build.py +63 -0
- forgex/cli/commands/checkpoint.py +41 -0
- forgex/cli/commands/config.py +225 -0
- forgex/cli/commands/context.py +94 -0
- forgex/cli/commands/diff.py +32 -0
- forgex/cli/commands/do_cmd.py +50 -0
- forgex/cli/commands/doctor.py +175 -0
- forgex/cli/commands/fix.py +46 -0
- forgex/cli/commands/init.py +41 -0
- forgex/cli/commands/logs.py +102 -0
- forgex/cli/commands/models.py +144 -0
- forgex/cli/commands/plan.py +103 -0
- forgex/cli/commands/review.py +76 -0
- forgex/cli/commands/rollback.py +65 -0
- forgex/cli/commands/run.py +75 -0
- forgex/cli/commands/status.py +141 -0
- forgex/cli/commands/supervise.py +61 -0
- forgex/cli/commands/task.py +136 -0
- forgex/cli/commands/tools.py +116 -0
- forgex/cli/commands/verify.py +65 -0
- forgex/cli/context_memory.py +62 -0
- forgex/cli/intent.py +209 -0
- forgex/cli/progress.py +101 -0
- forgex/cli/repl.py +377 -0
- forgex/cli/runtime.py +233 -0
- forgex/cli/session.py +95 -0
- forgex/cli/ui.py +113 -0
- forgex/config/__init__.py +53 -0
- forgex/config/loader.py +189 -0
- forgex/config/schema.py +145 -0
- forgex/context/__init__.py +6 -0
- forgex/context/chunker.py +102 -0
- forgex/context/dependency_graph.py +110 -0
- forgex/context/indexer.py +161 -0
- forgex/context/manager.py +174 -0
- forgex/context/retriever.py +99 -0
- forgex/models/__init__.py +25 -0
- forgex/models/base.py +112 -0
- forgex/models/capabilities.py +21 -0
- forgex/models/factory.py +66 -0
- forgex/models/groq.py +13 -0
- forgex/models/local.py +10 -0
- forgex/models/openai_compatible.py +233 -0
- forgex/models/registry.py +81 -0
- forgex/models/router.py +332 -0
- forgex/orchestration/__init__.py +36 -0
- forgex/orchestration/checkpoints.py +117 -0
- forgex/orchestration/engine.py +573 -0
- forgex/orchestration/events.py +101 -0
- forgex/orchestration/lockfile.py +82 -0
- forgex/orchestration/retry.py +65 -0
- forgex/orchestration/state.py +89 -0
- forgex/orchestration/stores.py +90 -0
- forgex/orchestration/workflow.py +103 -0
- forgex/project.py +209 -0
- forgex/prompts/__init__.py +27 -0
- forgex/prompts/architect.md +18 -0
- forgex/prompts/builder.md +20 -0
- forgex/prompts/expert_coder.md +25 -0
- forgex/prompts/reviewer.md +11 -0
- forgex/prompts/supervisor.md +26 -0
- forgex/research/__init__.py +15 -0
- forgex/research/citations.py +16 -0
- forgex/research/engine.py +83 -0
- forgex/research/sources.py +154 -0
- forgex/sandbox/__init__.py +37 -0
- forgex/sandbox/executor.py +418 -0
- forgex/sandbox/permissions.py +67 -0
- forgex/sandbox/policies.py +172 -0
- forgex/schemas/__init__.py +101 -0
- forgex/schemas/agent.py +109 -0
- forgex/schemas/checkpoint.py +19 -0
- forgex/schemas/error.py +125 -0
- forgex/schemas/event.py +76 -0
- forgex/schemas/plan.py +84 -0
- forgex/schemas/project.py +92 -0
- forgex/schemas/research.py +25 -0
- forgex/schemas/result.py +68 -0
- forgex/schemas/task.py +118 -0
- forgex/storage/__init__.py +25 -0
- forgex/storage/database.py +40 -0
- forgex/storage/migrations.py +130 -0
- forgex/storage/repositories.py +492 -0
- forgex/tools/__init__.py +73 -0
- forgex/tools/base.py +212 -0
- forgex/tools/dependencies.py +137 -0
- forgex/tools/filesystem.py +322 -0
- forgex/tools/git.py +239 -0
- forgex/tools/project.py +39 -0
- forgex/tools/search.py +205 -0
- forgex/tools/terminal.py +81 -0
- forgex/tools/testing.py +100 -0
- forgex/tools/verification.py +174 -0
- forgex/utils/__init__.py +15 -0
- forgex/utils/logging.py +28 -0
- forgex/utils/paths.py +48 -0
- forgex/utils/redact.py +44 -0
- forgex_cli-0.1.0.dist-info/METADATA +189 -0
- forgex_cli-0.1.0.dist-info/RECORD +115 -0
- forgex_cli-0.1.0.dist-info/WHEEL +4 -0
- forgex_cli-0.1.0.dist-info/entry_points.txt +2 -0
- 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
|
+
]
|
forgex/schemas/agent.py
ADDED
|
@@ -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
|
forgex/schemas/error.py
ADDED
|
@@ -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
|
forgex/schemas/event.py
ADDED
|
@@ -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
|
forgex/schemas/result.py
ADDED
|
@@ -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
|