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,283 @@
1
+ """Expert Coder agent (spec section 10).
2
+
3
+ A senior reviewer that improves the Builder's work: diff-first context,
4
+ minimal targeted changes, verification through tools.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ from collections.abc import Callable
11
+ from pathlib import Path
12
+
13
+ from forgex.agents.base import BaseAgent, prefetch_files
14
+ from forgex.config.schema import ForgexConfig
15
+ from forgex.models.router import ModelRouter
16
+ from forgex.orchestration.checkpoints import create_checkpoint, find_pre_task_checkpoint
17
+ from forgex.orchestration.events import EventRecorder
18
+ from forgex.orchestration.stores import ProjectStores
19
+ from forgex.research.citations import format_notes
20
+ from forgex.sandbox.permissions import permission_set_for
21
+ from forgex.schemas.agent import AgentRole, ExpertCoderResult
22
+ from forgex.schemas.error import ErrorRecord, ForgexError, ProjectError
23
+ from forgex.schemas.event import EventType
24
+ from forgex.schemas.plan import ArchitecturePlan
25
+ from forgex.schemas.research import ResearchNote
26
+ from forgex.schemas.task import Task
27
+ from forgex.tools import ToolExecutor, build_tool_registry, create_context
28
+ from forgex.tools.git import git_changed_files, git_diff, git_diff_range, is_dirty
29
+
30
+ logger = logging.getLogger("forgex.agents.expert_coder")
31
+
32
+ MAX_DIFF_CHARS = 20_000
33
+ MAX_PREFETCH_CHARS = 40_000
34
+ MAX_PREFETCH_FILES = 12
35
+
36
+
37
+ class ExpertCoderAgent(BaseAgent):
38
+ role = AgentRole.EXPERT_CODER
39
+ prompt_name = "expert_coder"
40
+
41
+ def __init__(
42
+ self,
43
+ *,
44
+ router: ModelRouter,
45
+ tools: ToolExecutor,
46
+ events: EventRecorder | None = None,
47
+ run_id: str | None = None,
48
+ ) -> None:
49
+ super().__init__(router=router, events=events, run_id=run_id)
50
+ self._tools = tools
51
+
52
+ async def review(
53
+ self,
54
+ *,
55
+ task: Task | None = None,
56
+ plan: ArchitecturePlan | None = None,
57
+ research: list[ResearchNote] | None = None,
58
+ diff: str = "",
59
+ changed_files: list[str] | None = None,
60
+ errors: list[ErrorRecord] | None = None,
61
+ extra_context: str = "",
62
+ ) -> ExpertCoderResult:
63
+ if self._events:
64
+ self._events.record(
65
+ EventType.AGENT_STARTED,
66
+ {"agent": self.role.value, "task": task.id if task else None},
67
+ run_id=self._run_id,
68
+ )
69
+
70
+ changed = list(changed_files or [])
71
+ seeds = list(changed)
72
+ try:
73
+ from forgex.context.manager import ContextManager
74
+
75
+ manager = ContextManager(self._tools.context.workspace)
76
+ for s in changed:
77
+ for imp in manager.importers_of(s, max_depth=1):
78
+ if imp not in seeds:
79
+ seeds.append(imp)
80
+ seeds = seeds[:MAX_PREFETCH_FILES]
81
+ except Exception:
82
+ logger.warning("impact analysis unavailable; changed files only", exc_info=True)
83
+
84
+ existing = prefetch_files(
85
+ self._tools.context.workspace,
86
+ seeds,
87
+ budget=MAX_PREFETCH_CHARS,
88
+ max_files=MAX_PREFETCH_FILES,
89
+ )
90
+
91
+ parts: list[str] = []
92
+ if task:
93
+ parts.append(
94
+ f"<task>\nID: {task.id}\nDescription: {task.description}\nCriteria: {', '.join(task.acceptance_criteria)}\n</task>"
95
+ )
96
+ if plan:
97
+ parts.append(f"<plan>\nGoal: {plan.goal}\nArchitecture: {plan.architecture}\n</plan>")
98
+ if research:
99
+ parts.append(f"<research>\n{format_notes(research)}\n</research>")
100
+ if errors:
101
+ err_text = "\n".join(f"- [{e.source}] {e.kind}: {e.message}" for e in errors[-5:])
102
+ parts.append(f"<errors>\n{err_text}\n</errors>")
103
+ if extra_context:
104
+ parts.append(extra_context)
105
+
106
+ diff_snippet = diff[:MAX_DIFF_CHARS] + ("\n... [diff trimmed]" if len(diff) > MAX_DIFF_CHARS else "")
107
+ parts.append(f"<diff>\n{diff_snippet or '(no diff)'}\n</diff>")
108
+ if existing:
109
+ parts.append(f"<existing_files>\n{existing}\n</existing_files>")
110
+
111
+ parts.append(
112
+ "Review the changes above. Refactor, optimize, fix, simplify, and verify with tests. If the code is already solid, change nothing."
113
+ )
114
+
115
+ instruction = "\n\n".join(parts)
116
+ loop_res = await self._tool_loop(instruction, self._tools, ExpertCoderResult)
117
+ result: ExpertCoderResult = loop_res.report
118
+
119
+ if self._events:
120
+ self._events.record(
121
+ EventType.AGENT_COMPLETED,
122
+ {
123
+ "agent": self.role.value,
124
+ "status": result.status,
125
+ "improvements": len(result.improvements),
126
+ "changed_files": result.changed_files,
127
+ },
128
+ run_id=self._run_id,
129
+ )
130
+ return result
131
+
132
+
133
+ def build_coder(
134
+ *,
135
+ config: ForgexConfig,
136
+ workspace: Path,
137
+ stores: ProjectStores,
138
+ router: ModelRouter | None = None,
139
+ approval: Callable[[str], bool] | None = None,
140
+ run_id: str | None = None,
141
+ ) -> ExpertCoderAgent:
142
+ router = router or ModelRouter(config, usage=stores.usage)
143
+ tool_ctx = create_context(
144
+ workspace=workspace,
145
+ config=config,
146
+ events=stores.recorder,
147
+ approval=approval,
148
+ run_id=run_id,
149
+ agent=AgentRole.EXPERT_CODER,
150
+ )
151
+ tools = ToolExecutor(
152
+ registry=build_tool_registry(),
153
+ permissions=permission_set_for(AgentRole.EXPERT_CODER),
154
+ context=tool_ctx,
155
+ )
156
+ return ExpertCoderAgent(
157
+ router=router,
158
+ tools=tools,
159
+ events=stores.recorder,
160
+ run_id=run_id,
161
+ )
162
+
163
+
164
+ def resolve_review_scope(
165
+ *,
166
+ stores: ProjectStores,
167
+ task_id: str | None,
168
+ base_commit: str | None,
169
+ ) -> tuple[Task | None, str, list[str]] | None:
170
+ task = None
171
+ if task_id is not None:
172
+ tasks = stores.tasks.load()
173
+ task = next((t for t in tasks if t.id == task_id), None)
174
+ if task is None:
175
+ raise ProjectError(f"Unknown task: {task_id}")
176
+
177
+ if base_commit is None and task_id is not None and not is_dirty(stores.root):
178
+ record = find_pre_task_checkpoint(task_id, stores=stores)
179
+ if record is not None and record.git_commit:
180
+ base_commit = record.git_commit
181
+
182
+ if base_commit:
183
+ diff = git_diff_range(stores.root, base_commit)
184
+ changed = git_changed_files(stores.root, base_commit)
185
+ else:
186
+ diff = git_diff(stores.root)
187
+ changed = git_changed_files(stores.root)
188
+
189
+ if not diff and not changed:
190
+ return None
191
+ return task, diff, changed
192
+
193
+
194
+ async def _single_review(
195
+ *,
196
+ stores: ProjectStores,
197
+ config: ForgexConfig,
198
+ task_id: str | None = None,
199
+ base_commit: str | None = None,
200
+ coder: ExpertCoderAgent | None = None,
201
+ approval: Callable[[str], bool] | None = None,
202
+ ) -> ExpertCoderResult | None:
203
+ state = stores.state.load()
204
+ if state is None:
205
+ raise ProjectError("Project state missing — run `forgex init` first.")
206
+
207
+ scope = resolve_review_scope(stores=stores, task_id=task_id, base_commit=base_commit)
208
+ if scope is None:
209
+ return None
210
+ task, diff, changed = scope
211
+
212
+ create_checkpoint(
213
+ f"pre-review{'-' + task_id if task_id else ''}",
214
+ stores=stores,
215
+ description="Before expert coder review",
216
+ )
217
+
218
+ agent = coder or build_coder(
219
+ config=config,
220
+ workspace=stores.root,
221
+ stores=stores,
222
+ approval=approval,
223
+ run_id=state.current_run_id,
224
+ )
225
+ owns_agent = coder is None
226
+ try:
227
+ result = await agent.review(
228
+ task=task,
229
+ plan=state.architecture_plan,
230
+ research=state.research,
231
+ diff=diff,
232
+ changed_files=changed,
233
+ errors=state.errors[-5:],
234
+ )
235
+ except ForgexError as exc:
236
+ state.errors.append(
237
+ ErrorRecord(
238
+ source="expert_coder",
239
+ kind=type(exc).__name__,
240
+ message=str(exc),
241
+ run_id=state.current_run_id,
242
+ )
243
+ )
244
+ stores.save_state(state)
245
+ raise
246
+ finally:
247
+ if owns_agent:
248
+ await agent.aclose()
249
+
250
+ if result.changed_files:
251
+ state.changed_files = sorted(set(state.changed_files) | set(result.changed_files))
252
+ stores.save_state(state)
253
+ return result
254
+
255
+
256
+ async def review_work(
257
+ *,
258
+ stores: ProjectStores,
259
+ config: ForgexConfig,
260
+ task_id: str | None = None,
261
+ base_commit: str | None = None,
262
+ coder: ExpertCoderAgent | None = None,
263
+ approval: Callable[[str], bool] | None = None,
264
+ ) -> ExpertCoderResult | None:
265
+ if config.ensemble.enabled:
266
+ from forgex.agents.ensemble import ensemble_review
267
+
268
+ return await ensemble_review(
269
+ stores=stores,
270
+ config=config,
271
+ task_id=task_id,
272
+ base_commit=base_commit,
273
+ applier=coder,
274
+ approval=approval,
275
+ )
276
+ return await _single_review(
277
+ stores=stores,
278
+ config=config,
279
+ task_id=task_id,
280
+ base_commit=base_commit,
281
+ coder=coder,
282
+ approval=approval,
283
+ )
@@ -0,0 +1,303 @@
1
+ """Supervisor agent (spec sections 11, 56, 58).
2
+
3
+ inspect → diagnose → instruct repair.
4
+ Verification is deterministic; this agent adds audit judgment;
5
+ quality gates enforce that red tests/builds never pass.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ from collections.abc import Callable
12
+ from pathlib import Path
13
+
14
+ from forgex.agents.base import BaseAgent
15
+ from forgex.config.schema import ForgexConfig
16
+ from forgex.models.router import ModelRouter
17
+ from forgex.orchestration.events import EventRecorder
18
+ from forgex.orchestration.stores import ProjectStores
19
+ from forgex.research.citations import format_notes
20
+ from forgex.sandbox.executor import make_executor
21
+ from forgex.sandbox.permissions import permission_set_for
22
+ from forgex.schemas.agent import (
23
+ AgentRole,
24
+ SupervisorJudgment,
25
+ SupervisorReport,
26
+ )
27
+ from forgex.schemas.error import ErrorRecord, ProjectError
28
+ from forgex.schemas.event import EventType
29
+ from forgex.schemas.plan import ArchitecturePlan
30
+ from forgex.schemas.research import ResearchNote
31
+ from forgex.schemas.result import VerificationOutcome
32
+ from forgex.schemas.task import Task
33
+ from forgex.tools import ToolExecutor, build_tool_registry, create_context
34
+ from forgex.tools.git import current_branch, git_log, has_git_repo, is_dirty
35
+ from forgex.tools.verification import run_verification
36
+
37
+ logger = logging.getLogger("forgex.agents.supervisor")
38
+
39
+
40
+ def _git_summary(root: Path) -> str:
41
+ if not has_git_repo(root):
42
+ return "not a git repository"
43
+ branch = current_branch(root) or "unknown"
44
+ dirty = "dirty" if is_dirty(root) else "clean"
45
+ log = git_log(root, limit=5)
46
+ return f"Branch: {branch} ({dirty})\nRecent commits:\n{log}"
47
+
48
+
49
+ def _format_verification(outcome: VerificationOutcome) -> str:
50
+ lines = []
51
+ if outcome.tests:
52
+ t = outcome.tests
53
+ p = t.passed if t.passed is not None else "?"
54
+ f = t.failed if t.failed is not None else "?"
55
+ lines.append(f"Tests: exit={t.exit_code}, passed={p}, failed={f}, timed_out={t.timed_out}")
56
+ if t.output_tail:
57
+ lines.append(f"--- test tail ---\n{t.output_tail[-1500:]}")
58
+ else:
59
+ lines.append("Tests: not detected / not run")
60
+
61
+ if outcome.build:
62
+ b = outcome.build
63
+ lines.append(f"Build: exit={b.exit_code}, timed_out={b.timed_out}")
64
+ if not b.ok and b.output_tail:
65
+ lines.append(f"--- build error ---\n{b.output_tail[-1000:]}")
66
+
67
+ if outcome.lint:
68
+ ln = outcome.lint
69
+ lines.append(f"Lint: exit={ln.exit_code}")
70
+ if not ln.ok and ln.output_tail:
71
+ lines.append(f"--- lint error ---\n{ln.output_tail[-1000:]}")
72
+
73
+ return "\n".join(lines)
74
+
75
+
76
+ def apply_quality_gates(
77
+ judgment: SupervisorJudgment, outcome: VerificationOutcome
78
+ ) -> SupervisorReport:
79
+ """Compose final report. Gates can only worsen a verdict, never improve it."""
80
+ status = judgment.status
81
+ errors = list(judgment.errors)
82
+ warnings = list(judgment.warnings)
83
+ req_changes = list(judgment.required_changes)
84
+
85
+ # 1. Hard gates
86
+ if outcome.tests is not None:
87
+ if (outcome.tests.exit_code is not None and outcome.tests.exit_code not in (0, 5)) or (
88
+ outcome.tests.failed and outcome.tests.failed > 0
89
+ ):
90
+ status = "fail"
91
+ msg = f"tests failed ({outcome.tests.failed or 0} failed, exit {outcome.tests.exit_code})"
92
+ if msg not in errors:
93
+ errors.append(msg)
94
+ if not req_changes:
95
+ req_changes.append("Fix the failing test suite")
96
+ if outcome.tests.timed_out:
97
+ status = "fail"
98
+ if "tests timed out" not in errors:
99
+ errors.append("tests timed out")
100
+
101
+ if outcome.build is not None and not outcome.build.ok:
102
+ status = "fail"
103
+ msg = f"build failed (exit {outcome.build.exit_code})"
104
+ if msg not in errors:
105
+ errors.append(msg)
106
+ if not req_changes:
107
+ req_changes.append("Fix build failures")
108
+
109
+ if outcome.lint is not None and not outcome.lint.ok:
110
+ status = "fail"
111
+ msg = f"lint failed (exit {outcome.lint.exit_code})"
112
+ if msg not in errors:
113
+ errors.append(msg)
114
+
115
+ # 2. Soft gates (floor at needs_review)
116
+ if outcome.tests is None:
117
+ warnings.append("no test suite detected")
118
+ if status == "pass":
119
+ status = "needs_review"
120
+ elif outcome.tests.exit_code == 5:
121
+ warnings.append("pytest collected 0 tests")
122
+ if status == "pass":
123
+ status = "needs_review"
124
+ elif outcome.tests.exit_code is None:
125
+ warnings.append("test command was not executed")
126
+ if not any("tests failed" in e for e in errors):
127
+ errors.append("tests failed: command denied")
128
+ status = "needs_review"
129
+
130
+ # 3. Model verdict preservation
131
+ if judgment.status in ("fail", "needs_review"):
132
+ status = judgment.status
133
+
134
+ return SupervisorReport(
135
+ status=status,
136
+ summary=judgment.summary,
137
+ errors=errors,
138
+ warnings=warnings,
139
+ required_changes=req_changes,
140
+ recommended_changes=judgment.recommended_changes,
141
+ tests=outcome.tests,
142
+ build=outcome.build,
143
+ lint=outcome.lint,
144
+ )
145
+
146
+
147
+ class SupervisorAgent(BaseAgent):
148
+ role = AgentRole.SUPERVISOR
149
+ prompt_name = "supervisor"
150
+
151
+ def __init__(
152
+ self,
153
+ *,
154
+ router: ModelRouter,
155
+ tools: ToolExecutor,
156
+ events: EventRecorder | None = None,
157
+ run_id: str | None = None,
158
+ ) -> None:
159
+ super().__init__(router=router, events=events, run_id=run_id)
160
+ self._tools = tools
161
+
162
+ async def audit(
163
+ self,
164
+ *,
165
+ request: str,
166
+ plan: ArchitecturePlan | None,
167
+ tasks: list[Task],
168
+ verification: VerificationOutcome,
169
+ research: list[ResearchNote] | None = None,
170
+ errors: list[ErrorRecord] | None = None,
171
+ ) -> SupervisorJudgment:
172
+ if self._events:
173
+ self._events.record(
174
+ EventType.AGENT_STARTED,
175
+ {"agent": self.role.value},
176
+ run_id=self._run_id,
177
+ )
178
+
179
+ v_text = _format_verification(verification)
180
+ git_text = _git_summary(self._tools.context.workspace)
181
+ research_text = format_notes(research or [])
182
+
183
+ tasks_summary = "\n".join(
184
+ f"- [{t.status.value}] {t.id}: {t.description} (attempts: {t.attempts})"
185
+ for t in tasks
186
+ )
187
+ err_text = "\n".join(f"- [{e.source}] {e.kind}: {e.message}" for e in (errors or [])[-5:])
188
+
189
+ instruction = (
190
+ f"<user_request>\n{request}\n</user_request>\n\n"
191
+ f"<plan>\nGoal: {plan.goal if plan else 'none'}\nCriteria: {', '.join(plan.acceptance_criteria) if plan else 'none'}\n</plan>\n\n"
192
+ f"<tasks>\n{tasks_summary}\n</tasks>\n\n"
193
+ f"<verification>\n{v_text}\n</verification>\n\n"
194
+ f"<git>\n{git_text}\n</git>\n\n"
195
+ f"<research>\n{research_text}\n</research>\n\n"
196
+ f"<errors>\n{err_text or '(no errors recorded)'}\n</errors>\n\n"
197
+ "Audit the repository. Use tools to inspect code if needed. Return your decisive verdict (pass/fail/needs_review)."
198
+ )
199
+
200
+ loop_res = await self._tool_loop(instruction, self._tools, SupervisorJudgment)
201
+ result: SupervisorJudgment = loop_res.report
202
+
203
+ if self._events:
204
+ self._events.record(
205
+ EventType.AGENT_COMPLETED,
206
+ {
207
+ "agent": self.role.value,
208
+ "status": result.status,
209
+ "errors": len(result.errors),
210
+ "required_changes": len(result.required_changes),
211
+ },
212
+ run_id=self._run_id,
213
+ )
214
+ return result
215
+
216
+
217
+ def build_supervisor(
218
+ *,
219
+ config: ForgexConfig,
220
+ workspace: Path,
221
+ stores: ProjectStores,
222
+ router: ModelRouter | None = None,
223
+ approval: Callable[[str], bool] | None = None,
224
+ run_id: str | None = None,
225
+ ) -> SupervisorAgent:
226
+ router = router or ModelRouter(config, usage=stores.usage)
227
+ tool_ctx = create_context(
228
+ workspace=workspace,
229
+ config=config,
230
+ events=stores.recorder,
231
+ approval=approval,
232
+ run_id=run_id,
233
+ agent=AgentRole.SUPERVISOR,
234
+ )
235
+ tools = ToolExecutor(
236
+ registry=build_tool_registry(),
237
+ permissions=permission_set_for(AgentRole.SUPERVISOR),
238
+ context=tool_ctx,
239
+ )
240
+ return SupervisorAgent(
241
+ router=router,
242
+ tools=tools,
243
+ events=stores.recorder,
244
+ run_id=run_id,
245
+ )
246
+
247
+
248
+ async def supervise(
249
+ *,
250
+ stores: ProjectStores,
251
+ config: ForgexConfig,
252
+ approval: Callable[[str], bool] | None = None,
253
+ include_build: bool = True,
254
+ include_lint: bool = True,
255
+ supervisor: SupervisorAgent | None = None,
256
+ ) -> SupervisorReport:
257
+ state = stores.state.load()
258
+ if state is None:
259
+ raise ProjectError("Project state missing — run `forgex init` first.")
260
+
261
+ agent = supervisor or build_supervisor(
262
+ config=config,
263
+ workspace=stores.root,
264
+ stores=stores,
265
+ approval=approval,
266
+ run_id=state.current_run_id,
267
+ )
268
+ owns_agent = supervisor is None
269
+
270
+ exec_inst = getattr(getattr(getattr(agent, "_tools", None), "context", None), "executor", None)
271
+ if exec_inst is None:
272
+ exec_inst = make_executor(stores.root, approval=approval)
273
+
274
+ outcome = await run_verification(
275
+ root=stores.root,
276
+ executor=exec_inst,
277
+ events=stores.recorder,
278
+ run_id=state.current_run_id,
279
+ include_build=include_build,
280
+ include_lint=include_lint,
281
+ )
282
+
283
+ try:
284
+ judgment = await agent.audit(
285
+ request=state.user_request or "",
286
+ plan=state.architecture_plan,
287
+ tasks=stores.tasks.load(),
288
+ verification=outcome,
289
+ research=state.research,
290
+ errors=state.errors,
291
+ )
292
+ finally:
293
+ if owns_agent:
294
+ await agent.aclose()
295
+
296
+ report = apply_quality_gates(judgment, outcome)
297
+
298
+ state.test_results = outcome.tests
299
+ state.build_results = outcome.build
300
+ state.last_supervisor_status = report.status
301
+ stores.save_state(state)
302
+
303
+ return report
forgex/cli/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """CLI package."""