ai-forge-cli 0.1.2__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.
- ai_forge_cli-0.1.2/LICENSE +21 -0
- ai_forge_cli-0.1.2/PKG-INFO +8 -0
- ai_forge_cli-0.1.2/README.md +286 -0
- ai_forge_cli-0.1.2/pyproject.toml +21 -0
- ai_forge_cli-0.1.2/setup.cfg +4 -0
- ai_forge_cli-0.1.2/src/ai_forge_cli.egg-info/PKG-INFO +8 -0
- ai_forge_cli-0.1.2/src/ai_forge_cli.egg-info/SOURCES.txt +30 -0
- ai_forge_cli-0.1.2/src/ai_forge_cli.egg-info/dependency_links.txt +1 -0
- ai_forge_cli-0.1.2/src/ai_forge_cli.egg-info/entry_points.txt +2 -0
- ai_forge_cli-0.1.2/src/ai_forge_cli.egg-info/requires.txt +1 -0
- ai_forge_cli-0.1.2/src/ai_forge_cli.egg-info/top_level.txt +1 -0
- ai_forge_cli-0.1.2/src/cli/__init__.py +2 -0
- ai_forge_cli-0.1.2/src/cli/__main__.py +4 -0
- ai_forge_cli-0.1.2/src/cli/bundle.py +117 -0
- ai_forge_cli-0.1.2/src/cli/commands/__init__.py +28 -0
- ai_forge_cli-0.1.2/src/cli/commands/base.py +26 -0
- ai_forge_cli-0.1.2/src/cli/commands/context.py +66 -0
- ai_forge_cli-0.1.2/src/cli/commands/find.py +122 -0
- ai_forge_cli-0.1.2/src/cli/commands/init.py +447 -0
- ai_forge_cli-0.1.2/src/cli/commands/inspect.py +111 -0
- ai_forge_cli-0.1.2/src/cli/commands/list_cmd.py +72 -0
- ai_forge_cli-0.1.2/src/cli/commands/update.py +78 -0
- ai_forge_cli-0.1.2/src/cli/common.py +120 -0
- ai_forge_cli-0.1.2/src/cli/forge.py +65 -0
- ai_forge_cli-0.1.2/src/cli/index.py +156 -0
- ai_forge_cli-0.1.2/src/cli/walker.py +799 -0
- ai_forge_cli-0.1.2/tests/test_cli.py +134 -0
- ai_forge_cli-0.1.2/tests/test_find.py +113 -0
- ai_forge_cli-0.1.2/tests/test_index.py +63 -0
- ai_forge_cli-0.1.2/tests/test_init.py +141 -0
- ai_forge_cli-0.1.2/tests/test_update.py +70 -0
- ai_forge_cli-0.1.2/tests/test_walker.py +128 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 William James
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
```
|
|
4
|
+
███████╗ ██████╗ ██████╗ ██████╗ ███████╗
|
|
5
|
+
██╔════╝██╔═══██╗██╔══██╗██╔════╝ ██╔════╝
|
|
6
|
+
█████╗ ██║ ██║██████╔╝██║ ███╗█████╗
|
|
7
|
+
██╔══╝ ██║ ██║██╔══██╗██║ ██║██╔══╝
|
|
8
|
+
██║ ╚██████╔╝██║ ██║╚██████╔╝███████╗
|
|
9
|
+
╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
**Spec-driven agent development.**
|
|
13
|
+
The agent drives the interview. The specs drive the code.
|
|
14
|
+
|
|
15
|
+
[](https://github.com/GreyFlames07/forge/actions/workflows/ci.yml)
|
|
16
|
+
[](LICENSE)
|
|
17
|
+
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## What it is
|
|
23
|
+
|
|
24
|
+
A six-layer YAML spec system, a Python CLI for context assembly, and **ten agent skills** that take a human from a vague product idea to working, audited, hardened, validated code — with the agent asking questions and the human answering, not the reverse.
|
|
25
|
+
|
|
26
|
+
Built on the premise that **people explain systems well under questioning but poorly when cold-prompted**. forge inverts the default "human prompts agent → agent implements" loop into "agent interviews human → structured spec emerges → agent implements from spec".
|
|
27
|
+
|
|
28
|
+
Runs in **Claude Code**, **OpenAI Codex CLI**, and any **agentskills.io-compatible client** (VS Code Copilot, Cursor).
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## The pipeline
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
idea
|
|
36
|
+
↓
|
|
37
|
+
forge-discover → foundation (modules, L0 vocab, L1 conventions, L5 posture)
|
|
38
|
+
↓
|
|
39
|
+
forge-decompose → atom inventory (stub files, module populated, entry-point hints)
|
|
40
|
+
↓
|
|
41
|
+
forge-atom → complete specs (one atom at a time — three interview shapes)
|
|
42
|
+
↓
|
|
43
|
+
forge-compose → L4 composition (flows + journeys from completed atoms)
|
|
44
|
+
↓
|
|
45
|
+
forge-audit → quality gate (seven audit passes, severity-ranked findings)
|
|
46
|
+
↓
|
|
47
|
+
forge-armour → security hardening (trust model, policies, abuse-case review)
|
|
48
|
+
↓
|
|
49
|
+
forge-implement → code + tests (parallel subagents, test-before-impl isolation)
|
|
50
|
+
↓
|
|
51
|
+
forge-validate → validation report (static analysis, test mapping, live interaction probes)
|
|
52
|
+
↓
|
|
53
|
+
working system
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Each skill is a markdown directive file (agent-facing) plus a longer framework doc (human reference). Each uses the `forge` CLI to sense project state and load context — agents don't inline spec content in prompts.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Install
|
|
61
|
+
|
|
62
|
+
### Fresh machine — four commands
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
git clone https://github.com/GreyFlames07/forge.git
|
|
66
|
+
cd forge
|
|
67
|
+
uv venv --python 3.13 .venv && uv pip install -e . pytest
|
|
68
|
+
./scripts/install-skills.sh install
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
This wires the `forge` binary into `~/.local/bin/` and symlinks the ten skills into `~/.claude/skills/`, `~/.codex/skills/`, and `~/.agents/skills/` — discoverable by every supported client.
|
|
72
|
+
|
|
73
|
+
### Verify
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
forge --version # shows installed forge version
|
|
77
|
+
forge --help # shows: init, update, context, list, inspect, find
|
|
78
|
+
.venv/bin/pytest # 54 passed
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Requirements
|
|
82
|
+
|
|
83
|
+
| | |
|
|
84
|
+
|---|---|
|
|
85
|
+
| Python | ≥ 3.13 |
|
|
86
|
+
| Package manager | [`uv`](https://docs.astral.sh/uv/) recommended; pip works |
|
|
87
|
+
| One agent client | Claude Code, Codex CLI, or an agentskills.io-compatible client |
|
|
88
|
+
|
|
89
|
+
If `forge: command not found`: add `~/.local/bin` to PATH.
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
export PATH="$HOME/.local/bin:$PATH" # add to ~/.zshrc to persist
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Quick start
|
|
98
|
+
|
|
99
|
+
Bootstrap a new project in any empty directory:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
mkdir ~/my-idea && cd ~/my-idea
|
|
103
|
+
forge init
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
✦ INITIALISING FORGE
|
|
108
|
+
▸ Forge init in /Users/you/my-idea
|
|
109
|
+
|
|
110
|
+
✓ .forge/
|
|
111
|
+
✓ 6 spec subdirectories
|
|
112
|
+
✓ 12 schema templates → .forge/templates/
|
|
113
|
+
✓ 30/30 skill symlinks → .claude/skills/, .codex/skills/, .agents/skills/
|
|
114
|
+
|
|
115
|
+
───── Next steps ─────
|
|
116
|
+
|
|
117
|
+
Set the spec dir (add to your shell rc to persist):
|
|
118
|
+
export FORGE_SPEC_DIR="/Users/you/my-idea/.forge"
|
|
119
|
+
|
|
120
|
+
Start a session in this directory:
|
|
121
|
+
claude │ codex │ any agentskills.io client
|
|
122
|
+
|
|
123
|
+
Trigger a forge skill with a natural-language prompt:
|
|
124
|
+
"I want to build a tool that does X"
|
|
125
|
+
"Decompose the PAY module into atoms"
|
|
126
|
+
"Audit the specs before implementation"
|
|
127
|
+
"Harden the specs for security before implementation"
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Open an agent session in that directory and describe your idea in natural language. The relevant skill activates; the interview begins.
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## CLI
|
|
135
|
+
|
|
136
|
+
| Command | Purpose |
|
|
137
|
+
|---|---|
|
|
138
|
+
| `forge init` | Scaffold a new project (`.forge/` + skill symlinks + schema templates) |
|
|
139
|
+
| `forge update` | Refresh init-managed project assets to the current Forge version |
|
|
140
|
+
| `forge --version` | Print the installed Forge CLI version |
|
|
141
|
+
| `forge list [--kind K]` | Enumerate entities in the spec dir |
|
|
142
|
+
| `forge inspect <id>` | Lightweight metadata probe |
|
|
143
|
+
| `forge context <id>` | Full implementation-ready bundle for an entity |
|
|
144
|
+
| `forge find <query>` | Substring search across names + descriptions |
|
|
145
|
+
|
|
146
|
+
Spec-dir resolution order: `--spec-dir` flag > `$FORGE_SPEC_DIR` env var > auto-discover (walks upward looking for `.forge/`).
|
|
147
|
+
|
|
148
|
+
Full CLI guide: [`docs/cli-guide.md`](docs/cli-guide.md).
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## The ten skills
|
|
153
|
+
|
|
154
|
+
| Skill | Role | Input | Output |
|
|
155
|
+
|---|---|---|---|
|
|
156
|
+
| **forge-discover** | Interviewer (product framing) | Vague idea | Project foundation: modules, L0 skeleton, L1 conventions, L5 posture |
|
|
157
|
+
| **forge-decompose** | Structural extractor | One bounded module | Exhaustive atom stubs (four-pass extraction) |
|
|
158
|
+
| **forge-atom** | Contract specifier | One atom stub | Complete L3 spec + L0 cascades + module completions |
|
|
159
|
+
| **forge-compose** | Composition specifier | Completed atoms + project decisions | L4 flow/journey specs with explicit boundary/retry/compensation/idempotency decisions |
|
|
160
|
+
| **forge-audit** | Challenger / reviewer | Completed specs | Severity-ranked findings with inline edits; seven audit passes |
|
|
161
|
+
| **forge-armour** | Security challenger | Audited specs | Security hardening pass, trust-model capture, approved project/module/atom security edits |
|
|
162
|
+
| **forge-implement** | Orchestrator | Audited spec corpus | Code + tests, dep-graph parallel, test-before-impl isolation |
|
|
163
|
+
| **forge-validate** | Post-implementation validator | Implemented system + spec corpus | Validation report: static analysis, test-to-spec mapping, live interaction probes |
|
|
164
|
+
| **forge-test-writer** | Subagent | One entity + level | Unit/integration/system tests with audit doc |
|
|
165
|
+
| **forge-implementer** | Subagent | One entity | Implementation code, blind to tests |
|
|
166
|
+
|
|
167
|
+
Skills activate via natural-language prompts (universal) or slash-commands (Claude Code only).
|
|
168
|
+
|
|
169
|
+
Each skill has a framework doc (mental model) under `docs/skills/<skill>/framework.md` and a directive SKILL.md under `.agents/skills/<skill>/`.
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## The spec system
|
|
174
|
+
|
|
175
|
+
Six layers, each a source-of-truth YAML file set.
|
|
176
|
+
|
|
177
|
+
| Layer | Purpose |
|
|
178
|
+
|---|---|
|
|
179
|
+
| **L0 Registry** | Vocabulary — types, errors, constants, external schemas, side-effect markers |
|
|
180
|
+
| **L1 Conventions** | Project-wide defaults — retry policy, logging, security posture, verification floors |
|
|
181
|
+
| **L2 Architecture** | Modules — ownership, tech stacks, persistence, permissions, policies |
|
|
182
|
+
| **L3 Behavior** | Atoms (smallest spec unit) + artifacts (non-executing deps) |
|
|
183
|
+
| **L4 Composition** | Flows (saga orchestrations) + journeys (user-facing paths) |
|
|
184
|
+
| **L5 Operations** | Runtime — platform, deployment, rate limiting, event semantics, observability (SLA targets, metrics, alerts) |
|
|
185
|
+
|
|
186
|
+
Schema reference: [`docs/framework-overview.md`](docs/framework-overview.md).
|
|
187
|
+
Full schema templates: [`src/templates/`](src/templates/).
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## Repository layout
|
|
192
|
+
|
|
193
|
+
```
|
|
194
|
+
forge/
|
|
195
|
+
├── src/
|
|
196
|
+
│ ├── cli/ Python CLI package (the forge command)
|
|
197
|
+
│ ├── templates/ L0-L5 schema templates (symlinked into projects by forge init)
|
|
198
|
+
│ └── example/ Working example spec corpus (used by tests)
|
|
199
|
+
├── .agents/skills/ The 10 forge skills (installed into agent clients)
|
|
200
|
+
│ ├── forge-discover/
|
|
201
|
+
│ ├── forge-decompose/
|
|
202
|
+
│ ├── forge-atom/
|
|
203
|
+
│ ├── forge-compose/
|
|
204
|
+
│ ├── forge-audit/
|
|
205
|
+
│ ├── forge-armour/
|
|
206
|
+
│ ├── forge-implement/
|
|
207
|
+
│ ├── forge-validate/
|
|
208
|
+
│ ├── forge-test-writer/
|
|
209
|
+
│ └── forge-implementer/
|
|
210
|
+
├── docs/
|
|
211
|
+
│ ├── skills/ Framework docs for each skill (mental models)
|
|
212
|
+
│ ├── cli-guide.md Full CLI reference
|
|
213
|
+
│ └── framework-overview.md
|
|
214
|
+
├── scripts/
|
|
215
|
+
│ └── install-skills.sh Global skill install + CLI symlink
|
|
216
|
+
├── tests/ pytest suite — 54 tests
|
|
217
|
+
├── pyproject.toml
|
|
218
|
+
└── README.md
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Development
|
|
224
|
+
|
|
225
|
+
### Testing
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
.venv/bin/pytest -v
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
The `src/example/` directory is a complete working spec corpus (a fictional payments app) that doubles as the test fixture and a reference for what a finished project looks like.
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
export FORGE_SPEC_DIR="$(pwd)/src/example"
|
|
235
|
+
forge list # see what's there
|
|
236
|
+
forge context atm.pay.charge_card # inspect a full atom bundle
|
|
237
|
+
forge find charge # search across entities
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Adding a new skill
|
|
241
|
+
|
|
242
|
+
1. Create `.agents/skills/<name>/SKILL.md` with [agentskills.io](https://agentskills.io) frontmatter (`name`, `description`).
|
|
243
|
+
2. Add the skill to `SKILLS=()` in `scripts/install-skills.sh`.
|
|
244
|
+
3. Add the skill to `SKILL_NAMES` in `src/cli/commands/init.py`.
|
|
245
|
+
4. Add a framework doc under `docs/skills/<name>/framework.md` if the skill has a substantial mental model.
|
|
246
|
+
5. `./scripts/install-skills.sh install` to wire up globally.
|
|
247
|
+
6. In any existing projects: `forge update` to refresh their managed scaffolding and local skill links.
|
|
248
|
+
|
|
249
|
+
### Uninstall
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
./scripts/install-skills.sh uninstall
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Removes all global skill symlinks and the `forge` CLI binary. Does not touch project-local `.forge/` directories (those are owned by each project).
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## Contributing
|
|
260
|
+
|
|
261
|
+
Pull requests welcome. See [`CONTRIBUTING.md`](CONTRIBUTING.md) for:
|
|
262
|
+
|
|
263
|
+
- Branching and commit conventions (Conventional Commits)
|
|
264
|
+
- Local dev setup
|
|
265
|
+
- PR requirements and CI expectations
|
|
266
|
+
- Release process
|
|
267
|
+
|
|
268
|
+
The repo uses standard open-source governance: no direct pushes to `main`, PRs require code-owner approval, CI must be green, history stays linear.
|
|
269
|
+
|
|
270
|
+
Changes are tracked in [`CHANGELOG.md`](CHANGELOG.md) following [Keep a Changelog](https://keepachangelog.com).
|
|
271
|
+
|
|
272
|
+
## Security
|
|
273
|
+
|
|
274
|
+
Security issues: see [`SECURITY.md`](SECURITY.md). Do not open public issues for vulnerabilities.
|
|
275
|
+
|
|
276
|
+
## License
|
|
277
|
+
|
|
278
|
+
MIT — see [`LICENSE`](LICENSE).
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
<div align="center">
|
|
283
|
+
|
|
284
|
+
<sub>Built for agent-driven development. Questions over prompts. Specs over intentions. Code follows.</sub>
|
|
285
|
+
|
|
286
|
+
</div>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ai-forge-cli"
|
|
7
|
+
version = "0.1.2"
|
|
8
|
+
description = "Context walker for the Forge L0-L5 spec system"
|
|
9
|
+
requires-python = ">=3.11"
|
|
10
|
+
dependencies = ["pyyaml>=6.0"]
|
|
11
|
+
|
|
12
|
+
[project.scripts]
|
|
13
|
+
forge = "cli.forge:main"
|
|
14
|
+
|
|
15
|
+
[tool.setuptools.packages.find]
|
|
16
|
+
where = ["src"]
|
|
17
|
+
include = ["cli*"]
|
|
18
|
+
|
|
19
|
+
[tool.pytest.ini_options]
|
|
20
|
+
testpaths = ["tests"]
|
|
21
|
+
pythonpath = ["src"]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/ai_forge_cli.egg-info/PKG-INFO
|
|
5
|
+
src/ai_forge_cli.egg-info/SOURCES.txt
|
|
6
|
+
src/ai_forge_cli.egg-info/dependency_links.txt
|
|
7
|
+
src/ai_forge_cli.egg-info/entry_points.txt
|
|
8
|
+
src/ai_forge_cli.egg-info/requires.txt
|
|
9
|
+
src/ai_forge_cli.egg-info/top_level.txt
|
|
10
|
+
src/cli/__init__.py
|
|
11
|
+
src/cli/__main__.py
|
|
12
|
+
src/cli/bundle.py
|
|
13
|
+
src/cli/common.py
|
|
14
|
+
src/cli/forge.py
|
|
15
|
+
src/cli/index.py
|
|
16
|
+
src/cli/walker.py
|
|
17
|
+
src/cli/commands/__init__.py
|
|
18
|
+
src/cli/commands/base.py
|
|
19
|
+
src/cli/commands/context.py
|
|
20
|
+
src/cli/commands/find.py
|
|
21
|
+
src/cli/commands/init.py
|
|
22
|
+
src/cli/commands/inspect.py
|
|
23
|
+
src/cli/commands/list_cmd.py
|
|
24
|
+
src/cli/commands/update.py
|
|
25
|
+
tests/test_cli.py
|
|
26
|
+
tests/test_find.py
|
|
27
|
+
tests/test_index.py
|
|
28
|
+
tests/test_init.py
|
|
29
|
+
tests/test_update.py
|
|
30
|
+
tests/test_walker.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pyyaml>=6.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
cli
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Bundle formatter — renders a walker output dict as YAML, JSON, or markdown.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
from collections import OrderedDict
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import yaml
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Human-readable headers for each bundle section.
|
|
15
|
+
_SECTION_HEADERS: dict[str, str] = {
|
|
16
|
+
"target": "Target",
|
|
17
|
+
"l0_registry_slice": "L0 — Registry (sliced)",
|
|
18
|
+
"l1_conventions": "L1 — Conventions",
|
|
19
|
+
"l2_module": "L2 — Owner Module",
|
|
20
|
+
"l2_entry_points": "L2 — Invoking Entry Points",
|
|
21
|
+
"policies_applied": "L2 — Policies Applied",
|
|
22
|
+
"policies": "L2 — Policies",
|
|
23
|
+
"shared_module_interfaces": "L2 — Whitelisted Module Interfaces",
|
|
24
|
+
"l3_atom": "L3 — Target Atom",
|
|
25
|
+
"l3_artifact": "L3 — Target Artifact",
|
|
26
|
+
"owned_atoms": "L3 — Owned Atoms",
|
|
27
|
+
"owned_artifacts": "L3 — Owned Artifacts",
|
|
28
|
+
"called_atom_signatures": "L3 — Called Atom Signatures",
|
|
29
|
+
"producer_atom_signature": "L3 — Producer Atom Signature",
|
|
30
|
+
"source_artifacts": "L3 — Upstream Source Artifacts",
|
|
31
|
+
"consumer_signatures": "L3 — Consumer Signatures",
|
|
32
|
+
"training_artifact": "L3 — Training Artifact",
|
|
33
|
+
"handler_atoms": "L3 — Handler Atoms",
|
|
34
|
+
"step_atom_signatures": "L3 — Step Atom Signatures",
|
|
35
|
+
"l4_journey": "L4 — Target Journey",
|
|
36
|
+
"l4_orchestration": "L4 — Target Orchestration",
|
|
37
|
+
"invoked_orchestrations": "L4 — Invoked Orchestrations",
|
|
38
|
+
"l4_callers": "L4 — Callers (atoms consumed here)",
|
|
39
|
+
"l5_operations": "L5 — Operations",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# Ensure PyYAML emits OrderedDicts in insertion order.
|
|
44
|
+
def _represent_ordered_dict(dumper, data):
|
|
45
|
+
return dumper.represent_mapping("tag:yaml.org,2002:map", data.items())
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
yaml.add_representer(OrderedDict, _represent_ordered_dict)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def render(bundle: OrderedDict[str, Any], fmt: str = "yaml") -> str:
|
|
52
|
+
if fmt == "json":
|
|
53
|
+
return json.dumps(bundle, indent=2, default=_json_default)
|
|
54
|
+
if fmt == "markdown":
|
|
55
|
+
return _render_markdown(bundle)
|
|
56
|
+
return _render_yaml(bundle)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _render_yaml(bundle: OrderedDict[str, Any]) -> str:
|
|
60
|
+
parts: list[str] = []
|
|
61
|
+
target = bundle.get("target") or {}
|
|
62
|
+
parts.append("# " + "=" * 66)
|
|
63
|
+
parts.append(f"# FORGE CONTEXT BUNDLE")
|
|
64
|
+
parts.append(f"# target: {target.get('id', '<unknown>')}")
|
|
65
|
+
parts.append(f"# kind: {target.get('kind', '<unknown>')}")
|
|
66
|
+
if target.get("atom_kind"):
|
|
67
|
+
parts.append(f"# atom_kind: {target['atom_kind']}")
|
|
68
|
+
parts.append("# " + "=" * 66)
|
|
69
|
+
parts.append("")
|
|
70
|
+
|
|
71
|
+
for key, value in bundle.items():
|
|
72
|
+
if key == "target":
|
|
73
|
+
continue
|
|
74
|
+
if value is None or (isinstance(value, (dict, list)) and not value):
|
|
75
|
+
continue
|
|
76
|
+
header = _SECTION_HEADERS.get(key, key)
|
|
77
|
+
parts.append("# " + "-" * 66)
|
|
78
|
+
parts.append(f"# {header}")
|
|
79
|
+
parts.append("# " + "-" * 66)
|
|
80
|
+
parts.append(yaml.dump({key: value}, sort_keys=False, allow_unicode=True,
|
|
81
|
+
default_flow_style=False, width=100).rstrip())
|
|
82
|
+
parts.append("")
|
|
83
|
+
|
|
84
|
+
return "\n".join(parts) + "\n"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _render_markdown(bundle: OrderedDict[str, Any]) -> str:
|
|
88
|
+
target = bundle.get("target") or {}
|
|
89
|
+
parts: list[str] = [
|
|
90
|
+
f"# Forge context: `{target.get('id', '<unknown>')}`",
|
|
91
|
+
"",
|
|
92
|
+
f"- **kind**: `{target.get('kind', '<unknown>')}`",
|
|
93
|
+
]
|
|
94
|
+
if target.get("atom_kind"):
|
|
95
|
+
parts.append(f"- **atom_kind**: `{target['atom_kind']}`")
|
|
96
|
+
parts.append("")
|
|
97
|
+
|
|
98
|
+
for key, value in bundle.items():
|
|
99
|
+
if key == "target":
|
|
100
|
+
continue
|
|
101
|
+
if value is None or (isinstance(value, (dict, list)) and not value):
|
|
102
|
+
continue
|
|
103
|
+
header = _SECTION_HEADERS.get(key, key)
|
|
104
|
+
parts.append(f"## {header}")
|
|
105
|
+
parts.append("")
|
|
106
|
+
parts.append("```yaml")
|
|
107
|
+
parts.append(yaml.dump({key: value}, sort_keys=False, allow_unicode=True,
|
|
108
|
+
default_flow_style=False, width=100).rstrip())
|
|
109
|
+
parts.append("```")
|
|
110
|
+
parts.append("")
|
|
111
|
+
return "\n".join(parts)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _json_default(o: Any) -> Any:
|
|
115
|
+
if isinstance(o, OrderedDict):
|
|
116
|
+
return dict(o)
|
|
117
|
+
raise TypeError(f"Unserializable: {type(o)}")
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command registry.
|
|
3
|
+
|
|
4
|
+
To add a new command:
|
|
5
|
+
1. Create `cli/commands/<name>.py` exposing NAME, HELP, register, run
|
|
6
|
+
(see `base.py` for the contract, or copy any existing command as a
|
|
7
|
+
starting point).
|
|
8
|
+
2. Import it below and append to ALL_COMMANDS.
|
|
9
|
+
|
|
10
|
+
That's it — `forge.py` auto-discovers every command in ALL_COMMANDS,
|
|
11
|
+
builds its subparser, and wires dispatch via `args.handler`.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from cli.commands import context as _context
|
|
15
|
+
from cli.commands import find as _find
|
|
16
|
+
from cli.commands import init as _init
|
|
17
|
+
from cli.commands import inspect as _inspect
|
|
18
|
+
from cli.commands import list_cmd as _list_cmd
|
|
19
|
+
from cli.commands import update as _update
|
|
20
|
+
|
|
21
|
+
ALL_COMMANDS = [
|
|
22
|
+
_init,
|
|
23
|
+
_update,
|
|
24
|
+
_context,
|
|
25
|
+
_list_cmd,
|
|
26
|
+
_inspect,
|
|
27
|
+
_find,
|
|
28
|
+
]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command module contract.
|
|
3
|
+
|
|
4
|
+
Every module in `cli.commands` must expose:
|
|
5
|
+
|
|
6
|
+
NAME: str
|
|
7
|
+
The subcommand name. Used as `forge <NAME> ...` on the CLI.
|
|
8
|
+
|
|
9
|
+
HELP: str
|
|
10
|
+
One-line help text shown in `forge --help`.
|
|
11
|
+
|
|
12
|
+
def register(sub: argparse._SubParsersAction) -> None:
|
|
13
|
+
Add this command's subparser with all its arguments to the
|
|
14
|
+
provided _SubParsersAction. MUST call
|
|
15
|
+
`parser.set_defaults(handler=run)` on the created subparser so
|
|
16
|
+
dispatch works without any if/elif chain in forge.main.
|
|
17
|
+
|
|
18
|
+
def run(args: argparse.Namespace) -> int:
|
|
19
|
+
Execute the command. Return an exit code:
|
|
20
|
+
0 = success
|
|
21
|
+
1 = usage error (unknown id, missing spec dir, etc.)
|
|
22
|
+
2 = soft warning (e.g., bundle emitted but has unresolved refs)
|
|
23
|
+
|
|
24
|
+
Commands should reuse helpers from `cli.common` rather than reimplementing
|
|
25
|
+
spec-dir loading, id suggestion, or description formatting.
|
|
26
|
+
"""
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""`forge context <id>` — build a full context bundle."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from cli import bundle as bundle_mod
|
|
9
|
+
from cli import common
|
|
10
|
+
from cli import index as index_mod
|
|
11
|
+
from cli import walker
|
|
12
|
+
|
|
13
|
+
NAME = "context"
|
|
14
|
+
HELP = "Build a full implementation-ready context bundle for an id."
|
|
15
|
+
DESCRIPTION = (
|
|
16
|
+
"Walks the spec dependency graph from <id> and emits everything "
|
|
17
|
+
"an agent needs to implement it: the target spec, referenced L0 "
|
|
18
|
+
"entries (sliced, not the whole registry), the owning module, "
|
|
19
|
+
"applicable policies, L1 conventions, L4 callers with derived "
|
|
20
|
+
"implications, and L5 operations."
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def register(sub: argparse._SubParsersAction) -> None:
|
|
25
|
+
p = sub.add_parser(NAME, help=HELP, description=DESCRIPTION)
|
|
26
|
+
p.add_argument(
|
|
27
|
+
"id",
|
|
28
|
+
help="Target id. Must be an atom, module, journey, flow, or artifact.",
|
|
29
|
+
)
|
|
30
|
+
common.add_spec_dir_arg(p)
|
|
31
|
+
p.add_argument(
|
|
32
|
+
"--format", choices=["yaml", "json", "markdown"], default="yaml",
|
|
33
|
+
help=(
|
|
34
|
+
"Output format. yaml (default) is most token-efficient. "
|
|
35
|
+
"json is most parseable. markdown wraps each section in a "
|
|
36
|
+
"heading + code block for pasting into a chat."
|
|
37
|
+
),
|
|
38
|
+
)
|
|
39
|
+
p.set_defaults(handler=run)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def run(args: argparse.Namespace) -> int:
|
|
43
|
+
idx, rc = common.load_index(args.spec_dir)
|
|
44
|
+
if rc != 0:
|
|
45
|
+
return rc
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
index_mod.classify(idx, args.id)
|
|
49
|
+
except (KeyError, ValueError) as e:
|
|
50
|
+
print(f"error: {e}", file=sys.stderr)
|
|
51
|
+
common.suggest_similar(idx, args.id)
|
|
52
|
+
return 1
|
|
53
|
+
|
|
54
|
+
bundle, unresolved = walker.walk(idx, args.id)
|
|
55
|
+
output = bundle_mod.render(bundle, fmt=args.format)
|
|
56
|
+
sys.stdout.write(output)
|
|
57
|
+
|
|
58
|
+
if unresolved:
|
|
59
|
+
seen: set[str] = set()
|
|
60
|
+
uniq = [u for u in unresolved if not (u in seen or seen.add(u))]
|
|
61
|
+
print(f"\n# Unresolved references ({len(uniq)}):", file=sys.stderr)
|
|
62
|
+
for u in uniq:
|
|
63
|
+
print(f"# - {u}", file=sys.stderr)
|
|
64
|
+
return 2
|
|
65
|
+
|
|
66
|
+
return 0
|