forge-dev 0.1.2__tar.gz → 0.1.4__tar.gz
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_dev-0.1.2 → forge_dev-0.1.4}/.github/workflows/release-please.yml +1 -0
- {forge_dev-0.1.2 → forge_dev-0.1.4}/.gitignore +1 -0
- forge_dev-0.1.4/.release-please-manifest.json +3 -0
- {forge_dev-0.1.2 → forge_dev-0.1.4}/CHANGELOG.md +14 -0
- {forge_dev-0.1.2 → forge_dev-0.1.4}/PKG-INFO +48 -10
- {forge_dev-0.1.2 → forge_dev-0.1.4}/README.md +47 -9
- {forge_dev-0.1.2 → forge_dev-0.1.4}/forge_core/cli.py +227 -9
- {forge_dev-0.1.2 → forge_dev-0.1.4}/forge_core/registry.py +21 -9
- {forge_dev-0.1.2 → forge_dev-0.1.4}/pyproject.toml +1 -1
- forge_dev-0.1.2/.release-please-manifest.json +0 -3
- forge_dev-0.1.2/CLAUDE.md +0 -81
- {forge_dev-0.1.2 → forge_dev-0.1.4}/.github/workflows/publish.yml +0 -0
- {forge_dev-0.1.2 → forge_dev-0.1.4}/.release-please-config.json +0 -0
- {forge_dev-0.1.2 → forge_dev-0.1.4}/claude_skill/SKILL.md +0 -0
- {forge_dev-0.1.2 → forge_dev-0.1.4}/forge_core/__init__.py +0 -0
- {forge_dev-0.1.2 → forge_dev-0.1.4}/forge_core/agents/__init__.py +0 -0
- {forge_dev-0.1.2 → forge_dev-0.1.4}/forge_core/auditor.py +0 -0
- {forge_dev-0.1.2 → forge_dev-0.1.4}/forge_core/detector.py +0 -0
- {forge_dev-0.1.2 → forge_dev-0.1.4}/forge_core/editor_bridge.py +0 -0
- {forge_dev-0.1.2 → forge_dev-0.1.4}/forge_core/models.py +0 -0
- {forge_dev-0.1.2 → forge_dev-0.1.4}/forge_core/phases/__init__.py +0 -0
- {forge_dev-0.1.2 → forge_dev-0.1.4}/forge_core/phases/coherence.py +0 -0
- {forge_dev-0.1.2 → forge_dev-0.1.4}/forge_core/phases/context.py +0 -0
- {forge_dev-0.1.2 → forge_dev-0.1.4}/forge_core/phases/intake.py +0 -0
- {forge_dev-0.1.2 → forge_dev-0.1.4}/forge_core/standards/api-first-design.yaml +0 -0
- {forge_dev-0.1.2 → forge_dev-0.1.4}/forge_core/standards/microservice-packaging.yaml +0 -0
- {forge_dev-0.1.2 → forge_dev-0.1.4}/forge_core/standards/observability.yaml +0 -0
- {forge_dev-0.1.2 → forge_dev-0.1.4}/forge_core/standards/security-baseline.yaml +0 -0
- {forge_dev-0.1.2 → forge_dev-0.1.4}/forge_core/standards/type-safety.yaml +0 -0
- {forge_dev-0.1.2 → forge_dev-0.1.4}/forge_core/templates/__init__.py +0 -0
- {forge_dev-0.1.2 → forge_dev-0.1.4}/forge_core/utils/__init__.py +0 -0
- {forge_dev-0.1.2 → forge_dev-0.1.4}/mcp_server/__init__.py +0 -0
- {forge_dev-0.1.2 → forge_dev-0.1.4}/mcp_server/server.py +0 -0
- {forge_dev-0.1.2 → forge_dev-0.1.4}/references/implementation-phases.md +0 -0
- {forge_dev-0.1.2 → forge_dev-0.1.4}/setup.sh +0 -0
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.1.4](https://github.com/luiskcr/forge/compare/v0.1.3...v0.1.4) (2026-04-12)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* refresh flow for already-initialized projects + config commands ([fae7242](https://github.com/luiskcr/forge/commit/fae7242072b5dfa6ae23c40bd601b906cc122ab2))
|
|
9
|
+
|
|
10
|
+
## [0.1.3](https://github.com/luiskcr/forge/compare/v0.1.2...v0.1.3) (2026-04-12)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Documentation
|
|
14
|
+
|
|
15
|
+
* recommend uv as install method and document macOS Tahoe pyexpat issue ([292518a](https://github.com/luiskcr/forge/commit/292518a01ca7d546fa892a4618cced8033290524))
|
|
16
|
+
|
|
3
17
|
## [0.1.2](https://github.com/luiskcr/forge/compare/v0.1.1...v0.1.2) (2026-04-12)
|
|
4
18
|
|
|
5
19
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: forge-dev
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: AI-Native Development Workflow Engine
|
|
5
5
|
Author: NaiaTech
|
|
6
6
|
License-Expression: MIT
|
|
@@ -48,35 +48,73 @@ Forge encodes a complete development paradigm — from receiving a vague require
|
|
|
48
48
|
|
|
49
49
|
## Installation
|
|
50
50
|
|
|
51
|
+
Forge is published on PyPI as `forge-dev`. The recommended way to install it is with [`uv`](https://github.com/astral-sh/uv), which manages its own Python and isolates CLI tools automatically:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# 1. Install uv (one-time, if you don't have it)
|
|
55
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh # macOS / Linux
|
|
56
|
+
# Windows PowerShell:
|
|
57
|
+
# powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
|
|
58
|
+
|
|
59
|
+
# 2. Install forge-dev as a global tool
|
|
60
|
+
uv tool install forge-dev
|
|
61
|
+
|
|
62
|
+
# 3. Verify
|
|
63
|
+
forge --version
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
<details>
|
|
67
|
+
<summary>Alternative: <code>pipx</code></summary>
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
pipx install forge-dev
|
|
71
|
+
forge --version
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
</details>
|
|
75
|
+
|
|
76
|
+
<details>
|
|
77
|
+
<summary>Alternative: plain <code>pip</code></summary>
|
|
78
|
+
|
|
51
79
|
```bash
|
|
52
80
|
pip install forge-dev
|
|
53
81
|
```
|
|
54
82
|
|
|
55
|
-
|
|
83
|
+
</details>
|
|
84
|
+
|
|
85
|
+
<details>
|
|
86
|
+
<summary>From source (for contributors)</summary>
|
|
56
87
|
|
|
57
88
|
```bash
|
|
58
89
|
git clone https://github.com/luiskcr/forge.git
|
|
59
90
|
cd forge
|
|
60
|
-
pip install -e .
|
|
91
|
+
pip install -e ".[dev]"
|
|
61
92
|
```
|
|
62
93
|
|
|
63
|
-
|
|
94
|
+
</details>
|
|
95
|
+
|
|
96
|
+
Requires Python ≥ 3.11. If you use `uv`, it will download a compatible Python automatically.
|
|
64
97
|
|
|
65
98
|
### Updating
|
|
66
99
|
|
|
100
|
+
Use the same tool you installed with:
|
|
101
|
+
|
|
67
102
|
```bash
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
103
|
+
uv tool upgrade forge-dev # if installed via uv (recommended)
|
|
104
|
+
pipx upgrade forge-dev # if installed via pipx
|
|
105
|
+
pip install --upgrade forge-dev # if installed via pip
|
|
106
|
+
forge --version # verify the new version
|
|
71
107
|
```
|
|
72
108
|
|
|
73
|
-
|
|
109
|
+
To pin a specific version:
|
|
74
110
|
|
|
75
111
|
```bash
|
|
76
|
-
|
|
112
|
+
uv tool install forge-dev==0.1.2 --force
|
|
77
113
|
```
|
|
78
114
|
|
|
79
|
-
> Homebrew is not supported
|
|
115
|
+
> **Homebrew is not supported.** Forge is distributed exclusively via PyPI.
|
|
116
|
+
|
|
117
|
+
> **Note for macOS users on Tahoe (macOS 26):** the Homebrew bottles for `python@3.13` and `python@3.14` currently ship a broken `pyexpat` module (a `libexpat` symbol mismatch) that breaks `pip`, `pipx` and `venv`. If you hit `ImportError: Symbol not found: _XML_SetAllocTrackerActivationThreshold`, use `uv` instead — it bundles its own Python and avoids the issue entirely.
|
|
80
118
|
|
|
81
119
|
## How Forge Works
|
|
82
120
|
|
|
@@ -19,35 +19,73 @@ Forge encodes a complete development paradigm — from receiving a vague require
|
|
|
19
19
|
|
|
20
20
|
## Installation
|
|
21
21
|
|
|
22
|
+
Forge is published on PyPI as `forge-dev`. The recommended way to install it is with [`uv`](https://github.com/astral-sh/uv), which manages its own Python and isolates CLI tools automatically:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# 1. Install uv (one-time, if you don't have it)
|
|
26
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh # macOS / Linux
|
|
27
|
+
# Windows PowerShell:
|
|
28
|
+
# powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
|
|
29
|
+
|
|
30
|
+
# 2. Install forge-dev as a global tool
|
|
31
|
+
uv tool install forge-dev
|
|
32
|
+
|
|
33
|
+
# 3. Verify
|
|
34
|
+
forge --version
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
<details>
|
|
38
|
+
<summary>Alternative: <code>pipx</code></summary>
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pipx install forge-dev
|
|
42
|
+
forge --version
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
</details>
|
|
46
|
+
|
|
47
|
+
<details>
|
|
48
|
+
<summary>Alternative: plain <code>pip</code></summary>
|
|
49
|
+
|
|
22
50
|
```bash
|
|
23
51
|
pip install forge-dev
|
|
24
52
|
```
|
|
25
53
|
|
|
26
|
-
|
|
54
|
+
</details>
|
|
55
|
+
|
|
56
|
+
<details>
|
|
57
|
+
<summary>From source (for contributors)</summary>
|
|
27
58
|
|
|
28
59
|
```bash
|
|
29
60
|
git clone https://github.com/luiskcr/forge.git
|
|
30
61
|
cd forge
|
|
31
|
-
pip install -e .
|
|
62
|
+
pip install -e ".[dev]"
|
|
32
63
|
```
|
|
33
64
|
|
|
34
|
-
|
|
65
|
+
</details>
|
|
66
|
+
|
|
67
|
+
Requires Python ≥ 3.11. If you use `uv`, it will download a compatible Python automatically.
|
|
35
68
|
|
|
36
69
|
### Updating
|
|
37
70
|
|
|
71
|
+
Use the same tool you installed with:
|
|
72
|
+
|
|
38
73
|
```bash
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
74
|
+
uv tool upgrade forge-dev # if installed via uv (recommended)
|
|
75
|
+
pipx upgrade forge-dev # if installed via pipx
|
|
76
|
+
pip install --upgrade forge-dev # if installed via pip
|
|
77
|
+
forge --version # verify the new version
|
|
42
78
|
```
|
|
43
79
|
|
|
44
|
-
|
|
80
|
+
To pin a specific version:
|
|
45
81
|
|
|
46
82
|
```bash
|
|
47
|
-
|
|
83
|
+
uv tool install forge-dev==0.1.2 --force
|
|
48
84
|
```
|
|
49
85
|
|
|
50
|
-
> Homebrew is not supported
|
|
86
|
+
> **Homebrew is not supported.** Forge is distributed exclusively via PyPI.
|
|
87
|
+
|
|
88
|
+
> **Note for macOS users on Tahoe (macOS 26):** the Homebrew bottles for `python@3.13` and `python@3.14` currently ship a broken `pyexpat` module (a `libexpat` symbol mismatch) that breaks `pip`, `pipx` and `venv`. If you hit `ImportError: Symbol not found: _XML_SetAllocTrackerActivationThreshold`, use `uv` instead — it bundles its own Python and avoids the issue entirely.
|
|
51
89
|
|
|
52
90
|
## How Forge Works
|
|
53
91
|
|
|
@@ -28,22 +28,127 @@ def main() -> None:
|
|
|
28
28
|
pass
|
|
29
29
|
|
|
30
30
|
|
|
31
|
+
def _refresh_initialized_project(project_path: Path, detection) -> None:
|
|
32
|
+
"""Refresh an already-initialized project.
|
|
33
|
+
|
|
34
|
+
Called when `forge init` runs on a directory that already has `.forge/`.
|
|
35
|
+
Keeps the existing context as the source of truth, but:
|
|
36
|
+
- Tops up any UNSPECIFIED fields that the current detector can now resolve
|
|
37
|
+
(e.g., the user added a main.bicep since last init)
|
|
38
|
+
- Regenerates the Claude Code skill and discovery prompt (Forge-owned)
|
|
39
|
+
- Generates CLAUDE.md if it was deleted, but never overwrites a custom one
|
|
40
|
+
- Ensures .forge/overrides/ exists
|
|
41
|
+
"""
|
|
42
|
+
from forge_core.editor_bridge import collect_unspecified_fields, write_editor_file
|
|
43
|
+
from forge_core.phases.context import load_context, save_context
|
|
44
|
+
|
|
45
|
+
ctx = load_context(project_path)
|
|
46
|
+
if ctx is None:
|
|
47
|
+
console.print(
|
|
48
|
+
"[red].forge/context.yaml is missing or unreadable. "
|
|
49
|
+
"Run `forge init --force` to recreate.[/red]"
|
|
50
|
+
)
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
# Top up unspecified fields from current detection
|
|
54
|
+
detected = detection.detected_stack or {}
|
|
55
|
+
topped_up: list[str] = []
|
|
56
|
+
mapping = {
|
|
57
|
+
"cloud": ("cloud", ctx.cloud.value),
|
|
58
|
+
"backend": ("backend", ctx.backend.value),
|
|
59
|
+
"frontend": ("frontend", ctx.frontend.value),
|
|
60
|
+
"database": ("database", ctx.database.value),
|
|
61
|
+
"auth": ("auth", ctx.auth.value),
|
|
62
|
+
}
|
|
63
|
+
ctx_data = ctx.model_dump(mode="json")
|
|
64
|
+
for det_key, (ctx_key, current) in mapping.items():
|
|
65
|
+
if current == "unspecified" and det_key in detected:
|
|
66
|
+
ctx_data[ctx_key] = detected[det_key]
|
|
67
|
+
topped_up.append(f"{ctx_key}={detected[det_key]}")
|
|
68
|
+
# CICD nested
|
|
69
|
+
if ctx_data.get("cicd", {}).get("iac") == "unspecified" and "iac" in detected:
|
|
70
|
+
ctx_data["cicd"]["iac"] = detected["iac"]
|
|
71
|
+
topped_up.append(f"cicd.iac={detected['iac']}")
|
|
72
|
+
if ctx_data.get("cicd", {}).get("provider") == "unspecified" and "cicd" in detected:
|
|
73
|
+
ctx_data["cicd"]["provider"] = detected["cicd"]
|
|
74
|
+
topped_up.append(f"cicd.provider={detected['cicd']}")
|
|
75
|
+
|
|
76
|
+
if topped_up:
|
|
77
|
+
from forge_core.models import ProjectContext
|
|
78
|
+
try:
|
|
79
|
+
ctx = ProjectContext(**ctx_data)
|
|
80
|
+
save_context(ctx, project_path)
|
|
81
|
+
except Exception as exc:
|
|
82
|
+
console.print(f"[red]Could not persist detected fields: {exc}[/red]")
|
|
83
|
+
|
|
84
|
+
# Ensure overrides/
|
|
85
|
+
overrides_dir = project_path / ".forge" / "overrides"
|
|
86
|
+
overrides_dir.mkdir(parents=True, exist_ok=True)
|
|
87
|
+
|
|
88
|
+
# Regenerate editor files
|
|
89
|
+
claude_path = project_path / "CLAUDE.md"
|
|
90
|
+
if not claude_path.exists():
|
|
91
|
+
write_editor_file(project_path, ctx, "claude")
|
|
92
|
+
claude_status = "[green]generated[/green]"
|
|
93
|
+
else:
|
|
94
|
+
# Don't overwrite a customized CLAUDE.md, but refresh OTHER editor files
|
|
95
|
+
# that already exist (cursor, copilot, etc.)
|
|
96
|
+
from forge_core.editor_bridge import sync_editor_file
|
|
97
|
+
sync_editor_file(project_path, ctx)
|
|
98
|
+
claude_status = "[dim]kept (customized or existing)[/dim]"
|
|
99
|
+
|
|
100
|
+
write_editor_file(project_path, ctx, "skill")
|
|
101
|
+
|
|
102
|
+
remaining = collect_unspecified_fields(ctx)
|
|
103
|
+
discovery_file = project_path / ".forge" / "discovery_prompt.md"
|
|
104
|
+
if remaining:
|
|
105
|
+
write_editor_file(project_path, ctx, "discovery")
|
|
106
|
+
elif discovery_file.exists():
|
|
107
|
+
discovery_file.unlink()
|
|
108
|
+
|
|
109
|
+
# Summary
|
|
110
|
+
lines = [
|
|
111
|
+
"[green]✓ Forge refreshed[/green]",
|
|
112
|
+
"",
|
|
113
|
+
f"Project: [bold]{ctx.name}[/bold]",
|
|
114
|
+
f"CLAUDE.md: {claude_status}",
|
|
115
|
+
]
|
|
116
|
+
if topped_up:
|
|
117
|
+
lines.append(f"[green]Detected new:[/green] {', '.join(topped_up)}")
|
|
118
|
+
if remaining:
|
|
119
|
+
fields = ", ".join(f"`{r['key']}`" for r in remaining)
|
|
120
|
+
lines.append(f"[yellow]Still unspecified:[/yellow] {fields}")
|
|
121
|
+
lines.append("Read `.forge/discovery_prompt.md` in your AI editor.")
|
|
122
|
+
else:
|
|
123
|
+
lines.append("[green]All context fields resolved.[/green]")
|
|
124
|
+
lines.append("")
|
|
125
|
+
lines.append("Tip: use `forge init --force` to wipe context and re-run from scratch.")
|
|
126
|
+
|
|
127
|
+
console.print(Panel("\n".join(lines), title="🔄 Refreshed", border_style="green"))
|
|
128
|
+
|
|
129
|
+
|
|
31
130
|
@main.command()
|
|
32
131
|
@click.option("--name", "-n", help="Project name (default: directory name)")
|
|
33
132
|
@click.option("--backend", "-b", help="Backend framework override")
|
|
34
133
|
@click.option("--no-interactive", is_flag=True, help="Skip interactive questions")
|
|
35
|
-
|
|
134
|
+
@click.option(
|
|
135
|
+
"--force",
|
|
136
|
+
is_flag=True,
|
|
137
|
+
help="Wipe existing .forge/context.yaml and re-run init from scratch",
|
|
138
|
+
)
|
|
139
|
+
def init(name: str | None, backend: str | None, no_interactive: bool, force: bool) -> None:
|
|
36
140
|
"""Initialize Forge in the current directory.
|
|
37
|
-
|
|
141
|
+
|
|
38
142
|
Detects the project state and adapts:
|
|
39
143
|
- Empty folder → guided setup conversation
|
|
40
144
|
- Has docs → analyzes documents, generates brief
|
|
41
145
|
- Has code → detects stack, offers assessment
|
|
42
|
-
- Has .forge/ →
|
|
146
|
+
- Has .forge/ → refresh editor files + re-run detection (use --force to reset)
|
|
43
147
|
"""
|
|
44
148
|
from forge_core.detector import detect_project
|
|
45
149
|
from forge_core.phases.context import (
|
|
46
150
|
get_questions_for_empty_project,
|
|
151
|
+
load_context,
|
|
47
152
|
resolve_context,
|
|
48
153
|
save_context,
|
|
49
154
|
)
|
|
@@ -65,14 +170,29 @@ def init(name: str | None, backend: str | None, no_interactive: bool) -> None:
|
|
|
65
170
|
))
|
|
66
171
|
console.print()
|
|
67
172
|
|
|
68
|
-
if detection.has_forge:
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
console.print(" forge assess — re-evaluate against latest standards")
|
|
73
|
-
console.print(" forge audit — run audit agents")
|
|
173
|
+
if detection.has_forge and not force:
|
|
174
|
+
# Refresh mode: keep existing context, top up any newly detectable fields,
|
|
175
|
+
# regenerate editor files (skill, discovery prompt, CLAUDE.md if missing).
|
|
176
|
+
_refresh_initialized_project(project_path, detection)
|
|
74
177
|
return
|
|
75
178
|
|
|
179
|
+
if detection.has_forge and force:
|
|
180
|
+
# Wipe context.yaml so the rest of the function proceeds as a fresh init.
|
|
181
|
+
(project_path / ".forge" / "context.yaml").unlink(missing_ok=True)
|
|
182
|
+
console.print("[yellow]--force: existing .forge/context.yaml removed.[/yellow]")
|
|
183
|
+
detection.has_forge = False
|
|
184
|
+
detection.forge_context_path = None
|
|
185
|
+
# Re-evaluate state so the branching below picks the right path
|
|
186
|
+
if detection.code_files or detection.config_files:
|
|
187
|
+
from forge_core.models import ProjectState
|
|
188
|
+
detection.state = ProjectState.HAS_CODE
|
|
189
|
+
elif detection.doc_files:
|
|
190
|
+
from forge_core.models import ProjectState
|
|
191
|
+
detection.state = ProjectState.HAS_DOCS
|
|
192
|
+
else:
|
|
193
|
+
from forge_core.models import ProjectState
|
|
194
|
+
detection.state = ProjectState.EMPTY
|
|
195
|
+
|
|
76
196
|
if detection.is_empty:
|
|
77
197
|
console.print("[cyan]Empty directory detected. Let's set up your project.[/cyan]")
|
|
78
198
|
console.print()
|
|
@@ -633,6 +753,104 @@ def audit(files: tuple[str, ...], full: bool) -> None:
|
|
|
633
753
|
)
|
|
634
754
|
|
|
635
755
|
|
|
756
|
+
@main.group()
|
|
757
|
+
def config() -> None:
|
|
758
|
+
"""Manage the global Forge user config (~/.forge/user/config.yaml)."""
|
|
759
|
+
pass
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
@config.command("show")
|
|
763
|
+
def config_show() -> None:
|
|
764
|
+
"""Print the current global user config."""
|
|
765
|
+
import yaml as _yaml
|
|
766
|
+
|
|
767
|
+
from forge_core.registry import USER_DIR
|
|
768
|
+
|
|
769
|
+
config_path = USER_DIR / "config.yaml"
|
|
770
|
+
if not config_path.exists():
|
|
771
|
+
console.print(f"[yellow]No config at {config_path}[/yellow]")
|
|
772
|
+
return
|
|
773
|
+
console.print(_yaml.dump(_yaml.safe_load(config_path.read_text()), sort_keys=False))
|
|
774
|
+
|
|
775
|
+
|
|
776
|
+
@config.command("reset")
|
|
777
|
+
@click.option("--yes", is_flag=True, help="Skip confirmation")
|
|
778
|
+
def config_reset(yes: bool) -> None:
|
|
779
|
+
"""Regenerate ~/.forge/user/config.yaml with current defaults.
|
|
780
|
+
|
|
781
|
+
Use this after upgrading Forge if you're seeing stale options (e.g. the
|
|
782
|
+
config was written by an older version with hardcoded defaults like
|
|
783
|
+
`iac: pulumi`). The new defaults leave all stack fields as `unspecified`,
|
|
784
|
+
so you decide per-project or via `forge config set`.
|
|
785
|
+
"""
|
|
786
|
+
from forge_core.registry import USER_DIR, _write_default_user_config # type: ignore
|
|
787
|
+
|
|
788
|
+
config_path = USER_DIR / "config.yaml"
|
|
789
|
+
if config_path.exists() and not yes:
|
|
790
|
+
if not click.confirm(
|
|
791
|
+
f"Overwrite {config_path} with the current Forge defaults?",
|
|
792
|
+
default=False,
|
|
793
|
+
):
|
|
794
|
+
console.print("[yellow]Cancelled.[/yellow]")
|
|
795
|
+
return
|
|
796
|
+
|
|
797
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
798
|
+
_write_default_user_config(config_path)
|
|
799
|
+
console.print(f"[green]✓ {config_path} regenerated.[/green]")
|
|
800
|
+
console.print(
|
|
801
|
+
"[dim]All stack fields are now `unspecified` — project init will "
|
|
802
|
+
"ask you or detect them from the code.[/dim]"
|
|
803
|
+
)
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
@config.command("set")
|
|
807
|
+
@click.argument("assignments", nargs=-1, required=True)
|
|
808
|
+
def config_set(assignments: tuple[str, ...]) -> None:
|
|
809
|
+
"""Set global defaults: `forge config set cloud=azure cicd.iac=bicep`."""
|
|
810
|
+
import yaml as _yaml
|
|
811
|
+
|
|
812
|
+
from forge_core.registry import USER_DIR
|
|
813
|
+
|
|
814
|
+
config_path = USER_DIR / "config.yaml"
|
|
815
|
+
data = _yaml.safe_load(config_path.read_text()) if config_path.exists() else {}
|
|
816
|
+
|
|
817
|
+
updated: list[str] = []
|
|
818
|
+
for assignment in assignments:
|
|
819
|
+
if "=" not in assignment:
|
|
820
|
+
console.print(f"[red]✗ '{assignment}' — expected key=value[/red]")
|
|
821
|
+
continue
|
|
822
|
+
key, raw = assignment.split("=", 1)
|
|
823
|
+
key = key.strip()
|
|
824
|
+
value: object = raw.strip()
|
|
825
|
+
if value in ("true", "True"):
|
|
826
|
+
value = True
|
|
827
|
+
elif value in ("false", "False"):
|
|
828
|
+
value = False
|
|
829
|
+
parts = key.split(".")
|
|
830
|
+
target = data
|
|
831
|
+
for p in parts[:-1]:
|
|
832
|
+
target = target.setdefault(p, {})
|
|
833
|
+
target[parts[-1]] = value
|
|
834
|
+
updated.append(f"{key}={raw}")
|
|
835
|
+
|
|
836
|
+
if not updated:
|
|
837
|
+
sys.exit(1)
|
|
838
|
+
|
|
839
|
+
# Validate by loading through UserConfig
|
|
840
|
+
from forge_core.models import UserConfig
|
|
841
|
+
try:
|
|
842
|
+
UserConfig(**data)
|
|
843
|
+
except Exception as exc:
|
|
844
|
+
console.print(f"[red]✗ Validation failed:[/red] {exc}")
|
|
845
|
+
sys.exit(1)
|
|
846
|
+
|
|
847
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
848
|
+
with open(config_path, "w") as f:
|
|
849
|
+
_yaml.dump(data, f, default_flow_style=False, sort_keys=False)
|
|
850
|
+
for u in updated:
|
|
851
|
+
console.print(f"[green]✓ {u}[/green]")
|
|
852
|
+
|
|
853
|
+
|
|
636
854
|
@main.group()
|
|
637
855
|
def context() -> None:
|
|
638
856
|
"""Inspect or update the project context (.forge/context.yaml).
|
|
@@ -186,16 +186,28 @@ def version_workflow(change_description: str) -> str:
|
|
|
186
186
|
# ── Private helpers ────────────────────────────────────────────────────────
|
|
187
187
|
|
|
188
188
|
def _write_default_user_config(path: Path) -> None:
|
|
189
|
-
"""Write the default user config with
|
|
189
|
+
"""Write the default user config with no opinionated choices.
|
|
190
|
+
|
|
191
|
+
All stack fields default to UNSPECIFIED so Forge never silently assumes
|
|
192
|
+
a cloud / IaC / auth / database choice. The user sets their preferences
|
|
193
|
+
explicitly with `forge config set key=value` (or by editing this file).
|
|
194
|
+
|
|
195
|
+
The non-stack fields (observability, api, ai) keep reasonable technical
|
|
196
|
+
defaults that don't lock the user into a vendor.
|
|
197
|
+
"""
|
|
190
198
|
config = UserConfig(
|
|
191
|
-
cloud="
|
|
199
|
+
cloud="unspecified",
|
|
192
200
|
backend=None, # Ask each time
|
|
193
|
-
frontend="
|
|
194
|
-
database="
|
|
195
|
-
auth="
|
|
196
|
-
ai={
|
|
201
|
+
frontend="unspecified",
|
|
202
|
+
database="unspecified",
|
|
203
|
+
auth="unspecified",
|
|
204
|
+
ai={
|
|
205
|
+
"enabled": False,
|
|
206
|
+
"providers": [],
|
|
207
|
+
"observability": True,
|
|
208
|
+
},
|
|
197
209
|
observability={
|
|
198
|
-
"apm": "
|
|
210
|
+
"apm": "unspecified",
|
|
199
211
|
"metrics": "prometheus",
|
|
200
212
|
"logs": "loki",
|
|
201
213
|
"dashboards": "grafana",
|
|
@@ -207,8 +219,8 @@ def _write_default_user_config(path: Path) -> None:
|
|
|
207
219
|
"versioning": "url-prefix",
|
|
208
220
|
},
|
|
209
221
|
cicd={
|
|
210
|
-
"provider": "
|
|
211
|
-
"iac": "
|
|
222
|
+
"provider": "unspecified",
|
|
223
|
+
"iac": "unspecified",
|
|
212
224
|
"environments": ["dev", "staging", "production"],
|
|
213
225
|
},
|
|
214
226
|
standards={
|
forge_dev-0.1.2/CLAUDE.md
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
# CLAUDE.md
|
|
2
|
-
|
|
3
|
-
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
-
|
|
5
|
-
## Project
|
|
6
|
-
|
|
7
|
-
Forge is an **AI-Native Development Workflow Engine** — a Python CLI + MCP server that transforms requirements into production-ready code. It generates prompts and structured artifacts (briefs, plans, audit reports) that are consumed by LLM-backed editors (Claude Code, Cursor, Copilot). Forge itself does not call LLMs; it prepares and orchestrates the context.
|
|
8
|
-
|
|
9
|
-
Published to PyPI as `forge-dev`. Installed binary is `forge`.
|
|
10
|
-
|
|
11
|
-
## Commands
|
|
12
|
-
|
|
13
|
-
```bash
|
|
14
|
-
# Install for local dev (editable)
|
|
15
|
-
pip install -e ".[dev]"
|
|
16
|
-
|
|
17
|
-
# Run the CLI
|
|
18
|
-
forge --help
|
|
19
|
-
forge init # initialize .forge/ in cwd
|
|
20
|
-
forge status # show project state
|
|
21
|
-
forge sync --format claude # generate CLAUDE.md from Forge governance
|
|
22
|
-
forge audit --full # build audit prompt for entire project
|
|
23
|
-
forge intake <file> # process requirement doc into a brief
|
|
24
|
-
|
|
25
|
-
# Run the MCP server
|
|
26
|
-
python -m mcp_server.server
|
|
27
|
-
|
|
28
|
-
# Quality gates
|
|
29
|
-
ruff check .
|
|
30
|
-
ruff format .
|
|
31
|
-
mypy forge_core mcp_server
|
|
32
|
-
pytest # testpaths = ["tests"] (no tests yet)
|
|
33
|
-
pytest tests/path/to/test.py::test_name # single test
|
|
34
|
-
|
|
35
|
-
# Release (trusted publishing via GitHub Actions)
|
|
36
|
-
# 1. bump version in forge_core/__init__.py AND pyproject.toml
|
|
37
|
-
# 2. gh release create vX.Y.Z --title "..." --notes "..."
|
|
38
|
-
# → .github/workflows/publish.yml builds and publishes to PyPI automatically
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
Python ≥3.11, ruff line-length 100, mypy strict mode.
|
|
42
|
-
|
|
43
|
-
## Architecture
|
|
44
|
-
|
|
45
|
-
Forge has **three persistence layers** — understand these before touching code that reads/writes state:
|
|
46
|
-
|
|
47
|
-
1. **`~/.forge/`** — global registry (managed by `forge_core/registry.py`)
|
|
48
|
-
- `core/`: upstream definitions (phases, agents, templates, standards) — updated via `forge upgrade`
|
|
49
|
-
- `user/`: user customizations that must survive upgrades (standards, patterns, anti-patterns, mcps.yaml, config.yaml, history)
|
|
50
|
-
- `versions/`: workflow version snapshots
|
|
51
|
-
- **Invariant**: `forge upgrade` must never touch `user/`; project `.forge/` dirs are never touched by any global operation.
|
|
52
|
-
|
|
53
|
-
2. **`<project>/.forge/`** — per-project state
|
|
54
|
-
- `context.yaml` (stack/cloud/auth decisions), `brief.yaml`, `plan.yaml`, `journal.md`, `audit/*.md`, `maturity.yaml`
|
|
55
|
-
- Created by `forge init`, read by every other command.
|
|
56
|
-
|
|
57
|
-
3. **Editor bridge output** — `CLAUDE.md`, `.cursorrules`, etc. generated from (1)+(2) by `forge sync`. This is the *delivery mechanism* — Forge's entire value is translating governance into something the editor reads automatically.
|
|
58
|
-
|
|
59
|
-
### Module layout
|
|
60
|
-
|
|
61
|
-
- `forge_core/cli.py` — Click command group; every subcommand lazy-imports its dependencies to keep `forge --help` fast. Each command is thin glue that calls into phases/auditor/editor_bridge.
|
|
62
|
-
- `forge_core/registry.py` — sole owner of `~/.forge/`. Everything that reads or writes the global registry goes through here (`ensure_registry`, `load_user_config`, `load_standards`, `load_mcps`, `record_project`, `version_workflow`).
|
|
63
|
-
- `forge_core/detector.py` — inspects a directory and classifies it (`is_empty` / `has_docs` / `has_existing_code` / `has_forge`). Drives the branching in `forge init`.
|
|
64
|
-
- `forge_core/models.py` — Pydantic models for context, briefs, standards, MCPs, enums (Backend, Cloud, Auth, Regulatory, etc.). Serialization contract between all layers; breaking these changes all persisted state.
|
|
65
|
-
- `forge_core/phases/` — one module per workflow phase. Each phase produces an artifact in `.forge/`:
|
|
66
|
-
- `context.py` → `context.yaml` (+ `get_questions_for_empty_project`, `resolve_context`)
|
|
67
|
-
- `intake.py` → `brief.yaml` (+ classification, prompt builder)
|
|
68
|
-
- `coherence.py` → validates new standards against existing ones before they're added
|
|
69
|
-
- `forge_core/auditor.py` — builds audit prompts (file-scoped and full-project) from context + standards. Does not execute audits; writes prompts to `.forge/audit/` for the LLM editor to consume.
|
|
70
|
-
- `forge_core/editor_bridge.py` — central translator: (context + standards + patterns + journal) → editor-specific instruction file. Add new editor formats here; `cli.py sync` just dispatches by format name.
|
|
71
|
-
- `forge_core/standards/*.yaml` — shipped baseline standards (security, observability, type-safety, api-first, microservice-packaging). These seed `~/.forge/core/standards/` on first run.
|
|
72
|
-
- `mcp_server/server.py` — `FastMCP` wrapper exposing the same operations as MCP tools (`forge_init`, `forge_intake`, `forge_audit`, etc.). Input models use `extra="forbid"`; mirror CLI semantics exactly.
|
|
73
|
-
- `claude_skill/SKILL.md` — Claude Code skill definition that teaches the agent when to invoke Forge.
|
|
74
|
-
|
|
75
|
-
### Key design rules
|
|
76
|
-
|
|
77
|
-
- **Forge builds prompts; LLMs run them.** Commands like `intake`, `audit`, `assess` write a `*_prompt.md` file rather than calling an LLM. Don't add LLM client code to `forge_core`.
|
|
78
|
-
- **Phases write artifacts, commands are glue.** If you're tempted to put logic in `cli.py`, it probably belongs in a phase module or `editor_bridge.py`.
|
|
79
|
-
- **Registry paths live in `registry.py` only.** Don't hardcode `~/.forge/...` elsewhere; import `FORGE_HOME`, `CORE_DIR`, `USER_DIR`.
|
|
80
|
-
- **CLI imports are lazy inside command bodies** (see `cli.py`) — preserve this to keep startup fast. Don't hoist imports to module top.
|
|
81
|
-
- **Standards are YAML, loaded by name.** Adding a new built-in standard = drop a YAML in `forge_core/standards/` matching the existing shape; no code change.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|