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,116 @@
|
|
|
1
|
+
"""`forgex tools` — inspect and manually run tools (agent debugging surface)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
from typing import Annotated
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
|
|
12
|
+
from forgex.cli.ui import JsonFlag, Output, PlainFlag, make_approval, ui_from
|
|
13
|
+
from forgex.config import load_config
|
|
14
|
+
from forgex.orchestration.stores import open_stores
|
|
15
|
+
from forgex.sandbox.permissions import permission_set_for
|
|
16
|
+
from forgex.schemas.agent import AgentRole
|
|
17
|
+
from forgex.schemas.error import ConfigError, PermissionDeniedError, ProjectError
|
|
18
|
+
from forgex.tools import ToolCall, ToolExecutor, ToolResult, build_tool_registry, create_context
|
|
19
|
+
from forgex.utils.paths import find_project_root
|
|
20
|
+
|
|
21
|
+
app = typer.Typer(no_args_is_help=True, help="Inspect and manually run FORGEX tools.")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@app.command("list")
|
|
25
|
+
def list_command(
|
|
26
|
+
ctx: typer.Context,
|
|
27
|
+
json_out: JsonFlag = False,
|
|
28
|
+
plain: PlainFlag = False,
|
|
29
|
+
) -> None:
|
|
30
|
+
"""List all tools with their required permissions."""
|
|
31
|
+
ui = ui_from(ctx, json_out=json_out, plain=plain)
|
|
32
|
+
registry = build_tool_registry()
|
|
33
|
+
payload = [
|
|
34
|
+
{
|
|
35
|
+
"name": t.name,
|
|
36
|
+
"description": t.description,
|
|
37
|
+
"permissions": sorted(p.value for p in t.required_permissions),
|
|
38
|
+
"parameters": t.parameters_schema(),
|
|
39
|
+
}
|
|
40
|
+
for t in registry.tools()
|
|
41
|
+
]
|
|
42
|
+
ui.data({"tools": payload})
|
|
43
|
+
if ui.mode is Output.RICH:
|
|
44
|
+
table = Table(show_header=True, header_style="bold")
|
|
45
|
+
table.add_column("Tool")
|
|
46
|
+
table.add_column("Permissions")
|
|
47
|
+
table.add_column("Description", overflow="fold")
|
|
48
|
+
for item in payload:
|
|
49
|
+
perms_str = ", ".join(item["permissions"]) if isinstance(item["permissions"], list) else str(item["permissions"])
|
|
50
|
+
table.add_row(str(item["name"]), perms_str or "—", str(item["description"]))
|
|
51
|
+
ui.console.print(table)
|
|
52
|
+
elif ui.mode is Output.PLAIN:
|
|
53
|
+
for item in payload:
|
|
54
|
+
perms_str = ",".join(item["permissions"]) if isinstance(item["permissions"], list) else str(item["permissions"])
|
|
55
|
+
ui.console.print(f"{item['name']} [{perms_str}]: {item['description']}")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@app.command("run")
|
|
59
|
+
def run_command(
|
|
60
|
+
ctx: typer.Context,
|
|
61
|
+
name: Annotated[str, typer.Argument(help="Tool name, e.g. read_file")],
|
|
62
|
+
agent: Annotated[str, typer.Option("--agent", "-a", help="Act as this agent role.")] = "builder",
|
|
63
|
+
args_json: Annotated[str | None, typer.Option("--args", help="JSON object of tool arguments.")] = "{}",
|
|
64
|
+
json_out: JsonFlag = False,
|
|
65
|
+
plain: PlainFlag = False,
|
|
66
|
+
) -> None:
|
|
67
|
+
"""Run one tool as one agent."""
|
|
68
|
+
ui = ui_from(ctx, json_out=json_out, plain=plain)
|
|
69
|
+
root = find_project_root()
|
|
70
|
+
if root is None:
|
|
71
|
+
raise ProjectError("Not inside a FORGEX project — run `forgex init` first.")
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
role = AgentRole(agent)
|
|
75
|
+
except ValueError as exc:
|
|
76
|
+
valid = ", ".join(r.value for r in AgentRole)
|
|
77
|
+
raise ConfigError(f"Unknown agent role '{agent}'. Valid: {valid}") from exc
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
arguments = json.loads(args_json or "{}")
|
|
81
|
+
if not isinstance(arguments, dict):
|
|
82
|
+
raise TypeError("args must be a JSON object")
|
|
83
|
+
except Exception as exc:
|
|
84
|
+
raise ConfigError(f"--args must be a valid JSON object: {exc}") from exc
|
|
85
|
+
|
|
86
|
+
config = load_config(root)
|
|
87
|
+
registry = build_tool_registry()
|
|
88
|
+
with open_stores(root) as stores:
|
|
89
|
+
tool_ctx = create_context(
|
|
90
|
+
workspace=root,
|
|
91
|
+
config=config,
|
|
92
|
+
events=stores.recorder,
|
|
93
|
+
approval=make_approval(ui),
|
|
94
|
+
agent=role,
|
|
95
|
+
)
|
|
96
|
+
executor = ToolExecutor(
|
|
97
|
+
registry=registry,
|
|
98
|
+
permissions=permission_set_for(role),
|
|
99
|
+
context=tool_ctx,
|
|
100
|
+
)
|
|
101
|
+
try:
|
|
102
|
+
result = asyncio.run(executor.execute(ToolCall(name=name, arguments=arguments)))
|
|
103
|
+
except PermissionDeniedError as exc:
|
|
104
|
+
result = ToolResult(ok=False, denied=True, error=str(exc))
|
|
105
|
+
|
|
106
|
+
payload = {
|
|
107
|
+
"tool": name,
|
|
108
|
+
"agent": role.value,
|
|
109
|
+
"ok": result.ok,
|
|
110
|
+
"denied": result.denied,
|
|
111
|
+
"error": result.error,
|
|
112
|
+
"output": result.output,
|
|
113
|
+
"data": result.data,
|
|
114
|
+
}
|
|
115
|
+
ui.console.print(json.dumps(payload, indent=2, default=str), soft_wrap=True, highlight=False)
|
|
116
|
+
raise typer.Exit(code=0 if result.ok else 1)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""`forgex verify` — deterministic verification harness (tests/build/lint)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from typing import Annotated
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from forgex.cli.ui import JsonFlag, PlainFlag, make_approval, ui_from
|
|
11
|
+
from forgex.config import load_config
|
|
12
|
+
from forgex.orchestration.stores import open_stores
|
|
13
|
+
from forgex.schemas.agent import AgentRole
|
|
14
|
+
from forgex.schemas.error import ProjectError
|
|
15
|
+
from forgex.tools import create_context
|
|
16
|
+
from forgex.tools.verification import run_verification
|
|
17
|
+
from forgex.utils.paths import find_project_root
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def verify_command(
|
|
21
|
+
ctx: typer.Context,
|
|
22
|
+
no_build: Annotated[bool, typer.Option("--no-build", help="Skip the build step.")] = False,
|
|
23
|
+
no_lint: Annotated[bool, typer.Option("--no-lint", help="Skip the lint step.")] = False,
|
|
24
|
+
json_out: JsonFlag = False,
|
|
25
|
+
plain: PlainFlag = False,
|
|
26
|
+
) -> None:
|
|
27
|
+
"""Run tests, build, and lint — deterministic, no LLM."""
|
|
28
|
+
ui = ui_from(ctx, json_out=json_out, plain=plain)
|
|
29
|
+
root = find_project_root()
|
|
30
|
+
if root is None:
|
|
31
|
+
raise ProjectError("Not inside a FORGEX project — run `forgex init` first.")
|
|
32
|
+
|
|
33
|
+
config = load_config(root)
|
|
34
|
+
with open_stores(root) as stores:
|
|
35
|
+
tool_ctx = create_context(
|
|
36
|
+
workspace=root,
|
|
37
|
+
config=config,
|
|
38
|
+
events=stores.recorder,
|
|
39
|
+
approval=make_approval(ui),
|
|
40
|
+
agent=AgentRole.SUPERVISOR,
|
|
41
|
+
)
|
|
42
|
+
outcome = asyncio.run(
|
|
43
|
+
run_verification(
|
|
44
|
+
root=root,
|
|
45
|
+
executor=tool_ctx.executor,
|
|
46
|
+
events=stores.recorder,
|
|
47
|
+
include_build=not no_build,
|
|
48
|
+
include_lint=not no_lint,
|
|
49
|
+
)
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
ui.data(outcome.model_dump(mode="json"))
|
|
53
|
+
steps = [("Tests", outcome.tests), ("Build", outcome.build), ("Lint", outcome.lint)]
|
|
54
|
+
for name, step in steps:
|
|
55
|
+
if step is None:
|
|
56
|
+
ui.text(f"{name:<6} — not detected / not configured")
|
|
57
|
+
elif step.ok:
|
|
58
|
+
ui.ok(f"{name:<6} {' '.join(step.command)}")
|
|
59
|
+
else:
|
|
60
|
+
ui.error(f"{name:<6} FAILED (exit {step.exit_code}{', timed out' if step.timed_out else ''})")
|
|
61
|
+
if step.output_tail:
|
|
62
|
+
ui.text(step.output_tail[-1500:])
|
|
63
|
+
|
|
64
|
+
ran = [s for _, s in steps if s is not None]
|
|
65
|
+
raise typer.Exit(code=0 if all(s.ok for s in ran) else 1)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Conversational memory and follow-up context tracker for FORGEX Universal CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
|
|
8
|
+
FOLLOWUP_PATTERNS = [
|
|
9
|
+
r"\b(?:it|that|those|them|these)\b",
|
|
10
|
+
r"\b(?:now\s+add|also\s+add|and\s+also|next\s+add)\b",
|
|
11
|
+
r"\b(?:fix\s+the\s+test|fix\s+tests?|make\s+it\s+pass)\b",
|
|
12
|
+
r"\b(?:refactor\s+it|update\s+it|change\s+it)\b",
|
|
13
|
+
r"\b(?:same\s+thing|previous\s+task)\b",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class ConversationContext:
|
|
19
|
+
"""Maintains recent task context across interactive prompts."""
|
|
20
|
+
|
|
21
|
+
history: list[str] = field(default_factory=list)
|
|
22
|
+
last_request: str | None = None
|
|
23
|
+
last_changed_files: list[str] = field(default_factory=list)
|
|
24
|
+
last_status: str | None = None
|
|
25
|
+
last_checkpoint_id: str | None = None
|
|
26
|
+
|
|
27
|
+
def record_run(
|
|
28
|
+
self,
|
|
29
|
+
request: str,
|
|
30
|
+
status: str,
|
|
31
|
+
changed_files: list[str],
|
|
32
|
+
checkpoint_id: str | None = None,
|
|
33
|
+
) -> None:
|
|
34
|
+
"""Update context memory with latest execution results."""
|
|
35
|
+
self.history.append(request)
|
|
36
|
+
self.last_request = request
|
|
37
|
+
self.last_status = status
|
|
38
|
+
self.last_changed_files = list(changed_files)
|
|
39
|
+
self.last_checkpoint_id = checkpoint_id
|
|
40
|
+
|
|
41
|
+
def is_followup(self, text: str) -> bool:
|
|
42
|
+
"""Detect if the new prompt refers back to the previous conversation context."""
|
|
43
|
+
if not self.last_request:
|
|
44
|
+
return False
|
|
45
|
+
clean = text.strip().lower()
|
|
46
|
+
return any(re.search(pat, clean) for pat in FOLLOWUP_PATTERNS)
|
|
47
|
+
|
|
48
|
+
def enrich_request(self, request: str) -> str:
|
|
49
|
+
"""If request is a follow-up, prepend context from previous runs."""
|
|
50
|
+
clean = request.strip()
|
|
51
|
+
if not self.is_followup(clean):
|
|
52
|
+
return clean
|
|
53
|
+
|
|
54
|
+
context_parts = []
|
|
55
|
+
if self.last_request:
|
|
56
|
+
context_parts.append(f"Previous task: {self.last_request}")
|
|
57
|
+
if self.last_changed_files:
|
|
58
|
+
file_sample = ", ".join(self.last_changed_files[:5])
|
|
59
|
+
context_parts.append(f"Modified files: {file_sample}")
|
|
60
|
+
|
|
61
|
+
context_str = " | ".join(context_parts)
|
|
62
|
+
return f"[Context: {context_str}] {clean}"
|
forgex/cli/intent.py
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""Natural language intent classification for FORGEX REPL."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from enum import Enum
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class IntentAction(str, Enum):
|
|
11
|
+
PLAN = "plan"
|
|
12
|
+
PLAN_AND_BUILD = "plan_and_build"
|
|
13
|
+
AUTONOMOUS = "autonomous"
|
|
14
|
+
BUILD = "build"
|
|
15
|
+
FIX = "fix"
|
|
16
|
+
REVIEW = "review"
|
|
17
|
+
VERIFY = "verify"
|
|
18
|
+
SUPERVISE = "supervise"
|
|
19
|
+
STATUS = "status"
|
|
20
|
+
DOCTOR = "doctor"
|
|
21
|
+
MODELS = "models"
|
|
22
|
+
DIFF = "diff"
|
|
23
|
+
CHECKPOINT = "checkpoint"
|
|
24
|
+
ROLLBACK = "rollback"
|
|
25
|
+
INIT = "init"
|
|
26
|
+
HELP = "help"
|
|
27
|
+
CHAT = "chat"
|
|
28
|
+
UNKNOWN = "unknown"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class IntentResult:
|
|
33
|
+
action: IntentAction
|
|
34
|
+
confidence: float
|
|
35
|
+
command_args: list[str]
|
|
36
|
+
description: str
|
|
37
|
+
pipeline: list[list[str]] = field(default_factory=list)
|
|
38
|
+
raw_request: str = ""
|
|
39
|
+
|
|
40
|
+
def __post_init__(self) -> None:
|
|
41
|
+
if not self.pipeline and self.command_args:
|
|
42
|
+
self.pipeline = [self.command_args]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
_INTENT_PATTERNS = [
|
|
46
|
+
# Plan and build patterns (e.g. "build me a REST API", "plan and build a service")
|
|
47
|
+
(
|
|
48
|
+
r"^(?:please\s+)?(?:build\s+me\s+|plan\s+and\s+build\s+)(.+)$",
|
|
49
|
+
IntentAction.PLAN_AND_BUILD,
|
|
50
|
+
lambda m: [["plan", m.group(1).strip()], ["build"]],
|
|
51
|
+
"Plan architecture and execute build pipeline",
|
|
52
|
+
),
|
|
53
|
+
# Dedicated plan patterns
|
|
54
|
+
(
|
|
55
|
+
r"^(?:please\s+)?(?:plan|design|architect|spec|outline)\s+(.+)$",
|
|
56
|
+
IntentAction.PLAN,
|
|
57
|
+
lambda m: [["plan", m.group(1).strip()]],
|
|
58
|
+
"Create an architectural plan",
|
|
59
|
+
),
|
|
60
|
+
(
|
|
61
|
+
r"^(?:create|implement|add)\s+(?:a|an|the)?\s*(.+)$",
|
|
62
|
+
IntentAction.PLAN,
|
|
63
|
+
lambda m: [["plan", m.group(1).strip()]],
|
|
64
|
+
"Plan and prepare implementation",
|
|
65
|
+
),
|
|
66
|
+
# Build patterns
|
|
67
|
+
(
|
|
68
|
+
r"^(?:please\s+)?(?:build|execute\s+plan|run\s+build|start\s+build)$",
|
|
69
|
+
IntentAction.BUILD,
|
|
70
|
+
lambda m: [["build"]],
|
|
71
|
+
"Execute full build pipeline",
|
|
72
|
+
),
|
|
73
|
+
# Fix patterns
|
|
74
|
+
(
|
|
75
|
+
r"^(?:please\s+)?(?:fix|repair|auto-?fix|heal|resolve\s+errors?|debug)(?:\s+(.*))?$",
|
|
76
|
+
IntentAction.FIX,
|
|
77
|
+
lambda m: [["fix"]],
|
|
78
|
+
"Verify, supervise, and auto-repair until green",
|
|
79
|
+
),
|
|
80
|
+
# Review patterns
|
|
81
|
+
(
|
|
82
|
+
r"^(?:please\s+)?(?:review|audit|check\s+code|inspect\s+changes?)(?:\s+(.*))?$",
|
|
83
|
+
IntentAction.REVIEW,
|
|
84
|
+
lambda m: [["review"]],
|
|
85
|
+
"Run expert code review",
|
|
86
|
+
),
|
|
87
|
+
# Verify patterns
|
|
88
|
+
(
|
|
89
|
+
r"^(?:please\s+)?(?:verify|test|run\s+tests?|check\s+tests?|lint)(?:\s+(.*))?$",
|
|
90
|
+
IntentAction.VERIFY,
|
|
91
|
+
lambda m: [["verify"]],
|
|
92
|
+
"Run deterministic verification (tests/lint/typecheck)",
|
|
93
|
+
),
|
|
94
|
+
# Supervise patterns
|
|
95
|
+
(
|
|
96
|
+
r"^(?:please\s+)?(?:supervise|audit\s+and\s+verify|full\s+audit)(?:\s+(.*))?$",
|
|
97
|
+
IntentAction.SUPERVISE,
|
|
98
|
+
lambda m: [["supervise"]],
|
|
99
|
+
"Run full supervision and verification",
|
|
100
|
+
),
|
|
101
|
+
# Status patterns
|
|
102
|
+
(
|
|
103
|
+
r"^(?:how\s+is\s+the\s+project|show\s+status|status\s+check|project\s+info|where\s+are\s+we)\b.*$",
|
|
104
|
+
IntentAction.STATUS,
|
|
105
|
+
lambda m: [["status"]],
|
|
106
|
+
"Show project status and metrics",
|
|
107
|
+
),
|
|
108
|
+
# Doctor patterns
|
|
109
|
+
(
|
|
110
|
+
r"^(?:check\s+environment|check\s+system|doctor|health\s+check|diagnose)\b.*$",
|
|
111
|
+
IntentAction.DOCTOR,
|
|
112
|
+
lambda m: [["doctor"]],
|
|
113
|
+
"Run environment diagnostic checks",
|
|
114
|
+
),
|
|
115
|
+
# Diff patterns
|
|
116
|
+
(
|
|
117
|
+
r"^(?:show\s+diff|git\s+diff|what\s+changed|changes)\b.*$",
|
|
118
|
+
IntentAction.DIFF,
|
|
119
|
+
lambda m: [["diff"]],
|
|
120
|
+
"Show current git diff",
|
|
121
|
+
),
|
|
122
|
+
# Checkpoint patterns
|
|
123
|
+
(
|
|
124
|
+
r"^(?:create\s+checkpoint|checkpoint|save\s+state|snapshot)(?:\s+(.+))?$",
|
|
125
|
+
IntentAction.CHECKPOINT,
|
|
126
|
+
lambda m: [["checkpoint", m.group(1).strip()]] if m.group(1) else [["checkpoint"]],
|
|
127
|
+
"Create git checkpoint",
|
|
128
|
+
),
|
|
129
|
+
# Rollback patterns
|
|
130
|
+
(
|
|
131
|
+
r"^(?:rollback|undo|revert\s+changes?)\b.*$",
|
|
132
|
+
IntentAction.ROLLBACK,
|
|
133
|
+
lambda m: [["rollback"]],
|
|
134
|
+
"Reversible rollback to previous checkpoint",
|
|
135
|
+
),
|
|
136
|
+
# Init patterns
|
|
137
|
+
(
|
|
138
|
+
r"^(?:init|initialize\s+project|create\s+project)\b.*$",
|
|
139
|
+
IntentAction.INIT,
|
|
140
|
+
lambda m: [["init"]],
|
|
141
|
+
"Initialize FORGEX in project directory",
|
|
142
|
+
),
|
|
143
|
+
# Models patterns
|
|
144
|
+
(
|
|
145
|
+
r"^(?:show\s+models|list\s+models|models?\s+catalog|which\s+models)\b.*$",
|
|
146
|
+
IntentAction.MODELS,
|
|
147
|
+
lambda m: [["models", "catalog"]],
|
|
148
|
+
"List model bindings and catalog",
|
|
149
|
+
),
|
|
150
|
+
# Help patterns
|
|
151
|
+
(
|
|
152
|
+
r"^(?:what\s+can\s+you\s+do|commands|help\s+me)\b.*$",
|
|
153
|
+
IntentAction.HELP,
|
|
154
|
+
lambda m: [["help"]],
|
|
155
|
+
"Show available commands and usage",
|
|
156
|
+
),
|
|
157
|
+
]
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def classify_intent(text: str) -> IntentResult:
|
|
161
|
+
"""Classify free-form natural language input into an action and CLI command pipeline."""
|
|
162
|
+
clean = text.strip()
|
|
163
|
+
if not clean:
|
|
164
|
+
return IntentResult(
|
|
165
|
+
action=IntentAction.UNKNOWN,
|
|
166
|
+
confidence=0.0,
|
|
167
|
+
command_args=[],
|
|
168
|
+
description="Empty input",
|
|
169
|
+
raw_request=clean,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
for pattern, action, pipeline_builder, desc in _INTENT_PATTERNS:
|
|
173
|
+
match = re.search(pattern, clean, re.IGNORECASE)
|
|
174
|
+
if match:
|
|
175
|
+
pipeline = pipeline_builder(match)
|
|
176
|
+
command_args = pipeline[0] if pipeline else []
|
|
177
|
+
raw_req = (
|
|
178
|
+
match.group(1).strip()
|
|
179
|
+
if match.groups() and match.group(1) is not None
|
|
180
|
+
else clean
|
|
181
|
+
)
|
|
182
|
+
return IntentResult(
|
|
183
|
+
action=action,
|
|
184
|
+
confidence=0.9,
|
|
185
|
+
command_args=command_args,
|
|
186
|
+
description=desc,
|
|
187
|
+
pipeline=pipeline,
|
|
188
|
+
raw_request=raw_req,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# Fallback: if text looks like a task or requirement, route to Architect planning
|
|
192
|
+
if len(clean.split()) >= 3:
|
|
193
|
+
args = ["plan", clean]
|
|
194
|
+
return IntentResult(
|
|
195
|
+
action=IntentAction.PLAN,
|
|
196
|
+
confidence=0.6,
|
|
197
|
+
command_args=args,
|
|
198
|
+
description=f"Plan architectural approach for: '{clean}'",
|
|
199
|
+
pipeline=[args],
|
|
200
|
+
raw_request=clean,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
return IntentResult(
|
|
204
|
+
action=IntentAction.UNKNOWN,
|
|
205
|
+
confidence=0.0,
|
|
206
|
+
command_args=[],
|
|
207
|
+
description="Could not determine intent",
|
|
208
|
+
raw_request=clean,
|
|
209
|
+
)
|
forgex/cli/progress.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Rich terminal progress and summary rendering for FORGEX Universal CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.panel import Panel
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def render_pipeline_header(request: str, console: Console | None = None) -> None:
|
|
13
|
+
"""Print an eye-catching start header for an autonomous build run."""
|
|
14
|
+
c = console or Console()
|
|
15
|
+
c.print(
|
|
16
|
+
Panel(
|
|
17
|
+
f"[bold cyan]Task:[/bold cyan] {request}\n"
|
|
18
|
+
"[dim]Autonomous Pipeline: Auto-Init ➔ Plan ➔ Build ➔ Verify ➔ Repair ➔ Checkpoint[/dim]",
|
|
19
|
+
title="[bold green]FORGEX Autonomous Execution[/bold green]",
|
|
20
|
+
border_style="cyan",
|
|
21
|
+
)
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def render_runtime_summary(
|
|
26
|
+
result: Any,
|
|
27
|
+
console: Console | None = None,
|
|
28
|
+
) -> None:
|
|
29
|
+
"""Render a comprehensive result panel after autonomous pipeline completion."""
|
|
30
|
+
c = console or Console()
|
|
31
|
+
|
|
32
|
+
grid = Table.grid(padding=(0, 2))
|
|
33
|
+
grid.add_column(style="dim", justify="right")
|
|
34
|
+
grid.add_column()
|
|
35
|
+
|
|
36
|
+
# Status
|
|
37
|
+
if result.status == "completed":
|
|
38
|
+
status_text = "[bold green]✔ SUCCESS[/bold green] (Verified & Approved)"
|
|
39
|
+
border = "green"
|
|
40
|
+
else:
|
|
41
|
+
status_text = f"[bold red]✖ FAILED[/bold red] ({result.reason or 'Unresolved errors'})"
|
|
42
|
+
border = "red"
|
|
43
|
+
|
|
44
|
+
grid.add_row("Outcome:", status_text)
|
|
45
|
+
grid.add_row("Task Request:", f"[bold white]{result.request}[/bold white]")
|
|
46
|
+
|
|
47
|
+
# Tasks
|
|
48
|
+
tasks_text = f"{result.tasks_completed}/{result.tasks_total} completed"
|
|
49
|
+
if result.tasks_completed == result.tasks_total and result.tasks_total > 0:
|
|
50
|
+
tasks_text = f"[green]{tasks_text}[/green]"
|
|
51
|
+
grid.add_row("Tasks Executed:", tasks_text)
|
|
52
|
+
|
|
53
|
+
# Changed files
|
|
54
|
+
if result.changed_files:
|
|
55
|
+
files_text = f"{len(result.changed_files)} file(s) modified/created"
|
|
56
|
+
grid.add_row("Code Changes:", files_text)
|
|
57
|
+
|
|
58
|
+
# Verification / Tests
|
|
59
|
+
if result.tests:
|
|
60
|
+
t = result.tests
|
|
61
|
+
test_summary = f"[green]{t.passed} passed[/green]"
|
|
62
|
+
if t.failed > 0:
|
|
63
|
+
test_summary += f", [red]{t.failed} failed[/red]"
|
|
64
|
+
if t.skipped > 0:
|
|
65
|
+
test_summary += f", [yellow]{t.skipped} skipped[/yellow]"
|
|
66
|
+
grid.add_row("Deterministic Tests:", test_summary)
|
|
67
|
+
|
|
68
|
+
# Build status
|
|
69
|
+
if result.build_ok is not None:
|
|
70
|
+
build_str = "[green]Passing[/green]" if result.build_ok else "[red]Failing[/red]"
|
|
71
|
+
grid.add_row("Build/Typecheck:", build_str)
|
|
72
|
+
|
|
73
|
+
# Checkpoint
|
|
74
|
+
if result.checkpoint_id:
|
|
75
|
+
grid.add_row("Git Checkpoint:", f"[cyan]{result.checkpoint_id[:8]}[/cyan] [dim](reversible snapshot)[/dim]")
|
|
76
|
+
|
|
77
|
+
# Duration
|
|
78
|
+
if getattr(result, "duration_seconds", 0) > 0:
|
|
79
|
+
grid.add_row("Pipeline Duration:", f"{result.duration_seconds:.1f}s")
|
|
80
|
+
|
|
81
|
+
# Token & Cost Usage
|
|
82
|
+
if getattr(result, "usage", None):
|
|
83
|
+
u = result.usage
|
|
84
|
+
prompt_t = u.get("prompt_tokens", 0)
|
|
85
|
+
compl_t = u.get("completion_tokens", 0)
|
|
86
|
+
cost = u.get("cost_usd", 0.0)
|
|
87
|
+
grid.add_row(
|
|
88
|
+
"Model Usage:",
|
|
89
|
+
f"{prompt_t:,} prompt + {compl_t:,} completion tokens [dim](${cost:.4f})[/dim]",
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
title = "[bold green]Autonomous Run Complete[/bold green]" if result.is_success else "[bold red]Autonomous Run Incomplete[/bold red]"
|
|
93
|
+
c.print(Panel(grid, title=title, border_style=border))
|
|
94
|
+
|
|
95
|
+
# Show list of files if any
|
|
96
|
+
if result.changed_files:
|
|
97
|
+
c.print("[dim]Modified files:[/dim]")
|
|
98
|
+
for f in result.changed_files[:10]:
|
|
99
|
+
c.print(f" [dim]•[/dim] [cyan]{f}[/cyan]")
|
|
100
|
+
if len(result.changed_files) > 10:
|
|
101
|
+
c.print(f" [dim]... and {len(result.changed_files) - 10} more files[/dim]")
|