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
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