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
forgex/schemas/task.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Task system (spec section 54).
|
|
2
|
+
|
|
3
|
+
Tasks support dependency ordering, priority, attempts, per-task files
|
|
4
|
+
and acceptance criteria. The graph operations (topological order,
|
|
5
|
+
readiness, cycle detection) live here so every consumer — engine, CLI,
|
|
6
|
+
tests — shares one implementation.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from collections import defaultdict
|
|
12
|
+
from datetime import UTC, datetime
|
|
13
|
+
from enum import Enum
|
|
14
|
+
|
|
15
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
16
|
+
|
|
17
|
+
from forgex.schemas.error import TaskGraphError
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TaskStatus(str, Enum):
|
|
21
|
+
PENDING = "pending"
|
|
22
|
+
IN_PROGRESS = "in_progress"
|
|
23
|
+
COMPLETED = "completed"
|
|
24
|
+
FAILED = "failed"
|
|
25
|
+
SKIPPED = "skipped"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TaskPriority(str, Enum):
|
|
29
|
+
LOW = "low"
|
|
30
|
+
MEDIUM = "medium"
|
|
31
|
+
HIGH = "high"
|
|
32
|
+
CRITICAL = "critical"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
_PRIORITY_RANK = {
|
|
36
|
+
TaskPriority.LOW: 0,
|
|
37
|
+
TaskPriority.MEDIUM: 1,
|
|
38
|
+
TaskPriority.HIGH: 2,
|
|
39
|
+
TaskPriority.CRITICAL: 3,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Task(BaseModel):
|
|
44
|
+
"""A single unit of work produced by the Architect, executed by the Builder."""
|
|
45
|
+
|
|
46
|
+
model_config = ConfigDict(extra="forbid")
|
|
47
|
+
|
|
48
|
+
id: str # e.g. "TASK-004"
|
|
49
|
+
description: str
|
|
50
|
+
priority: TaskPriority = TaskPriority.MEDIUM
|
|
51
|
+
status: TaskStatus = TaskStatus.PENDING
|
|
52
|
+
depends_on: list[str] = Field(default_factory=list)
|
|
53
|
+
files: list[str] = Field(default_factory=list)
|
|
54
|
+
acceptance_criteria: list[str] = Field(default_factory=list)
|
|
55
|
+
attempts: int = 0
|
|
56
|
+
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
|
57
|
+
updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
|
58
|
+
result: str | None = None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def topological_order(tasks: list[Task]) -> list[Task]:
|
|
62
|
+
"""Return tasks in topological order, with priority breaking ties among ready tasks.
|
|
63
|
+
|
|
64
|
+
Raises TaskGraphError on duplicate IDs, unknown dependencies, or cycles.
|
|
65
|
+
"""
|
|
66
|
+
task_map: dict[str, Task] = {}
|
|
67
|
+
for task in tasks:
|
|
68
|
+
if task.id in task_map:
|
|
69
|
+
raise TaskGraphError(f"Duplicate task ID: {task.id}")
|
|
70
|
+
task_map[task.id] = task
|
|
71
|
+
|
|
72
|
+
in_degree: dict[str, int] = {t.id: 0 for t in tasks}
|
|
73
|
+
dependents: dict[str, list[str]] = defaultdict(list)
|
|
74
|
+
|
|
75
|
+
for task in tasks:
|
|
76
|
+
for dep in task.depends_on:
|
|
77
|
+
if dep not in task_map:
|
|
78
|
+
raise TaskGraphError(f"Task {task.id} depends on unknown task: {dep}")
|
|
79
|
+
dependents[dep].append(task.id)
|
|
80
|
+
in_degree[task.id] += 1
|
|
81
|
+
|
|
82
|
+
# Ready heap or list sorted by priority descending
|
|
83
|
+
ready: list[str] = [t.id for t in tasks if in_degree[t.id] == 0]
|
|
84
|
+
ready.sort(key=lambda tid: (-_PRIORITY_RANK[task_map[tid].priority], tid))
|
|
85
|
+
|
|
86
|
+
ordered: list[Task] = []
|
|
87
|
+
|
|
88
|
+
while ready:
|
|
89
|
+
curr_id = ready.pop(0)
|
|
90
|
+
ordered.append(task_map[curr_id])
|
|
91
|
+
|
|
92
|
+
newly_ready: list[str] = []
|
|
93
|
+
for dep_id in dependents[curr_id]:
|
|
94
|
+
in_degree[dep_id] -= 1
|
|
95
|
+
if in_degree[dep_id] == 0:
|
|
96
|
+
newly_ready.append(dep_id)
|
|
97
|
+
|
|
98
|
+
if newly_ready:
|
|
99
|
+
ready.extend(newly_ready)
|
|
100
|
+
ready.sort(key=lambda tid: (-_PRIORITY_RANK[task_map[tid].priority], tid))
|
|
101
|
+
|
|
102
|
+
if len(ordered) != len(tasks):
|
|
103
|
+
raise TaskGraphError("Dependency cycle detected in tasks")
|
|
104
|
+
|
|
105
|
+
return ordered
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def ready_tasks(tasks: list[Task]) -> list[Task]:
|
|
109
|
+
"""Return list of tasks that are PENDING and whose dependencies are COMPLETED or SKIPPED."""
|
|
110
|
+
completed_or_skipped = {
|
|
111
|
+
t.id for t in tasks if t.status in (TaskStatus.COMPLETED, TaskStatus.SKIPPED)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
ready = []
|
|
115
|
+
for t in tasks:
|
|
116
|
+
if t.status == TaskStatus.PENDING and all(dep in completed_or_skipped for dep in t.depends_on):
|
|
117
|
+
ready.append(t)
|
|
118
|
+
return ready
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Storage package: SQLite database, migrations, and repositories."""
|
|
2
|
+
|
|
3
|
+
from forgex.storage.database import Database
|
|
4
|
+
from forgex.storage.migrations import run_migrations
|
|
5
|
+
from forgex.storage.repositories import (
|
|
6
|
+
CheckpointRepository,
|
|
7
|
+
ErrorRepository,
|
|
8
|
+
EventRepository,
|
|
9
|
+
ModelUsageRepository,
|
|
10
|
+
ProjectRepository,
|
|
11
|
+
RunRepository,
|
|
12
|
+
TaskRepository,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"CheckpointRepository",
|
|
17
|
+
"Database",
|
|
18
|
+
"ErrorRepository",
|
|
19
|
+
"EventRepository",
|
|
20
|
+
"ModelUsageRepository",
|
|
21
|
+
"ProjectRepository",
|
|
22
|
+
"RunRepository",
|
|
23
|
+
"TaskRepository",
|
|
24
|
+
"run_migrations",
|
|
25
|
+
]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""SQLite access layer (spec section 25).
|
|
2
|
+
|
|
3
|
+
The project database lives at .forgex/state.db (gitignored): a
|
|
4
|
+
rebuildable query index and run history. WAL mode keeps concurrent
|
|
5
|
+
readers safe; foreign keys are enforced.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import sqlite3
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from forgex.storage.migrations import run_migrations
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Database:
|
|
17
|
+
"""Lazy-connecting wrapper around a project-local SQLite database."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, path: Path) -> None:
|
|
20
|
+
self.path = Path(path)
|
|
21
|
+
self._conn: sqlite3.Connection | None = None
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def conn(self) -> sqlite3.Connection:
|
|
25
|
+
if self._conn is None:
|
|
26
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
27
|
+
conn = sqlite3.connect(str(self.path))
|
|
28
|
+
conn.row_factory = sqlite3.Row
|
|
29
|
+
conn.execute("PRAGMA foreign_keys = ON")
|
|
30
|
+
conn.execute("PRAGMA journal_mode = WAL")
|
|
31
|
+
conn.execute("PRAGMA synchronous = NORMAL")
|
|
32
|
+
conn.execute("PRAGMA busy_timeout = 5000")
|
|
33
|
+
run_migrations(conn)
|
|
34
|
+
self._conn = conn
|
|
35
|
+
return self._conn
|
|
36
|
+
|
|
37
|
+
def close(self) -> None:
|
|
38
|
+
if self._conn is not None:
|
|
39
|
+
self._conn.close()
|
|
40
|
+
self._conn = None
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Forward-only schema migrations for the project database.
|
|
2
|
+
|
|
3
|
+
v1 (Phase 2): projects, runs, tasks, events, errors
|
|
4
|
+
v2 (Phase 3): model_usage
|
|
5
|
+
v3 (Phase 6): checkpoints
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import sqlite3
|
|
11
|
+
|
|
12
|
+
SCHEMA_V1 = """
|
|
13
|
+
CREATE TABLE IF NOT EXISTS schema_version (
|
|
14
|
+
version INTEGER PRIMARY KEY
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
18
|
+
project_id TEXT PRIMARY KEY,
|
|
19
|
+
path TEXT NOT NULL,
|
|
20
|
+
name TEXT NOT NULL,
|
|
21
|
+
status TEXT NOT NULL,
|
|
22
|
+
created_at TEXT NOT NULL,
|
|
23
|
+
updated_at TEXT NOT NULL
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
CREATE TABLE IF NOT EXISTS runs (
|
|
27
|
+
run_id TEXT PRIMARY KEY,
|
|
28
|
+
project_id TEXT NOT NULL REFERENCES projects(project_id),
|
|
29
|
+
request TEXT NOT NULL,
|
|
30
|
+
status TEXT NOT NULL,
|
|
31
|
+
phase TEXT NOT NULL,
|
|
32
|
+
iteration INTEGER NOT NULL DEFAULT 0,
|
|
33
|
+
started_at TEXT NOT NULL,
|
|
34
|
+
finished_at TEXT,
|
|
35
|
+
summary TEXT,
|
|
36
|
+
error TEXT
|
|
37
|
+
);
|
|
38
|
+
CREATE INDEX IF NOT EXISTS idx_runs_project ON runs(project_id);
|
|
39
|
+
|
|
40
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
41
|
+
id TEXT NOT NULL,
|
|
42
|
+
project_id TEXT NOT NULL REFERENCES projects(project_id),
|
|
43
|
+
description TEXT NOT NULL,
|
|
44
|
+
priority TEXT NOT NULL DEFAULT 'medium',
|
|
45
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
46
|
+
depends_on TEXT NOT NULL DEFAULT '[]',
|
|
47
|
+
files TEXT NOT NULL DEFAULT '[]',
|
|
48
|
+
acceptance_criteria TEXT NOT NULL DEFAULT '[]',
|
|
49
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
50
|
+
created_at TEXT NOT NULL,
|
|
51
|
+
updated_at TEXT NOT NULL,
|
|
52
|
+
result TEXT,
|
|
53
|
+
PRIMARY KEY (id, project_id)
|
|
54
|
+
);
|
|
55
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_project ON tasks(project_id);
|
|
56
|
+
|
|
57
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
58
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
59
|
+
ts TEXT NOT NULL,
|
|
60
|
+
type TEXT NOT NULL,
|
|
61
|
+
run_id TEXT,
|
|
62
|
+
data TEXT NOT NULL
|
|
63
|
+
);
|
|
64
|
+
CREATE INDEX IF NOT EXISTS idx_events_type ON events(type);
|
|
65
|
+
|
|
66
|
+
CREATE TABLE IF NOT EXISTS errors (
|
|
67
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
68
|
+
project_id TEXT NOT NULL REFERENCES projects(project_id),
|
|
69
|
+
run_id TEXT,
|
|
70
|
+
ts TEXT NOT NULL,
|
|
71
|
+
source TEXT NOT NULL,
|
|
72
|
+
kind TEXT NOT NULL,
|
|
73
|
+
message TEXT NOT NULL,
|
|
74
|
+
detail TEXT
|
|
75
|
+
);
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
SCHEMA_V2 = """
|
|
79
|
+
CREATE TABLE IF NOT EXISTS model_usage (
|
|
80
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
81
|
+
ts TEXT NOT NULL,
|
|
82
|
+
run_id TEXT,
|
|
83
|
+
agent TEXT NOT NULL,
|
|
84
|
+
provider TEXT NOT NULL,
|
|
85
|
+
model TEXT NOT NULL,
|
|
86
|
+
input_tokens INTEGER,
|
|
87
|
+
output_tokens INTEGER,
|
|
88
|
+
total_tokens INTEGER,
|
|
89
|
+
latency_ms REAL,
|
|
90
|
+
attempts INTEGER NOT NULL DEFAULT 1,
|
|
91
|
+
success INTEGER NOT NULL DEFAULT 1,
|
|
92
|
+
error TEXT
|
|
93
|
+
);
|
|
94
|
+
CREATE INDEX IF NOT EXISTS idx_model_usage_run ON model_usage(run_id);
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
SCHEMA_V3 = """
|
|
98
|
+
CREATE TABLE IF NOT EXISTS checkpoints (
|
|
99
|
+
id TEXT PRIMARY KEY,
|
|
100
|
+
project_id TEXT NOT NULL,
|
|
101
|
+
label TEXT NOT NULL,
|
|
102
|
+
git_commit TEXT,
|
|
103
|
+
description TEXT,
|
|
104
|
+
created_at TEXT NOT NULL,
|
|
105
|
+
run_id TEXT
|
|
106
|
+
);
|
|
107
|
+
CREATE INDEX IF NOT EXISTS idx_checkpoints_project ON checkpoints(project_id);
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
MIGRATIONS: list[tuple[int, str]] = [
|
|
111
|
+
(1, SCHEMA_V1),
|
|
112
|
+
(2, SCHEMA_V2),
|
|
113
|
+
(3, SCHEMA_V3),
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def run_migrations(conn: sqlite3.Connection) -> int:
|
|
118
|
+
"""Run all unapplied migrations in order. Returns the current schema version."""
|
|
119
|
+
conn.execute("CREATE TABLE IF NOT EXISTS schema_version (version INTEGER PRIMARY KEY)")
|
|
120
|
+
row = conn.execute("SELECT version FROM schema_version ORDER BY version DESC LIMIT 1").fetchone()
|
|
121
|
+
current_version = row[0] if row else 0
|
|
122
|
+
|
|
123
|
+
for version, script in MIGRATIONS:
|
|
124
|
+
if version > current_version:
|
|
125
|
+
conn.executescript(script)
|
|
126
|
+
conn.execute("INSERT OR REPLACE INTO schema_version (version) VALUES (?)", (version,))
|
|
127
|
+
conn.commit()
|
|
128
|
+
current_version = version
|
|
129
|
+
|
|
130
|
+
return current_version
|