forge-dev 0.1.1__tar.gz → 0.1.3__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.3/.github/workflows/release-please.yml +20 -0
- {forge_dev-0.1.1 → forge_dev-0.1.3}/.gitignore +1 -0
- forge_dev-0.1.3/.release-please-config.json +28 -0
- forge_dev-0.1.3/.release-please-manifest.json +3 -0
- forge_dev-0.1.3/CHANGELOG.md +21 -0
- {forge_dev-0.1.1 → forge_dev-0.1.3}/PKG-INFO +48 -10
- {forge_dev-0.1.1 → forge_dev-0.1.3}/README.md +47 -9
- forge_dev-0.1.3/forge_core/__init__.py +8 -0
- {forge_dev-0.1.1 → forge_dev-0.1.3}/forge_core/cli.py +216 -18
- {forge_dev-0.1.1 → forge_dev-0.1.3}/forge_core/detector.py +84 -9
- {forge_dev-0.1.1 → forge_dev-0.1.3}/forge_core/editor_bridge.py +237 -4
- {forge_dev-0.1.1 → forge_dev-0.1.3}/forge_core/models.py +33 -15
- {forge_dev-0.1.1 → forge_dev-0.1.3}/forge_core/phases/context.py +146 -51
- {forge_dev-0.1.1 → forge_dev-0.1.3}/pyproject.toml +1 -1
- forge_dev-0.1.1/CLAUDE.md +0 -81
- forge_dev-0.1.1/forge_core/__init__.py +0 -3
- {forge_dev-0.1.1 → forge_dev-0.1.3}/.github/workflows/publish.yml +0 -0
- {forge_dev-0.1.1 → forge_dev-0.1.3}/claude_skill/SKILL.md +0 -0
- {forge_dev-0.1.1 → forge_dev-0.1.3}/forge_core/agents/__init__.py +0 -0
- {forge_dev-0.1.1 → forge_dev-0.1.3}/forge_core/auditor.py +0 -0
- {forge_dev-0.1.1 → forge_dev-0.1.3}/forge_core/phases/__init__.py +0 -0
- {forge_dev-0.1.1 → forge_dev-0.1.3}/forge_core/phases/coherence.py +0 -0
- {forge_dev-0.1.1 → forge_dev-0.1.3}/forge_core/phases/intake.py +0 -0
- {forge_dev-0.1.1 → forge_dev-0.1.3}/forge_core/registry.py +0 -0
- {forge_dev-0.1.1 → forge_dev-0.1.3}/forge_core/standards/api-first-design.yaml +0 -0
- {forge_dev-0.1.1 → forge_dev-0.1.3}/forge_core/standards/microservice-packaging.yaml +0 -0
- {forge_dev-0.1.1 → forge_dev-0.1.3}/forge_core/standards/observability.yaml +0 -0
- {forge_dev-0.1.1 → forge_dev-0.1.3}/forge_core/standards/security-baseline.yaml +0 -0
- {forge_dev-0.1.1 → forge_dev-0.1.3}/forge_core/standards/type-safety.yaml +0 -0
- {forge_dev-0.1.1 → forge_dev-0.1.3}/forge_core/templates/__init__.py +0 -0
- {forge_dev-0.1.1 → forge_dev-0.1.3}/forge_core/utils/__init__.py +0 -0
- {forge_dev-0.1.1 → forge_dev-0.1.3}/mcp_server/__init__.py +0 -0
- {forge_dev-0.1.1 → forge_dev-0.1.3}/mcp_server/server.py +0 -0
- {forge_dev-0.1.1 → forge_dev-0.1.3}/references/implementation-phases.md +0 -0
- {forge_dev-0.1.1 → forge_dev-0.1.3}/setup.sh +0 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
name: release-please
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: write
|
|
10
|
+
pull-requests: write
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
release-please:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: googleapis/release-please-action@v4
|
|
17
|
+
with:
|
|
18
|
+
token: ${{ secrets.RELEASE_PLEASE_TOKEN }}
|
|
19
|
+
config-file: .release-please-config.json
|
|
20
|
+
manifest-file: .release-please-manifest.json
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
|
|
3
|
+
"packages": {
|
|
4
|
+
".": {
|
|
5
|
+
"release-type": "python",
|
|
6
|
+
"package-name": "forge-dev",
|
|
7
|
+
"changelog-path": "CHANGELOG.md",
|
|
8
|
+
"bump-minor-pre-major": true,
|
|
9
|
+
"bump-patch-for-minor-pre-major": true,
|
|
10
|
+
"draft": false,
|
|
11
|
+
"prerelease": false,
|
|
12
|
+
"include-component-in-tag": false,
|
|
13
|
+
"tag-separator": "",
|
|
14
|
+
"changelog-sections": [
|
|
15
|
+
{ "type": "feat", "section": "Features" },
|
|
16
|
+
{ "type": "fix", "section": "Bug Fixes" },
|
|
17
|
+
{ "type": "perf", "section": "Performance" },
|
|
18
|
+
{ "type": "refactor", "section": "Refactoring" },
|
|
19
|
+
{ "type": "docs", "section": "Documentation" },
|
|
20
|
+
{ "type": "chore", "section": "Chores", "hidden": true },
|
|
21
|
+
{ "type": "ci", "section": "CI", "hidden": true },
|
|
22
|
+
{ "type": "test", "section": "Tests", "hidden": true },
|
|
23
|
+
{ "type": "build", "section": "Build", "hidden": true },
|
|
24
|
+
{ "type": "style", "section": "Style", "hidden": true }
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.1.3](https://github.com/luiskcr/forge/compare/v0.1.2...v0.1.3) (2026-04-12)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Documentation
|
|
7
|
+
|
|
8
|
+
* recommend uv as install method and document macOS Tahoe pyexpat issue ([292518a](https://github.com/luiskcr/forge/commit/292518a01ca7d546fa892a4618cced8033290524))
|
|
9
|
+
|
|
10
|
+
## [0.1.2](https://github.com/luiskcr/forge/compare/v0.1.1...v0.1.2) (2026-04-12)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* discovery-driven context resolution with unspecified fields ([c5b2269](https://github.com/luiskcr/forge/commit/c5b22699105939be657692a9eb0bab96efbd279f))
|
|
16
|
+
|
|
17
|
+
## Changelog
|
|
18
|
+
|
|
19
|
+
All notable changes to this project are documented in this file.
|
|
20
|
+
|
|
21
|
+
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) and uses [Conventional Commits](https://www.conventionalcommits.org/). The changelog below is maintained automatically by [release-please](https://github.com/googleapis/release-please) from commit messages on `main`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: forge-dev
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
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
|
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Forge — AI-Native Development Workflow Engine."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
__version__ = version("forge-dev")
|
|
7
|
+
except PackageNotFoundError: # pragma: no cover — running from source without install
|
|
8
|
+
__version__ = "0.0.0+unknown"
|
|
@@ -163,6 +163,47 @@ def init(name: str | None, backend: str | None, no_interactive: bool) -> None:
|
|
|
163
163
|
"## Entries\n\n"
|
|
164
164
|
)
|
|
165
165
|
|
|
166
|
+
# Create overrides/ with a README so users know what goes there
|
|
167
|
+
overrides_dir = project_path / ".forge" / "overrides"
|
|
168
|
+
overrides_dir.mkdir(parents=True, exist_ok=True)
|
|
169
|
+
overrides_readme = overrides_dir / "README.md"
|
|
170
|
+
if not overrides_readme.exists():
|
|
171
|
+
overrides_readme.write_text(
|
|
172
|
+
"# Project Overrides\n\n"
|
|
173
|
+
"Drop YAML files here to override or extend Forge governance for "
|
|
174
|
+
"this project only:\n\n"
|
|
175
|
+
"- `standards/<name>.yaml` — add or override a standard\n"
|
|
176
|
+
"- `patterns/<name>.yaml` — project-specific approved patterns\n"
|
|
177
|
+
"- `anti-patterns/<name>.yaml` — project-specific anti-patterns\n\n"
|
|
178
|
+
"These are merged on top of global user overrides from "
|
|
179
|
+
"`~/.forge/user/` when generating `CLAUDE.md`. Run "
|
|
180
|
+
"`forge sync` after editing.\n"
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# Generate editor instructions (CLAUDE.md), Claude Code skill, and —
|
|
184
|
+
# if the project has unresolved fields — the discovery prompt for the
|
|
185
|
+
# AI editor to fill in.
|
|
186
|
+
#
|
|
187
|
+
# CLAUDE.md is NOT overwritten if it already exists — user may have
|
|
188
|
+
# customized it. Skill and discovery files are Forge-owned and always
|
|
189
|
+
# regenerated.
|
|
190
|
+
from forge_core.editor_bridge import collect_unspecified_fields, write_editor_file
|
|
191
|
+
|
|
192
|
+
existing_claude = project_path / "CLAUDE.md"
|
|
193
|
+
if existing_claude.exists():
|
|
194
|
+
claude_md_path = existing_claude
|
|
195
|
+
claude_status = "[yellow]kept (already exists — run `forge sync` to overwrite)[/yellow]"
|
|
196
|
+
else:
|
|
197
|
+
claude_md_path = write_editor_file(project_path, context, "claude")
|
|
198
|
+
claude_status = "[green]generated[/green]"
|
|
199
|
+
skill_path = write_editor_file(project_path, context, "skill")
|
|
200
|
+
|
|
201
|
+
# Discovery prompt — only if there are unspecified fields
|
|
202
|
+
unspecified = collect_unspecified_fields(context)
|
|
203
|
+
discovery_path: Path | None = None
|
|
204
|
+
if unspecified:
|
|
205
|
+
discovery_path = write_editor_file(project_path, context, "discovery")
|
|
206
|
+
|
|
166
207
|
# Record in global history
|
|
167
208
|
from forge_core.registry import record_project
|
|
168
209
|
record_project(
|
|
@@ -171,21 +212,45 @@ def init(name: str | None, backend: str | None, no_interactive: bool) -> None:
|
|
|
171
212
|
{"type": context.type, "backend": context.backend, "cloud": context.cloud},
|
|
172
213
|
)
|
|
173
214
|
|
|
215
|
+
# Build the summary panel
|
|
216
|
+
summary_lines = [
|
|
217
|
+
f"[green]✓ Forge initialized for [bold]{context.name}[/bold][/green]\n",
|
|
218
|
+
f"Context: {context_path}",
|
|
219
|
+
f"Journal: {journal_path}",
|
|
220
|
+
f"Overrides: {overrides_dir}",
|
|
221
|
+
f"CLAUDE: {claude_md_path} — {claude_status}",
|
|
222
|
+
f"Skill: {skill_path}",
|
|
223
|
+
]
|
|
224
|
+
if discovery_path:
|
|
225
|
+
summary_lines.append(f"Discovery: {discovery_path} [yellow](fields need resolution)[/yellow]")
|
|
226
|
+
summary_lines += [
|
|
227
|
+
"",
|
|
228
|
+
f"Stack: {context.backend.value} + {context.frontend.value}",
|
|
229
|
+
f"Cloud: {context.cloud.value}",
|
|
230
|
+
f"Auth: {context.auth.value}",
|
|
231
|
+
"",
|
|
232
|
+
]
|
|
233
|
+
if unspecified:
|
|
234
|
+
fields = ", ".join(f"`{u['key']}`" for u in unspecified)
|
|
235
|
+
summary_lines.append(
|
|
236
|
+
f"[yellow]⚠ {len(unspecified)} field(s) unspecified:[/yellow] {fields}"
|
|
237
|
+
)
|
|
238
|
+
summary_lines.append(
|
|
239
|
+
"Read `.forge/discovery_prompt.md` in your AI editor — it will"
|
|
240
|
+
)
|
|
241
|
+
summary_lines.append(
|
|
242
|
+
"investigate the repo and fill these in automatically."
|
|
243
|
+
)
|
|
244
|
+
else:
|
|
245
|
+
summary_lines += [
|
|
246
|
+
"Next steps:",
|
|
247
|
+
" forge intake <file> — process a requirement document",
|
|
248
|
+
" forge audit — run audit agents",
|
|
249
|
+
" forge sync — regenerate editor files after changes",
|
|
250
|
+
]
|
|
251
|
+
|
|
174
252
|
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
|
-
))
|
|
253
|
+
console.print(Panel("\n".join(summary_lines), title="🔥 Ready", border_style="green"))
|
|
189
254
|
|
|
190
255
|
|
|
191
256
|
@main.command()
|
|
@@ -455,14 +520,16 @@ def mcps() -> None:
|
|
|
455
520
|
)
|
|
456
521
|
def sync(format: str) -> None:
|
|
457
522
|
"""Generate editor instruction files from Forge governance.
|
|
458
|
-
|
|
523
|
+
|
|
459
524
|
Translates ALL Forge knowledge (standards, patterns, journal,
|
|
460
525
|
context) into a CLAUDE.md, .cursorrules, or equivalent file
|
|
461
526
|
that the AI editor reads and follows.
|
|
462
|
-
|
|
527
|
+
|
|
463
528
|
This is the core value of Forge — it bridges governance to the editor.
|
|
529
|
+
Also always refreshes the Claude Code skill and cleans up the discovery
|
|
530
|
+
prompt if every context field is resolved.
|
|
464
531
|
"""
|
|
465
|
-
from forge_core.editor_bridge import write_editor_file
|
|
532
|
+
from forge_core.editor_bridge import collect_unspecified_fields, write_editor_file
|
|
466
533
|
from forge_core.phases.context import load_context
|
|
467
534
|
|
|
468
535
|
project_path = Path.cwd()
|
|
@@ -481,8 +548,26 @@ def sync(format: str) -> None:
|
|
|
481
548
|
path = write_editor_file(project_path, context, fmt)
|
|
482
549
|
console.print(f"[green]✓ Generated: {path}[/green]")
|
|
483
550
|
|
|
551
|
+
# Always refresh the skill file (Forge-owned)
|
|
552
|
+
skill_path = write_editor_file(project_path, context, "skill")
|
|
553
|
+
console.print(f"[green]✓ Generated: {skill_path}[/green]")
|
|
554
|
+
|
|
555
|
+
# Discovery prompt: regenerate if there are still unspecified fields,
|
|
556
|
+
# delete it if everything is resolved.
|
|
557
|
+
remaining = collect_unspecified_fields(context)
|
|
558
|
+
discovery_file = project_path / ".forge" / "discovery_prompt.md"
|
|
559
|
+
if remaining:
|
|
560
|
+
write_editor_file(project_path, context, "discovery")
|
|
561
|
+
console.print(
|
|
562
|
+
f"[yellow]⚠ {len(remaining)} field(s) still unspecified — "
|
|
563
|
+
"discovery prompt refreshed.[/yellow]"
|
|
564
|
+
)
|
|
565
|
+
elif discovery_file.exists():
|
|
566
|
+
discovery_file.unlink()
|
|
567
|
+
console.print("[dim]Removed .forge/discovery_prompt.md (all fields resolved).[/dim]")
|
|
568
|
+
|
|
484
569
|
console.print()
|
|
485
|
-
console.print("[dim]The editor will read
|
|
570
|
+
console.print("[dim]The editor will read these files automatically.[/dim]")
|
|
486
571
|
console.print("[dim]Re-run `forge sync` after changing standards or context.[/dim]")
|
|
487
572
|
|
|
488
573
|
|
|
@@ -548,5 +633,118 @@ def audit(files: tuple[str, ...], full: bool) -> None:
|
|
|
548
633
|
)
|
|
549
634
|
|
|
550
635
|
|
|
636
|
+
@main.group()
|
|
637
|
+
def context() -> None:
|
|
638
|
+
"""Inspect or update the project context (.forge/context.yaml).
|
|
639
|
+
|
|
640
|
+
Used by AI editors during the discovery flow to fill in fields Forge
|
|
641
|
+
could not auto-detect.
|
|
642
|
+
"""
|
|
643
|
+
pass
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
@context.command("show")
|
|
647
|
+
def context_show() -> None:
|
|
648
|
+
"""Print the resolved project context as YAML."""
|
|
649
|
+
import yaml as _yaml
|
|
650
|
+
|
|
651
|
+
from forge_core.phases.context import load_context
|
|
652
|
+
|
|
653
|
+
project_path = Path.cwd()
|
|
654
|
+
ctx = load_context(project_path)
|
|
655
|
+
if not ctx:
|
|
656
|
+
console.print("[red]No Forge project found. Run `forge init` first.[/red]")
|
|
657
|
+
sys.exit(1)
|
|
658
|
+
console.print(_yaml.dump(ctx.model_dump(mode="json"), sort_keys=False))
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
@context.command("set")
|
|
662
|
+
@click.argument("assignments", nargs=-1, required=True)
|
|
663
|
+
def context_set(assignments: tuple[str, ...]) -> None:
|
|
664
|
+
"""Set one or more context fields: `forge context set cloud=azure iac=bicep`.
|
|
665
|
+
|
|
666
|
+
Supported keys: cloud, backend, frontend, database, auth, type, description,
|
|
667
|
+
cicd.iac, cicd.provider, ai.enabled. Nested keys use dot notation.
|
|
668
|
+
After updates, CLAUDE.md and the skill are auto-regenerated.
|
|
669
|
+
"""
|
|
670
|
+
from forge_core.editor_bridge import collect_unspecified_fields, write_editor_file
|
|
671
|
+
from forge_core.phases.context import load_context, save_context
|
|
672
|
+
|
|
673
|
+
project_path = Path.cwd()
|
|
674
|
+
ctx = load_context(project_path)
|
|
675
|
+
if not ctx:
|
|
676
|
+
console.print("[red]No Forge project found. Run `forge init` first.[/red]")
|
|
677
|
+
sys.exit(1)
|
|
678
|
+
|
|
679
|
+
data = ctx.model_dump(mode="json")
|
|
680
|
+
updated: list[str] = []
|
|
681
|
+
errors: list[str] = []
|
|
682
|
+
|
|
683
|
+
for assignment in assignments:
|
|
684
|
+
if "=" not in assignment:
|
|
685
|
+
errors.append(f"'{assignment}' — expected key=value")
|
|
686
|
+
continue
|
|
687
|
+
key, raw_value = assignment.split("=", 1)
|
|
688
|
+
key = key.strip()
|
|
689
|
+
value: object = raw_value.strip()
|
|
690
|
+
|
|
691
|
+
# Booleans
|
|
692
|
+
if value in ("true", "True"):
|
|
693
|
+
value = True
|
|
694
|
+
elif value in ("false", "False"):
|
|
695
|
+
value = False
|
|
696
|
+
|
|
697
|
+
# Nested key (dot notation)
|
|
698
|
+
parts = key.split(".")
|
|
699
|
+
target = data
|
|
700
|
+
try:
|
|
701
|
+
for p in parts[:-1]:
|
|
702
|
+
target = target[p]
|
|
703
|
+
target[parts[-1]] = value
|
|
704
|
+
updated.append(f"{key}={raw_value}")
|
|
705
|
+
except (KeyError, TypeError):
|
|
706
|
+
errors.append(f"'{key}' — unknown field")
|
|
707
|
+
|
|
708
|
+
if errors:
|
|
709
|
+
for e in errors:
|
|
710
|
+
console.print(f"[red]✗ {e}[/red]")
|
|
711
|
+
if not updated:
|
|
712
|
+
sys.exit(1)
|
|
713
|
+
|
|
714
|
+
# Re-validate by round-tripping through the Pydantic model
|
|
715
|
+
from forge_core.models import ProjectContext
|
|
716
|
+
try:
|
|
717
|
+
new_ctx = ProjectContext(**data)
|
|
718
|
+
except Exception as exc:
|
|
719
|
+
console.print(f"[red]✗ Validation failed:[/red] {exc}")
|
|
720
|
+
sys.exit(1)
|
|
721
|
+
|
|
722
|
+
save_context(new_ctx, project_path)
|
|
723
|
+
|
|
724
|
+
# Regenerate editor files that exist and skill (skill is always owned by Forge)
|
|
725
|
+
from forge_core.editor_bridge import sync_editor_file
|
|
726
|
+
sync_editor_file(project_path, new_ctx)
|
|
727
|
+
write_editor_file(project_path, new_ctx, "skill")
|
|
728
|
+
|
|
729
|
+
# Clean up discovery prompt if nothing left unresolved
|
|
730
|
+
discovery_file = project_path / ".forge" / "discovery_prompt.md"
|
|
731
|
+
remaining = collect_unspecified_fields(new_ctx)
|
|
732
|
+
if not remaining and discovery_file.exists():
|
|
733
|
+
discovery_file.unlink()
|
|
734
|
+
console.print("[dim]Removed .forge/discovery_prompt.md (all fields resolved).[/dim]")
|
|
735
|
+
elif remaining:
|
|
736
|
+
# Refresh the discovery prompt with the remaining fields
|
|
737
|
+
write_editor_file(project_path, new_ctx, "discovery")
|
|
738
|
+
|
|
739
|
+
for u in updated:
|
|
740
|
+
console.print(f"[green]✓ {u}[/green]")
|
|
741
|
+
|
|
742
|
+
if remaining:
|
|
743
|
+
fields = ", ".join(r["key"] for r in remaining)
|
|
744
|
+
console.print(f"[yellow]Still unspecified:[/yellow] {fields}")
|
|
745
|
+
else:
|
|
746
|
+
console.print("[green]All context fields resolved.[/green]")
|
|
747
|
+
|
|
748
|
+
|
|
551
749
|
if __name__ == "__main__":
|
|
552
750
|
main()
|
|
@@ -28,6 +28,10 @@ CONFIG_FILES = {
|
|
|
28
28
|
"package.json", "pyproject.toml", "requirements.txt", "Cargo.toml",
|
|
29
29
|
"go.mod", "pom.xml", "build.gradle", "Gemfile", "composer.json",
|
|
30
30
|
"Makefile", "Dockerfile", "docker-compose.yml", "docker-compose.yaml",
|
|
31
|
+
# IaC
|
|
32
|
+
"main.bicep", "Pulumi.yaml", "main.tf", "terraform.tf", "azure.yaml",
|
|
33
|
+
# CI/CD
|
|
34
|
+
"azure-pipelines.yml", ".gitlab-ci.yml",
|
|
31
35
|
}
|
|
32
36
|
|
|
33
37
|
IGNORE_DIRS = {
|
|
@@ -157,14 +161,18 @@ def _scan_directory(
|
|
|
157
161
|
|
|
158
162
|
|
|
159
163
|
def _detect_stack(path: Path, config_files: list[Path]) -> dict[str, str]:
|
|
160
|
-
"""Detect the technology stack from config files.
|
|
164
|
+
"""Detect the technology stack from config files.
|
|
165
|
+
|
|
166
|
+
Only fills fields Forge can identify with high confidence from file
|
|
167
|
+
contents or presence. Anything uncertain is left out — the discovery
|
|
168
|
+
flow (via CLAUDE.md) asks the AI editor to resolve the gaps.
|
|
169
|
+
"""
|
|
161
170
|
stack: dict[str, str] = {}
|
|
162
171
|
config_names = {f.name for f in config_files}
|
|
163
172
|
|
|
164
|
-
# Backend
|
|
173
|
+
# ── Backend / language ────────────────────────────────────────────────
|
|
165
174
|
if "pyproject.toml" in config_names or "requirements.txt" in config_names:
|
|
166
175
|
stack["language"] = "python"
|
|
167
|
-
# Try to detect framework
|
|
168
176
|
for cfg in config_files:
|
|
169
177
|
if cfg.name in ("pyproject.toml", "requirements.txt"):
|
|
170
178
|
try:
|
|
@@ -190,20 +198,87 @@ def _detect_stack(path: Path, config_files: list[Path]) -> dict[str, str]:
|
|
|
190
198
|
stack["backend"] = "node/fastify"
|
|
191
199
|
elif "nestjs" in content or "@nestjs" in content:
|
|
192
200
|
stack["backend"] = "node/nestjs"
|
|
193
|
-
# Frontend
|
|
194
|
-
if '"
|
|
195
|
-
stack["frontend"] = "react"
|
|
196
|
-
elif '"next"' in content:
|
|
201
|
+
# Frontend
|
|
202
|
+
if '"next"' in content:
|
|
197
203
|
stack["frontend"] = "nextjs"
|
|
204
|
+
elif '"react"' in content or '"react-dom"' in content:
|
|
205
|
+
stack["frontend"] = "react"
|
|
198
206
|
elif '"vue"' in content:
|
|
199
207
|
stack["frontend"] = "vue"
|
|
200
208
|
elif '"svelte"' in content:
|
|
201
209
|
stack["frontend"] = "svelte"
|
|
210
|
+
elif '"@angular/core"' in content:
|
|
211
|
+
stack["frontend"] = "angular"
|
|
212
|
+
# Auth (only strong signals)
|
|
213
|
+
if "@azure/msal" in content:
|
|
214
|
+
stack["auth"] = "azure-ad-b2c"
|
|
215
|
+
elif '"next-auth"' in content or '"@auth/core"' in content:
|
|
216
|
+
stack["auth"] = "custom-jwt"
|
|
217
|
+
elif '"@clerk/' in content:
|
|
218
|
+
stack["auth"] = "clerk"
|
|
219
|
+
elif '"@supabase/supabase-js"' in content:
|
|
220
|
+
stack["auth"] = "supabase-auth"
|
|
221
|
+
elif '"auth0"' in content or '"@auth0/' in content:
|
|
222
|
+
stack["auth"] = "auth0"
|
|
223
|
+
except Exception:
|
|
224
|
+
pass
|
|
225
|
+
|
|
226
|
+
# ── IaC ───────────────────────────────────────────────────────────────
|
|
227
|
+
if any(f.suffix == ".bicep" for f in config_files) or "main.bicep" in config_names:
|
|
228
|
+
stack["iac"] = "bicep"
|
|
229
|
+
stack["cloud"] = "azure"
|
|
230
|
+
elif "Pulumi.yaml" in config_names:
|
|
231
|
+
stack["iac"] = "pulumi"
|
|
232
|
+
elif "main.tf" in config_names or "terraform.tf" in config_names:
|
|
233
|
+
stack["iac"] = "terraform"
|
|
234
|
+
elif "azure.yaml" in config_names:
|
|
235
|
+
stack["iac"] = "azd"
|
|
236
|
+
stack["cloud"] = "azure"
|
|
237
|
+
|
|
238
|
+
# Scan top-level for .bicep/.tf files even if not in CONFIG_FILES set
|
|
239
|
+
try:
|
|
240
|
+
for entry in path.iterdir():
|
|
241
|
+
if entry.is_file():
|
|
242
|
+
if entry.suffix == ".bicep":
|
|
243
|
+
stack.setdefault("iac", "bicep")
|
|
244
|
+
stack.setdefault("cloud", "azure")
|
|
245
|
+
elif entry.suffix == ".tf":
|
|
246
|
+
stack.setdefault("iac", "terraform")
|
|
247
|
+
except Exception:
|
|
248
|
+
pass
|
|
249
|
+
|
|
250
|
+
# ── CI/CD ─────────────────────────────────────────────────────────────
|
|
251
|
+
if (path / ".github" / "workflows").exists():
|
|
252
|
+
stack["cicd"] = "github-actions"
|
|
253
|
+
elif "azure-pipelines.yml" in config_names:
|
|
254
|
+
stack["cicd"] = "azure-devops"
|
|
255
|
+
elif ".gitlab-ci.yml" in config_names:
|
|
256
|
+
stack["cicd"] = "gitlab-ci"
|
|
257
|
+
|
|
258
|
+
# ── Database (from docker-compose services) ──────────────────────────
|
|
259
|
+
compose_name = (
|
|
260
|
+
"docker-compose.yml" if "docker-compose.yml" in config_names
|
|
261
|
+
else "docker-compose.yaml" if "docker-compose.yaml" in config_names
|
|
262
|
+
else None
|
|
263
|
+
)
|
|
264
|
+
if compose_name:
|
|
265
|
+
for cfg in config_files:
|
|
266
|
+
if cfg.name == compose_name:
|
|
267
|
+
try:
|
|
268
|
+
content = cfg.read_text(errors="ignore").lower()
|
|
269
|
+
if "postgres" in content:
|
|
270
|
+
stack["database"] = "postgresql"
|
|
271
|
+
elif "mysql" in content or "mariadb" in content:
|
|
272
|
+
stack["database"] = "mysql"
|
|
273
|
+
elif "mongo" in content:
|
|
274
|
+
stack["database"] = "mongodb"
|
|
275
|
+
elif "mssql" in content or "sql-server" in content:
|
|
276
|
+
stack["database"] = "sqlserver"
|
|
202
277
|
except Exception:
|
|
203
278
|
pass
|
|
204
279
|
|
|
205
|
-
# Docker
|
|
206
|
-
if "Dockerfile" in config_names or
|
|
280
|
+
# ── Docker ────────────────────────────────────────────────────────────
|
|
281
|
+
if "Dockerfile" in config_names or compose_name:
|
|
207
282
|
stack["containerized"] = "true"
|
|
208
283
|
|
|
209
284
|
return stack
|