forge-dev 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.
- forge_core/__init__.py +3 -0
- forge_core/agents/__init__.py +1 -0
- forge_core/auditor.py +330 -0
- forge_core/cli.py +552 -0
- forge_core/detector.py +209 -0
- forge_core/editor_bridge.py +543 -0
- forge_core/models.py +332 -0
- forge_core/phases/__init__.py +1 -0
- forge_core/phases/coherence.py +293 -0
- forge_core/phases/context.py +264 -0
- forge_core/phases/intake.py +340 -0
- forge_core/registry.py +247 -0
- forge_core/standards/api-first-design.yaml +24 -0
- forge_core/standards/microservice-packaging.yaml +30 -0
- forge_core/standards/observability.yaml +31 -0
- forge_core/standards/security-baseline.yaml +43 -0
- forge_core/standards/type-safety.yaml +23 -0
- forge_core/templates/__init__.py +1 -0
- forge_core/utils/__init__.py +1 -0
- forge_dev-0.1.0.dist-info/METADATA +134 -0
- forge_dev-0.1.0.dist-info/RECORD +25 -0
- forge_dev-0.1.0.dist-info/WHEEL +4 -0
- forge_dev-0.1.0.dist-info/entry_points.txt +2 -0
- mcp_server/__init__.py +1 -0
- mcp_server/server.py +1086 -0
forge_core/cli.py
ADDED
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
"""Forge CLI — AI-Native Development Workflow Engine.
|
|
2
|
+
|
|
3
|
+
Global CLI commands for managing Forge projects.
|
|
4
|
+
Installed as `forge` command via pip.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import click
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
from rich.panel import Panel
|
|
16
|
+
from rich.table import Table
|
|
17
|
+
|
|
18
|
+
console = Console()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@click.group()
|
|
22
|
+
@click.version_option(package_name="forge-dev")
|
|
23
|
+
def main() -> None:
|
|
24
|
+
"""Forge — AI-Native Development Workflow Engine.
|
|
25
|
+
|
|
26
|
+
Transform requirements into production-ready, audited, observable code.
|
|
27
|
+
"""
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@main.command()
|
|
32
|
+
@click.option("--name", "-n", help="Project name (default: directory name)")
|
|
33
|
+
@click.option("--backend", "-b", help="Backend framework override")
|
|
34
|
+
@click.option("--no-interactive", is_flag=True, help="Skip interactive questions")
|
|
35
|
+
def init(name: str | None, backend: str | None, no_interactive: bool) -> None:
|
|
36
|
+
"""Initialize Forge in the current directory.
|
|
37
|
+
|
|
38
|
+
Detects the project state and adapts:
|
|
39
|
+
- Empty folder → guided setup conversation
|
|
40
|
+
- Has docs → analyzes documents, generates brief
|
|
41
|
+
- Has code → detects stack, offers assessment
|
|
42
|
+
- Has .forge/ → offers to continue or re-evaluate
|
|
43
|
+
"""
|
|
44
|
+
from forge_core.detector import detect_project
|
|
45
|
+
from forge_core.phases.context import (
|
|
46
|
+
get_questions_for_empty_project,
|
|
47
|
+
resolve_context,
|
|
48
|
+
save_context,
|
|
49
|
+
)
|
|
50
|
+
from forge_core.registry import ensure_registry, load_user_config
|
|
51
|
+
|
|
52
|
+
# Ensure global registry exists
|
|
53
|
+
ensure_registry()
|
|
54
|
+
user_config = load_user_config()
|
|
55
|
+
|
|
56
|
+
# Detect what's in this directory
|
|
57
|
+
project_path = Path.cwd()
|
|
58
|
+
detection = detect_project(project_path)
|
|
59
|
+
|
|
60
|
+
console.print()
|
|
61
|
+
console.print(Panel(
|
|
62
|
+
f"[bold]Forge Init[/bold]\n{detection.summary()}",
|
|
63
|
+
title="🔥 Forge",
|
|
64
|
+
border_style="bright_red",
|
|
65
|
+
))
|
|
66
|
+
console.print()
|
|
67
|
+
|
|
68
|
+
if detection.has_forge:
|
|
69
|
+
console.print("[yellow]This project already has Forge initialized.[/yellow]")
|
|
70
|
+
console.print("Options:")
|
|
71
|
+
console.print(" forge status — see current state")
|
|
72
|
+
console.print(" forge assess — re-evaluate against latest standards")
|
|
73
|
+
console.print(" forge audit — run audit agents")
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
if detection.is_empty:
|
|
77
|
+
console.print("[cyan]Empty directory detected. Let's set up your project.[/cyan]")
|
|
78
|
+
console.print()
|
|
79
|
+
|
|
80
|
+
if no_interactive:
|
|
81
|
+
# Use defaults for everything
|
|
82
|
+
overrides = {}
|
|
83
|
+
if name:
|
|
84
|
+
overrides["name"] = name
|
|
85
|
+
if backend:
|
|
86
|
+
overrides["backend"] = backend
|
|
87
|
+
context = resolve_context(detection, user_config, project_name=name, overrides=overrides)
|
|
88
|
+
else:
|
|
89
|
+
# Interactive setup
|
|
90
|
+
questions = get_questions_for_empty_project(user_config)
|
|
91
|
+
overrides = {}
|
|
92
|
+
|
|
93
|
+
for q in questions:
|
|
94
|
+
if q["type"] == "text":
|
|
95
|
+
answer = click.prompt(f" {q['question']}")
|
|
96
|
+
overrides[q["id"]] = answer
|
|
97
|
+
elif q["type"] == "choice":
|
|
98
|
+
console.print(f" {q['question']}")
|
|
99
|
+
for i, opt in enumerate(q["options"], 1):
|
|
100
|
+
default_marker = " [default]" if opt == q.get("default") else ""
|
|
101
|
+
console.print(f" {i}. {opt}{default_marker}")
|
|
102
|
+
idx = click.prompt(" Choice", default="1", type=int)
|
|
103
|
+
overrides[q["id"]] = q["options"][min(idx - 1, len(q["options"]) - 1)]
|
|
104
|
+
elif q["type"] == "boolean":
|
|
105
|
+
answer = click.confirm(f" {q['question']}", default=q.get("default", True))
|
|
106
|
+
overrides[q["id"]] = answer
|
|
107
|
+
elif q["type"] == "multi_choice":
|
|
108
|
+
console.print(f" {q['question']}")
|
|
109
|
+
for i, opt in enumerate(q["options"], 1):
|
|
110
|
+
console.print(f" {i}. {opt}")
|
|
111
|
+
selected = click.prompt(" Select (comma-separated numbers)", default="")
|
|
112
|
+
if selected:
|
|
113
|
+
indices = [int(x.strip()) - 1 for x in selected.split(",")]
|
|
114
|
+
overrides[q["id"]] = [q["options"][i] for i in indices if 0 <= i < len(q["options"])]
|
|
115
|
+
else:
|
|
116
|
+
overrides[q["id"]] = q.get("default", [])
|
|
117
|
+
|
|
118
|
+
# Map question IDs to context fields
|
|
119
|
+
context_overrides = {}
|
|
120
|
+
if "mission" in overrides:
|
|
121
|
+
context_overrides["description"] = overrides["mission"]
|
|
122
|
+
if "type" in overrides:
|
|
123
|
+
context_overrides["type"] = overrides["type"]
|
|
124
|
+
if "regulatory" in overrides:
|
|
125
|
+
context_overrides["regulatory"] = overrides["regulatory"]
|
|
126
|
+
if "backend" in overrides:
|
|
127
|
+
context_overrides["backend"] = overrides["backend"]
|
|
128
|
+
elif backend:
|
|
129
|
+
context_overrides["backend"] = backend
|
|
130
|
+
if "ai_enabled" in overrides:
|
|
131
|
+
context_overrides["ai"] = {"enabled": overrides["ai_enabled"]}
|
|
132
|
+
|
|
133
|
+
context = resolve_context(
|
|
134
|
+
detection, user_config,
|
|
135
|
+
project_name=name,
|
|
136
|
+
overrides=context_overrides,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
elif detection.has_existing_code:
|
|
140
|
+
console.print("[cyan]Existing code detected. Analyzing stack...[/cyan]")
|
|
141
|
+
context = resolve_context(detection, user_config, project_name=name)
|
|
142
|
+
console.print(f"[green]Detected: {detection.detected_stack}[/green]")
|
|
143
|
+
|
|
144
|
+
else: # has_docs
|
|
145
|
+
console.print("[cyan]Documents found. Analyzing...[/cyan]")
|
|
146
|
+
console.print(f" Found {len(detection.doc_files)} document(s):")
|
|
147
|
+
for doc in detection.doc_files[:10]:
|
|
148
|
+
console.print(f" 📄 {doc.name}")
|
|
149
|
+
console.print()
|
|
150
|
+
console.print("[dim]Run `forge intake` to process these documents into a Forge Brief.[/dim]")
|
|
151
|
+
context = resolve_context(detection, user_config, project_name=name)
|
|
152
|
+
|
|
153
|
+
# Save context
|
|
154
|
+
context_path = save_context(context, project_path)
|
|
155
|
+
|
|
156
|
+
# Create journal
|
|
157
|
+
journal_path = project_path / ".forge" / "journal.md"
|
|
158
|
+
if not journal_path.exists():
|
|
159
|
+
journal_path.write_text(
|
|
160
|
+
f"# {context.name} — Forge Journal\n\n"
|
|
161
|
+
"Record project-specific learnings, workarounds, and nuances here.\n"
|
|
162
|
+
"Forge agents read this file to understand project-specific context.\n\n"
|
|
163
|
+
"## Entries\n\n"
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# Record in global history
|
|
167
|
+
from forge_core.registry import record_project
|
|
168
|
+
record_project(
|
|
169
|
+
context.name,
|
|
170
|
+
str(project_path),
|
|
171
|
+
{"type": context.type, "backend": context.backend, "cloud": context.cloud},
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
console.print()
|
|
175
|
+
console.print(Panel(
|
|
176
|
+
f"[green]✓ Forge initialized for [bold]{context.name}[/bold][/green]\n\n"
|
|
177
|
+
f"Context: {context_path}\n"
|
|
178
|
+
f"Journal: {journal_path}\n\n"
|
|
179
|
+
f"Stack: {context.backend.value} + {context.frontend.value}\n"
|
|
180
|
+
f"Cloud: {context.cloud.value}\n"
|
|
181
|
+
f"Auth: {context.auth.value}\n\n"
|
|
182
|
+
"Next steps:\n"
|
|
183
|
+
" forge intake <file> — process a requirement document\n"
|
|
184
|
+
" forge plan — generate implementation plan\n"
|
|
185
|
+
" forge audit — run audit agents",
|
|
186
|
+
title="🔥 Ready",
|
|
187
|
+
border_style="green",
|
|
188
|
+
))
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@main.command()
|
|
192
|
+
@click.argument("file", required=False, type=click.Path(exists=True))
|
|
193
|
+
@click.option("--text", "-t", help="Inline requirement text")
|
|
194
|
+
def intake(file: str | None, text: str | None) -> None:
|
|
195
|
+
"""Process a requirement into a Forge Brief.
|
|
196
|
+
|
|
197
|
+
Accepts a file (markdown, text, PDF) or inline text.
|
|
198
|
+
Produces a normalized brief in .forge/brief.yaml.
|
|
199
|
+
"""
|
|
200
|
+
from forge_core.phases.intake import (
|
|
201
|
+
build_intake_prompt,
|
|
202
|
+
classify_requirement,
|
|
203
|
+
save_brief,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
project_path = Path.cwd()
|
|
207
|
+
forge_dir = project_path / ".forge"
|
|
208
|
+
|
|
209
|
+
if not forge_dir.exists():
|
|
210
|
+
console.print("[red]No Forge project found. Run `forge init` first.[/red]")
|
|
211
|
+
sys.exit(1)
|
|
212
|
+
|
|
213
|
+
# Get the requirement content
|
|
214
|
+
if file:
|
|
215
|
+
content = Path(file).read_text(errors="ignore")
|
|
216
|
+
console.print(f"[cyan]Reading requirement from: {file}[/cyan]")
|
|
217
|
+
elif text:
|
|
218
|
+
content = text
|
|
219
|
+
else:
|
|
220
|
+
console.print("[yellow]No file or text provided.[/yellow]")
|
|
221
|
+
console.print("Usage:")
|
|
222
|
+
console.print(" forge intake requirements.md")
|
|
223
|
+
console.print(" forge intake --text 'Build a user dashboard with...'")
|
|
224
|
+
return
|
|
225
|
+
|
|
226
|
+
# Classify and build prompt
|
|
227
|
+
req_type = classify_requirement(content)
|
|
228
|
+
console.print(f"[dim]Classified as: {req_type.value}[/dim]")
|
|
229
|
+
|
|
230
|
+
prompt = build_intake_prompt(content, req_type)
|
|
231
|
+
|
|
232
|
+
console.print()
|
|
233
|
+
console.print(Panel(
|
|
234
|
+
"[bold]Intake Analysis Prompt Generated[/bold]\n\n"
|
|
235
|
+
"The intake prompt has been generated for LLM analysis.\n"
|
|
236
|
+
"In a full Forge setup, this would be sent to Claude/GPT\n"
|
|
237
|
+
"to produce the brief automatically.\n\n"
|
|
238
|
+
f"Requirement type: {req_type.value}\n"
|
|
239
|
+
f"Content length: {len(content)} chars\n\n"
|
|
240
|
+
"For now, the prompt is saved to .forge/intake_prompt.md\n"
|
|
241
|
+
"You can send it to an LLM manually or via Claude Code.",
|
|
242
|
+
title="📋 Intake",
|
|
243
|
+
border_style="cyan",
|
|
244
|
+
))
|
|
245
|
+
|
|
246
|
+
# Save the prompt for LLM processing
|
|
247
|
+
prompt_path = forge_dir / "intake_prompt.md"
|
|
248
|
+
prompt_path.write_text(prompt)
|
|
249
|
+
console.print(f"\n[dim]Prompt saved to: {prompt_path}[/dim]")
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
@main.command()
|
|
253
|
+
def status() -> None:
|
|
254
|
+
"""Show current Forge project status."""
|
|
255
|
+
from forge_core.phases.context import load_context
|
|
256
|
+
from forge_core.phases.intake import load_brief
|
|
257
|
+
from forge_core.registry import get_forge_version
|
|
258
|
+
|
|
259
|
+
project_path = Path.cwd()
|
|
260
|
+
forge_dir = project_path / ".forge"
|
|
261
|
+
|
|
262
|
+
if not forge_dir.exists():
|
|
263
|
+
console.print("[red]No Forge project found. Run `forge init` first.[/red]")
|
|
264
|
+
return
|
|
265
|
+
|
|
266
|
+
context = load_context(project_path)
|
|
267
|
+
brief = load_brief(project_path)
|
|
268
|
+
version = get_forge_version()
|
|
269
|
+
|
|
270
|
+
table = Table(title=f"🔥 Forge Status — {context.name if context else 'Unknown'}")
|
|
271
|
+
table.add_column("Component", style="cyan")
|
|
272
|
+
table.add_column("Status", style="green")
|
|
273
|
+
|
|
274
|
+
table.add_row("Forge Version", version)
|
|
275
|
+
table.add_row("Context", "✓" if context else "✗ Run `forge init`")
|
|
276
|
+
table.add_row("Brief", "✓" if brief else "— Run `forge intake`")
|
|
277
|
+
table.add_row("Plan", "✓" if (forge_dir / "plan.yaml").exists() else "— Run `forge plan`")
|
|
278
|
+
table.add_row("Journal", "✓" if (forge_dir / "journal.md").exists() else "—")
|
|
279
|
+
|
|
280
|
+
if context:
|
|
281
|
+
table.add_row("", "")
|
|
282
|
+
table.add_row("Stack", f"{context.backend.value} + {context.frontend.value}")
|
|
283
|
+
table.add_row("Cloud", context.cloud.value)
|
|
284
|
+
table.add_row("Auth", context.auth.value)
|
|
285
|
+
table.add_row("AI", "Enabled" if context.ai.enabled else "Disabled")
|
|
286
|
+
table.add_row("Type", context.type.value)
|
|
287
|
+
if context.regulatory:
|
|
288
|
+
table.add_row("Regulatory", ", ".join(r.value for r in context.regulatory))
|
|
289
|
+
|
|
290
|
+
console.print()
|
|
291
|
+
console.print(table)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
@main.command()
|
|
295
|
+
@click.argument("entry", required=False)
|
|
296
|
+
def journal(entry: str | None) -> None:
|
|
297
|
+
"""Add an entry to the project journal.
|
|
298
|
+
|
|
299
|
+
Records learnings, workarounds, and nuances specific to this project.
|
|
300
|
+
Forge agents read the journal for project-specific context.
|
|
301
|
+
"""
|
|
302
|
+
from datetime import datetime, timezone
|
|
303
|
+
|
|
304
|
+
project_path = Path.cwd()
|
|
305
|
+
journal_path = project_path / ".forge" / "journal.md"
|
|
306
|
+
|
|
307
|
+
if not journal_path.exists():
|
|
308
|
+
console.print("[red]No Forge project found. Run `forge init` first.[/red]")
|
|
309
|
+
return
|
|
310
|
+
|
|
311
|
+
if entry is None:
|
|
312
|
+
entry = click.prompt("Journal entry")
|
|
313
|
+
|
|
314
|
+
timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M")
|
|
315
|
+
with open(journal_path, "a") as f:
|
|
316
|
+
f.write(f"\n### {timestamp}\n{entry}\n")
|
|
317
|
+
|
|
318
|
+
console.print(f"[green]✓ Journal entry added[/green]")
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
@main.command()
|
|
322
|
+
@click.option("--name", "-n", required=True, help="Standard name")
|
|
323
|
+
@click.option("--area", "-a", required=True, help="Area this standard governs")
|
|
324
|
+
@click.option("--description", "-d", required=True, help="What the standard requires")
|
|
325
|
+
def standards(name: str, area: str, description: str) -> None:
|
|
326
|
+
"""Add or view standards.
|
|
327
|
+
|
|
328
|
+
When adding a new standard, runs the coherence checker first.
|
|
329
|
+
"""
|
|
330
|
+
import yaml as _yaml
|
|
331
|
+
|
|
332
|
+
from forge_core.phases.coherence import check_new_standard
|
|
333
|
+
from forge_core.registry import load_standards, version_workflow
|
|
334
|
+
|
|
335
|
+
existing = load_standards()
|
|
336
|
+
|
|
337
|
+
new_standard = {
|
|
338
|
+
"name": name,
|
|
339
|
+
"area": area,
|
|
340
|
+
"description": description,
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
# Run coherence check
|
|
344
|
+
report = check_new_standard(new_standard, existing)
|
|
345
|
+
console.print()
|
|
346
|
+
console.print(report.summary())
|
|
347
|
+
|
|
348
|
+
if not report.passed:
|
|
349
|
+
console.print("\n[red]Cannot add standard — resolve errors first.[/red]")
|
|
350
|
+
return
|
|
351
|
+
|
|
352
|
+
if report.issues:
|
|
353
|
+
if not click.confirm("\nProceed despite warnings?"):
|
|
354
|
+
return
|
|
355
|
+
|
|
356
|
+
# Save the standard
|
|
357
|
+
from forge_core.registry import USER_DIR
|
|
358
|
+
standards_dir = USER_DIR / "standards"
|
|
359
|
+
standards_dir.mkdir(parents=True, exist_ok=True)
|
|
360
|
+
|
|
361
|
+
std_path = standards_dir / f"{name.lower().replace(' ', '-')}.yaml"
|
|
362
|
+
with open(std_path, "w") as f:
|
|
363
|
+
_yaml.dump(new_standard, f, default_flow_style=False)
|
|
364
|
+
|
|
365
|
+
# Version the workflow change
|
|
366
|
+
version_id = version_workflow(f"Added standard: {name}")
|
|
367
|
+
console.print(f"\n[green]✓ Standard '{name}' added[/green]")
|
|
368
|
+
console.print(f"[dim]Workflow versioned: {version_id}[/dim]")
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
@main.command()
|
|
372
|
+
def upgrade() -> None:
|
|
373
|
+
"""Upgrade Forge core from upstream.
|
|
374
|
+
|
|
375
|
+
Updates ~/.forge/core/ while preserving ~/.forge/user/.
|
|
376
|
+
Project .forge/ directories are never touched.
|
|
377
|
+
"""
|
|
378
|
+
console.print("[yellow]Forge upgrade would pull latest from GitHub.[/yellow]")
|
|
379
|
+
console.print("[dim]Not yet implemented — will use git pull + migration check.[/dim]")
|
|
380
|
+
console.print()
|
|
381
|
+
console.print("The upgrade process will:")
|
|
382
|
+
console.print(" 1. Pull latest core/ from GitHub")
|
|
383
|
+
console.print(" 2. Run migration check for breaking changes")
|
|
384
|
+
console.print(" 3. Run coherence check against your user/ customizations")
|
|
385
|
+
console.print(" 4. Show impact report before applying")
|
|
386
|
+
console.print(" 5. Never touch project .forge/ directories")
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
@main.command()
|
|
390
|
+
def assess() -> None:
|
|
391
|
+
"""Assess an existing project against current Forge standards.
|
|
392
|
+
|
|
393
|
+
Produces a maturity report with opportunities for improvement.
|
|
394
|
+
"""
|
|
395
|
+
from forge_core.detector import detect_project
|
|
396
|
+
from forge_core.registry import load_standards
|
|
397
|
+
|
|
398
|
+
project_path = Path.cwd()
|
|
399
|
+
detection = detect_project(project_path)
|
|
400
|
+
|
|
401
|
+
if detection.is_empty:
|
|
402
|
+
console.print("[red]Nothing to assess — directory is empty.[/red]")
|
|
403
|
+
return
|
|
404
|
+
|
|
405
|
+
standards_list = load_standards()
|
|
406
|
+
|
|
407
|
+
console.print(Panel(
|
|
408
|
+
f"[bold]Maturity Assessment[/bold]\n\n"
|
|
409
|
+
f"Project: {project_path.name}\n"
|
|
410
|
+
f"Code files: {len(detection.code_files)}\n"
|
|
411
|
+
f"Detected stack: {detection.detected_stack}\n"
|
|
412
|
+
f"Standards to check: {len(standards_list)}\n\n"
|
|
413
|
+
"[dim]Full assessment requires LLM analysis.\n"
|
|
414
|
+
"The assessment prompt will be generated for processing.[/dim]",
|
|
415
|
+
title="📊 Assess",
|
|
416
|
+
border_style="yellow",
|
|
417
|
+
))
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
@main.command()
|
|
421
|
+
def mcps() -> None:
|
|
422
|
+
"""View and manage the MCP registry."""
|
|
423
|
+
from forge_core.registry import load_mcps
|
|
424
|
+
|
|
425
|
+
mcp_list = load_mcps()
|
|
426
|
+
|
|
427
|
+
if not mcp_list:
|
|
428
|
+
console.print("[dim]No MCPs configured. Edit ~/.forge/user/mcps.yaml[/dim]")
|
|
429
|
+
return
|
|
430
|
+
|
|
431
|
+
table = Table(title="MCP Registry")
|
|
432
|
+
table.add_column("Name", style="cyan")
|
|
433
|
+
table.add_column("Description")
|
|
434
|
+
table.add_column("Auto-suggest", style="green")
|
|
435
|
+
table.add_column("Transport")
|
|
436
|
+
|
|
437
|
+
for mcp in mcp_list:
|
|
438
|
+
table.add_row(
|
|
439
|
+
mcp.name,
|
|
440
|
+
mcp.description,
|
|
441
|
+
"✓" if mcp.auto_suggest else "—",
|
|
442
|
+
mcp.transport,
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
console.print()
|
|
446
|
+
console.print(table)
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
@main.command()
|
|
450
|
+
@click.option(
|
|
451
|
+
"--format", "-f",
|
|
452
|
+
type=click.Choice(["claude", "cursor", "copilot", "generic", "all"]),
|
|
453
|
+
default="claude",
|
|
454
|
+
help="Editor format to generate",
|
|
455
|
+
)
|
|
456
|
+
def sync(format: str) -> None:
|
|
457
|
+
"""Generate editor instruction files from Forge governance.
|
|
458
|
+
|
|
459
|
+
Translates ALL Forge knowledge (standards, patterns, journal,
|
|
460
|
+
context) into a CLAUDE.md, .cursorrules, or equivalent file
|
|
461
|
+
that the AI editor reads and follows.
|
|
462
|
+
|
|
463
|
+
This is the core value of Forge — it bridges governance to the editor.
|
|
464
|
+
"""
|
|
465
|
+
from forge_core.editor_bridge import write_editor_file
|
|
466
|
+
from forge_core.phases.context import load_context
|
|
467
|
+
|
|
468
|
+
project_path = Path.cwd()
|
|
469
|
+
context = load_context(project_path)
|
|
470
|
+
|
|
471
|
+
if not context:
|
|
472
|
+
console.print("[red]No Forge project found. Run `forge init` first.[/red]")
|
|
473
|
+
return
|
|
474
|
+
|
|
475
|
+
if format == "all":
|
|
476
|
+
formats = ["claude", "cursor", "copilot", "generic"]
|
|
477
|
+
else:
|
|
478
|
+
formats = [format]
|
|
479
|
+
|
|
480
|
+
for fmt in formats:
|
|
481
|
+
path = write_editor_file(project_path, context, fmt)
|
|
482
|
+
console.print(f"[green]✓ Generated: {path}[/green]")
|
|
483
|
+
|
|
484
|
+
console.print()
|
|
485
|
+
console.print("[dim]The editor will read this file automatically.[/dim]")
|
|
486
|
+
console.print("[dim]Re-run `forge sync` after changing standards or context.[/dim]")
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
@main.command()
|
|
490
|
+
@click.argument("files", nargs=-1, type=click.Path(exists=True))
|
|
491
|
+
@click.option("--full", is_flag=True, help="Audit entire project")
|
|
492
|
+
def audit(files: tuple[str, ...], full: bool) -> None:
|
|
493
|
+
"""Run audit agents against code.
|
|
494
|
+
|
|
495
|
+
Generates an audit prompt with all standards, then produces
|
|
496
|
+
a structured report of findings with fixes.
|
|
497
|
+
|
|
498
|
+
Examples:
|
|
499
|
+
forge audit src/api/claims.py — audit specific file
|
|
500
|
+
forge audit --full — audit entire project
|
|
501
|
+
"""
|
|
502
|
+
from forge_core.auditor import build_audit_prompt, build_file_audit_prompt
|
|
503
|
+
from forge_core.phases.context import load_context
|
|
504
|
+
from forge_core.registry import load_standards
|
|
505
|
+
|
|
506
|
+
project_path = Path.cwd()
|
|
507
|
+
context = load_context(project_path)
|
|
508
|
+
|
|
509
|
+
if not context:
|
|
510
|
+
console.print("[red]No Forge project found. Run `forge init` first.[/red]")
|
|
511
|
+
return
|
|
512
|
+
|
|
513
|
+
standards = load_standards()
|
|
514
|
+
|
|
515
|
+
if files:
|
|
516
|
+
# Audit specific files
|
|
517
|
+
for file_path in files:
|
|
518
|
+
content = Path(file_path).read_text(errors="ignore")
|
|
519
|
+
prompt = build_file_audit_prompt(content, file_path, context, standards)
|
|
520
|
+
|
|
521
|
+
# Save prompt for LLM processing
|
|
522
|
+
prompt_path = project_path / ".forge" / "audit" / f"audit_prompt_{Path(file_path).stem}.md"
|
|
523
|
+
prompt_path.parent.mkdir(parents=True, exist_ok=True)
|
|
524
|
+
prompt_path.write_text(prompt)
|
|
525
|
+
|
|
526
|
+
console.print(f"[cyan]Audit prompt generated for: {file_path}[/cyan]")
|
|
527
|
+
console.print(f"[dim]Saved to: {prompt_path}[/dim]")
|
|
528
|
+
else:
|
|
529
|
+
# Full project audit
|
|
530
|
+
target_files = None
|
|
531
|
+
if not full:
|
|
532
|
+
console.print("[yellow]Specify files or use --full for entire project.[/yellow]")
|
|
533
|
+
console.print(" forge audit src/api/claims.py")
|
|
534
|
+
console.print(" forge audit --full")
|
|
535
|
+
return
|
|
536
|
+
|
|
537
|
+
prompt = build_audit_prompt(project_path, context, standards)
|
|
538
|
+
prompt_path = project_path / ".forge" / "audit" / "full_audit_prompt.md"
|
|
539
|
+
prompt_path.parent.mkdir(parents=True, exist_ok=True)
|
|
540
|
+
prompt_path.write_text(prompt)
|
|
541
|
+
|
|
542
|
+
console.print("[cyan]Full project audit prompt generated.[/cyan]")
|
|
543
|
+
console.print(f"[dim]Saved to: {prompt_path}[/dim]")
|
|
544
|
+
console.print()
|
|
545
|
+
console.print(
|
|
546
|
+
"[dim]Send this prompt to Claude Code or your AI editor "
|
|
547
|
+
"to perform the audit.[/dim]"
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
if __name__ == "__main__":
|
|
552
|
+
main()
|