archforge-optimizer 0.1.0__tar.gz → 0.2.0__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.
Files changed (48) hide show
  1. {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/.gitignore +1 -1
  2. {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/PKG-INFO +46 -26
  3. {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/README.md +45 -25
  4. {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/cli.py +198 -17
  5. {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/config.py +1 -1
  6. archforge_optimizer-0.2.0/archforge/config_init.py +592 -0
  7. archforge_optimizer-0.1.0/archforge/config_init.py +0 -150
  8. {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/LICENSE +0 -0
  9. {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/__init__.py +0 -0
  10. {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/__main__.py +0 -0
  11. {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/architect.py +0 -0
  12. {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/diff.py +0 -0
  13. {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/engine.py +0 -0
  14. {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/gatekeeper.py +0 -0
  15. {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/host/__init__.py +0 -0
  16. {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/host/adapters/__init__.py +0 -0
  17. {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/host/adapters/base.py +0 -0
  18. {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/host/adapters/helpers.py +0 -0
  19. {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/host/adapters/langgraph.py +0 -0
  20. {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/host/base.py +0 -0
  21. {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/host/fake.py +0 -0
  22. {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/judge/__init__.py +0 -0
  23. {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/judge/base.py +0 -0
  24. {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/judge/scripted.py +0 -0
  25. {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/lint.py +0 -0
  26. {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/llm/__init__.py +0 -0
  27. {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/llm/_common.py +0 -0
  28. {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/llm/anthropic.py +0 -0
  29. {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/llm/base.py +0 -0
  30. {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/llm/gemini.py +0 -0
  31. {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/llm/groq.py +0 -0
  32. {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/llm/openai.py +0 -0
  33. {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/llm/scripted.py +0 -0
  34. {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/middleware.py +0 -0
  35. {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/models.py +0 -0
  36. {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/mutate.py +0 -0
  37. {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/otel.py +0 -0
  38. {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/runlog.py +0 -0
  39. {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/runner.py +0 -0
  40. {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/spec_builder.py +0 -0
  41. {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/stores/__init__.py +0 -0
  42. {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/stores/_jsonl.py +0 -0
  43. {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/stores/attempt_store.py +0 -0
  44. {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/stores/spec_store.py +0 -0
  45. {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/stores/trace_store.py +0 -0
  46. {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/suite.py +0 -0
  47. {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/userconfig.py +0 -0
  48. {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/pyproject.toml +0 -0
@@ -9,4 +9,4 @@ tests/
9
9
  # Keep the user-editable config (archforge.py) and suite (suite.json) trackable though.
10
10
  .archforge/*
11
11
  !.archforge/archforge.py
12
- !.archforge/suite.json
12
+ !.archforge/suite.jsondist/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: archforge-optimizer
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: ArchForge — a self-improving meta-layer over multi-agent systems
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -30,10 +30,19 @@ Description-Content-Type: text/markdown
30
30
 
31
31
  # ArchForge
32
32
 
33
+ <p align="center">
34
+ <img src="images/logo.png" width="160">
35
+ </p>
36
+
37
+ <p align="center">
38
+ <img src="https://img.shields.io/badge/Python-3.11%2B-3776AB?logo=python&logoColor=white">
39
+ <img src="https://img.shields.io/badge/License-MIT-green.svg">
40
+ </p>
41
+
33
42
  > A self-improving meta-layer over multi-agent systems.
34
43
  > Point it at your graph, give it a rubric, and it evolves your pipeline — one proven change per cycle.
35
44
 
36
- **Python ≥ 3.11** · **zero hard deps beyond pydantic** · **MIT-licensed** · install from PyPI with `pip install archforge-optimizer` · exercised end-to-end on a real LangGraph MAS (groq + google-genai + chroma, OpenTelemetry-traced).
45
+ **Minimal core dependencies · optional provider and tracing integrations** · install from PyPI with `pip install archforge-optimizer` · exercised end-to-end on a real LangGraph MAS (groq + google-genai + chroma, OpenTelemetry-traced).
37
46
 
38
47
  ArchForge sits **on top** of an existing multi-agent system (MAS) and improves it run-over-run. Each cycle it inspects where the judge docked points, proposes **one** targeted change — rewriting an agent's prompt, tuning a knob, adding a verifier, re-wiring a node, swapping a model — and keeps it only if it measurably beats the incumbent on a held-out suite. The host MAS keeps running tasks as normal; ArchForge observes the runs and feeds back an improved pipeline.
39
48
 
@@ -119,8 +128,8 @@ with tempfile.TemporaryDirectory() as d:
119
128
  ```
120
129
 
121
130
  ```
122
- $ archforge-optimizer init # once — scaffolds the project config the Engine reads
123
- $ python evolve_demo.py
131
+ archforge-optimizer init # once — scaffolds project config + the archforge_optimizer/ adapter package
132
+ python evolve_demo.py
124
133
  action=AUTO_PROMOTE margin=+0.15
125
134
  incumbent_mean=0.55 candidate_mean=0.70
126
135
  active_spec_id=57396a49 promoted=True
@@ -194,25 +203,29 @@ pip install archforge-optimizer
194
203
  # Optional: install the provider SDK(s) you actually run (none required to import)
195
204
  pip install "archforge-optimizer[providers-groq,providers-gemini]"
196
205
 
197
- # 2. Scaffold per-project config — writes .archforge/archforge.py (tunables),
198
- # .archforge/suite.json (the eval tasks), .env.example (key template)
206
+ # 2. Scaffold per-project config + the adapter package — writes:
207
+ # .archforge/archforge.py (tunables — ACTIVE sane defaults)
208
+ # .archforge/suite.json (the eval tasks you optimize against)
209
+ # archforge_optimizer/ (a generic LangGraph adapter skeleton — 5 files)
210
+ # __init__.py host.py app.py sidecar.py test_smoke_offline.py
199
211
  archforge-optimizer init
200
212
 
201
- # 3. Put your API keys in .env (gitignored) ── e.g. GROQ_API_KEY=..., GEMINI_API_KEY=...
213
+ # 3. Put your provider API key in a root `.env` (gitignored) ── e.g. GEMINI_API_KEY=...
214
+ # (init never writes or touches .env — it just tells you to put the key there.)
202
215
 
203
- # 4. Lint a Spec before running it — checks DAG validity, node refs, type rules
204
- archforge-optimizer lint path/to/spec.json
216
+ # 4. Edit your MAS details into archforge_optimizer/app.py — fill every `# EDIT:` marker
217
+ # (the node roster, edges, knobs, summarize/apply_llm_config hooks). Then build the
218
+ # bootstrap Spec from your EDITED adapter: it lints the roster first and writes
219
+ # archforge_optimizer/spec.json only if valid (rc=1 + the faults if not — fix + rerun).
220
+ archforge-optimizer make-spec # → archforge_optimizer/spec.json (lint OK)
205
221
 
206
- # 5. Run one Propose-Evaluate-Commit cycle against your MAS, wired by an adapter
207
- archforge-optimizer evolve \
208
- --adapter your_pkg.your_host:YourAdapter \
209
- --seed your_spec.json
222
+ # 5. Run one Propose-Evaluate-Commit cycle against your MAS. evolve auto-defaults
223
+ # --adapter archforge_optimizer.host:AppAdapter and --seed archforge_optimizer/spec.json
224
+ archforge-optimizer evolve
210
225
 
211
226
  # 6. Run the full loop: repeat evolve until K consecutive non-promotions (plateau)
212
- # or a cycle/budget cap is hit
213
- archforge-optimizer evolve-loop \
214
- --adapter your_pkg.your_host:YourAdapter \
215
- --seed your_spec.json --max-cycles 50
227
+ # or set flags in archforge.py
228
+ archforge-optimizer evolve-loop --max-cycles 50
216
229
 
217
230
  # 7. Inspect
218
231
  archforge-optimizer status # print the active incumbent Spec id, lineage, counts
@@ -220,11 +233,15 @@ archforge-optimizer report # print per-attempt score deltas (incumbent vs ca
220
233
  archforge-optimizer approve --all # move PENDING_HUMAN structural wins into active
221
234
  ```
222
235
 
236
+ > `init` never writes `.env.example` or `spec.json` — the provider key lives in your root
237
+ > `.env` (gitignored), and `spec.json` comes from `make-spec` (your real roster, linted),
238
+ > not a template. Lint any Spec by hand with `archforge-optimizer lint path/to/spec.json`.
239
+
223
240
  > You can also invoke as `python -m archforge ...` — identical surface.
224
241
  >
225
242
  > **From source (development).** Clone the repo and `pip install -e .` for an editable install.
226
243
 
227
- The `--provider` flag selects the LLM backing the Architect + Judge (`scripted` by default for zero-cost runs; `anthropic` / `openai` / `groq` / `gemini` for real runs). The host MAS is wired via `--adapter my_pkg.my_host:MyAdapter`.
244
+ The `--provider` flag selects the LLM backing the Architect + Judge (`anthropic` / `openai` / `groq` / `gemini` for real runs). The host MAS is wired via `--adapter my_pkg.my_host:MyAdapter` — and after `init`, `evolve` already defaults it to the `archforge_optimizer.host:AppAdapter`, so you only pass the flag for a custom adapter.
228
245
 
229
246
  ---
230
247
 
@@ -273,10 +290,12 @@ Your adapter builds a runnable pipeline from `spec` (the active incumbent's node
273
290
 
274
291
  A **generic LangGraph adapter** ships in `archforge/host/adapters/langgraph.py` and drives a real `graph.stream(...)` — "describe, don't introspect" (it reads node *names*, the stable surface; it never climbs your graph's internals). It is the easiest path for any LangGraph-based MAS. For other frameworks (CrewAI, AutoGen, raw call loops), subclass `BaseHostAdapter` (`archforge/host/adapters/base.py`) — the kit is factored so adapting *any* MAS is cheap, not bespoke-per-framework.
275
292
 
276
- Run it via the dotted-path seam:
293
+ **`init` scaffolds the adapter for you.** You don't code the wiring from scratch: `archforge-optimizer init` writes a generic, name-neutral `archforge_optimizer/` package (the LangGraph adapter skeleton above) into your project root. Edit the `# EDIT:` markers in `archforge_optimizer/app.py` to describe your MAS — the node roster (`_NODES`), edges (`_EDGES`), knob to state map, and the `summarize`/`apply_llm_config`/`reset_llm_config` hooks — then `archforge-optimizer make-spec` builds + lints `archforge_optimizer/spec.json` from it. Once scaffolded, `evolve` auto-defaults to the scaffold: `--adapter archforge_optimizer.host:AppAdapter` and `--seed archforge_optimizer/spec.json` (only pass the flags for a custom adapter/seed). Per-file clobber guards mean re-running `init` never overwrites your edits unless `--force`, and a missing/half-edited adapter is repaired even when `archforge.py` already exists.
294
+
295
+ Run it via the dotted-path seam — the scaffolded package uses the same `module:Class` form:
277
296
 
278
297
  ```bash
279
- archforge-optimizer evolve-loop --adapter your_pkg.your_host:YourAdapter --seed your_spec.json
298
+ archforge-optimizer evolve-loop # defaults: --adapter archforge_optimizer.host:AppAdapter --seed archforge_optimizer/spec.json
280
299
  ```
281
300
 
282
301
  ---
@@ -286,7 +305,8 @@ archforge-optimizer evolve-loop --adapter your_pkg.your_host:YourAdapter --seed
286
305
  ```
287
306
  archforge-optimizer <command> [flags]
288
307
 
289
- init scaffold .archforge/archforge.py + .env.example + suite.json for this project
308
+ init scaffold .archforge/archforge.py + suite.json + the archforge_optimizer/ adapter package
309
+ make-spec build + lint archforge_optimizer/spec.json from the EDITED adapter (writes only if it passes)
290
310
  lint <path> run the Spec Linter on a JSON Spec file
291
311
  evolve run one Propose-Evaluate-Commit cycle from the active incumbent
292
312
  evolve-loop repeat evolve until the budget cap or a plateau
@@ -301,8 +321,8 @@ archforge-optimizer <command> [flags]
301
321
  | Flag | Purpose |
302
322
  |---|---|
303
323
  | `--root <dir>` | project root holding `.archforge/` (default `.`) |
304
- | `--seed <path>` | bootstrap the root incumbent from a Spec JSON (first run) |
305
- | `--adapter <dotted.path[:Class]>` | your `HostMAS` adapter |
324
+ | `--seed <path>` | bootstrap the root incumbent from a Spec JSON (first run); defaults to `archforge_optimizer/spec.json` when present |
325
+ | `--adapter <dotted.path[:Class]>` | your `HostMAS` adapter; defaults to `archforge_optimizer.host:AppAdapter` when the scaffold is present (not on the `--provider scripted` fake path) |
306
326
  | `--provider {scripted\|anthropic\|openai\|groq\|gemini}` | LLM backing the Architect + Judge |
307
327
  | `--suite <path>` | evaluation suite JSON (default: `.archforge/suite.json`) |
308
328
  | `--tau <float>` | promotion margin τ |
@@ -368,7 +388,7 @@ archforge/
368
388
  diff.py spec_diff / format_diff (human-readable mutation deltas)
369
389
  runlog.py per-cycle run log (cards)
370
390
  models.py Spec/Node/Edge/Step/Trace/RunScore/Attempt/Change/...
371
- config.py .py / userconfig.py / config_init.py versioning + tunable resolver + `init`
391
+ config.py / userconfig.py / config_init.py versioning + tunable resolver + `init`
372
392
  ```
373
393
 
374
394
  ---
@@ -392,8 +412,6 @@ The public model surface (`archforge.models`) is the stable contract: `Spec`, `N
392
412
  - Provider SDKs (optional, install only what you run): `anthropic`, `openai`, `groq`, `google-genai`
393
413
  - For rich tracing (optional): `opentelemetry-sdk` + the per-SDK instrumentors you call
394
414
 
395
- > **Installing from source (development).** For an editable install, `pip install -e .` from a clone of this repository. A flat `pip install .` makes a *non-editable* copy in site-packages, so any later source edit won't take effect at the CLI — if a repo edit ever seems to no-op, check `python -c "import archforge; print(archforge.__file__)"` resolves to the repo, not site-packages.
396
-
397
415
  ---
398
416
 
399
417
  ## Roadmap
@@ -417,4 +435,6 @@ ArchForge is released under the **MIT License** — see [`LICENSE`](LICENSE) for
417
435
 
418
436
  ---
419
437
 
438
+ <p align="center">
420
439
  *ArchForge never patches live state — it swaps which versioned pipeline the host uses.*
440
+ </p>
@@ -1,9 +1,18 @@
1
1
  # ArchForge
2
2
 
3
+ <p align="center">
4
+ <img src="images/logo.png" width="160">
5
+ </p>
6
+
7
+ <p align="center">
8
+ <img src="https://img.shields.io/badge/Python-3.11%2B-3776AB?logo=python&logoColor=white">
9
+ <img src="https://img.shields.io/badge/License-MIT-green.svg">
10
+ </p>
11
+
3
12
  > A self-improving meta-layer over multi-agent systems.
4
13
  > Point it at your graph, give it a rubric, and it evolves your pipeline — one proven change per cycle.
5
14
 
6
- **Python ≥ 3.11** · **zero hard deps beyond pydantic** · **MIT-licensed** · install from PyPI with `pip install archforge-optimizer` · exercised end-to-end on a real LangGraph MAS (groq + google-genai + chroma, OpenTelemetry-traced).
15
+ **Minimal core dependencies · optional provider and tracing integrations** · install from PyPI with `pip install archforge-optimizer` · exercised end-to-end on a real LangGraph MAS (groq + google-genai + chroma, OpenTelemetry-traced).
7
16
 
8
17
  ArchForge sits **on top** of an existing multi-agent system (MAS) and improves it run-over-run. Each cycle it inspects where the judge docked points, proposes **one** targeted change — rewriting an agent's prompt, tuning a knob, adding a verifier, re-wiring a node, swapping a model — and keeps it only if it measurably beats the incumbent on a held-out suite. The host MAS keeps running tasks as normal; ArchForge observes the runs and feeds back an improved pipeline.
9
18
 
@@ -89,8 +98,8 @@ with tempfile.TemporaryDirectory() as d:
89
98
  ```
90
99
 
91
100
  ```
92
- $ archforge-optimizer init # once — scaffolds the project config the Engine reads
93
- $ python evolve_demo.py
101
+ archforge-optimizer init # once — scaffolds project config + the archforge_optimizer/ adapter package
102
+ python evolve_demo.py
94
103
  action=AUTO_PROMOTE margin=+0.15
95
104
  incumbent_mean=0.55 candidate_mean=0.70
96
105
  active_spec_id=57396a49 promoted=True
@@ -164,25 +173,29 @@ pip install archforge-optimizer
164
173
  # Optional: install the provider SDK(s) you actually run (none required to import)
165
174
  pip install "archforge-optimizer[providers-groq,providers-gemini]"
166
175
 
167
- # 2. Scaffold per-project config — writes .archforge/archforge.py (tunables),
168
- # .archforge/suite.json (the eval tasks), .env.example (key template)
176
+ # 2. Scaffold per-project config + the adapter package — writes:
177
+ # .archforge/archforge.py (tunables — ACTIVE sane defaults)
178
+ # .archforge/suite.json (the eval tasks you optimize against)
179
+ # archforge_optimizer/ (a generic LangGraph adapter skeleton — 5 files)
180
+ # __init__.py host.py app.py sidecar.py test_smoke_offline.py
169
181
  archforge-optimizer init
170
182
 
171
- # 3. Put your API keys in .env (gitignored) ── e.g. GROQ_API_KEY=..., GEMINI_API_KEY=...
183
+ # 3. Put your provider API key in a root `.env` (gitignored) ── e.g. GEMINI_API_KEY=...
184
+ # (init never writes or touches .env — it just tells you to put the key there.)
172
185
 
173
- # 4. Lint a Spec before running it — checks DAG validity, node refs, type rules
174
- archforge-optimizer lint path/to/spec.json
186
+ # 4. Edit your MAS details into archforge_optimizer/app.py — fill every `# EDIT:` marker
187
+ # (the node roster, edges, knobs, summarize/apply_llm_config hooks). Then build the
188
+ # bootstrap Spec from your EDITED adapter: it lints the roster first and writes
189
+ # archforge_optimizer/spec.json only if valid (rc=1 + the faults if not — fix + rerun).
190
+ archforge-optimizer make-spec # → archforge_optimizer/spec.json (lint OK)
175
191
 
176
- # 5. Run one Propose-Evaluate-Commit cycle against your MAS, wired by an adapter
177
- archforge-optimizer evolve \
178
- --adapter your_pkg.your_host:YourAdapter \
179
- --seed your_spec.json
192
+ # 5. Run one Propose-Evaluate-Commit cycle against your MAS. evolve auto-defaults
193
+ # --adapter archforge_optimizer.host:AppAdapter and --seed archforge_optimizer/spec.json
194
+ archforge-optimizer evolve
180
195
 
181
196
  # 6. Run the full loop: repeat evolve until K consecutive non-promotions (plateau)
182
- # or a cycle/budget cap is hit
183
- archforge-optimizer evolve-loop \
184
- --adapter your_pkg.your_host:YourAdapter \
185
- --seed your_spec.json --max-cycles 50
197
+ # or set flags in archforge.py
198
+ archforge-optimizer evolve-loop --max-cycles 50
186
199
 
187
200
  # 7. Inspect
188
201
  archforge-optimizer status # print the active incumbent Spec id, lineage, counts
@@ -190,11 +203,15 @@ archforge-optimizer report # print per-attempt score deltas (incumbent vs ca
190
203
  archforge-optimizer approve --all # move PENDING_HUMAN structural wins into active
191
204
  ```
192
205
 
206
+ > `init` never writes `.env.example` or `spec.json` — the provider key lives in your root
207
+ > `.env` (gitignored), and `spec.json` comes from `make-spec` (your real roster, linted),
208
+ > not a template. Lint any Spec by hand with `archforge-optimizer lint path/to/spec.json`.
209
+
193
210
  > You can also invoke as `python -m archforge ...` — identical surface.
194
211
  >
195
212
  > **From source (development).** Clone the repo and `pip install -e .` for an editable install.
196
213
 
197
- The `--provider` flag selects the LLM backing the Architect + Judge (`scripted` by default for zero-cost runs; `anthropic` / `openai` / `groq` / `gemini` for real runs). The host MAS is wired via `--adapter my_pkg.my_host:MyAdapter`.
214
+ The `--provider` flag selects the LLM backing the Architect + Judge (`anthropic` / `openai` / `groq` / `gemini` for real runs). The host MAS is wired via `--adapter my_pkg.my_host:MyAdapter` — and after `init`, `evolve` already defaults it to the `archforge_optimizer.host:AppAdapter`, so you only pass the flag for a custom adapter.
198
215
 
199
216
  ---
200
217
 
@@ -243,10 +260,12 @@ Your adapter builds a runnable pipeline from `spec` (the active incumbent's node
243
260
 
244
261
  A **generic LangGraph adapter** ships in `archforge/host/adapters/langgraph.py` and drives a real `graph.stream(...)` — "describe, don't introspect" (it reads node *names*, the stable surface; it never climbs your graph's internals). It is the easiest path for any LangGraph-based MAS. For other frameworks (CrewAI, AutoGen, raw call loops), subclass `BaseHostAdapter` (`archforge/host/adapters/base.py`) — the kit is factored so adapting *any* MAS is cheap, not bespoke-per-framework.
245
262
 
246
- Run it via the dotted-path seam:
263
+ **`init` scaffolds the adapter for you.** You don't code the wiring from scratch: `archforge-optimizer init` writes a generic, name-neutral `archforge_optimizer/` package (the LangGraph adapter skeleton above) into your project root. Edit the `# EDIT:` markers in `archforge_optimizer/app.py` to describe your MAS — the node roster (`_NODES`), edges (`_EDGES`), knob to state map, and the `summarize`/`apply_llm_config`/`reset_llm_config` hooks — then `archforge-optimizer make-spec` builds + lints `archforge_optimizer/spec.json` from it. Once scaffolded, `evolve` auto-defaults to the scaffold: `--adapter archforge_optimizer.host:AppAdapter` and `--seed archforge_optimizer/spec.json` (only pass the flags for a custom adapter/seed). Per-file clobber guards mean re-running `init` never overwrites your edits unless `--force`, and a missing/half-edited adapter is repaired even when `archforge.py` already exists.
264
+
265
+ Run it via the dotted-path seam — the scaffolded package uses the same `module:Class` form:
247
266
 
248
267
  ```bash
249
- archforge-optimizer evolve-loop --adapter your_pkg.your_host:YourAdapter --seed your_spec.json
268
+ archforge-optimizer evolve-loop # defaults: --adapter archforge_optimizer.host:AppAdapter --seed archforge_optimizer/spec.json
250
269
  ```
251
270
 
252
271
  ---
@@ -256,7 +275,8 @@ archforge-optimizer evolve-loop --adapter your_pkg.your_host:YourAdapter --seed
256
275
  ```
257
276
  archforge-optimizer <command> [flags]
258
277
 
259
- init scaffold .archforge/archforge.py + .env.example + suite.json for this project
278
+ init scaffold .archforge/archforge.py + suite.json + the archforge_optimizer/ adapter package
279
+ make-spec build + lint archforge_optimizer/spec.json from the EDITED adapter (writes only if it passes)
260
280
  lint <path> run the Spec Linter on a JSON Spec file
261
281
  evolve run one Propose-Evaluate-Commit cycle from the active incumbent
262
282
  evolve-loop repeat evolve until the budget cap or a plateau
@@ -271,8 +291,8 @@ archforge-optimizer <command> [flags]
271
291
  | Flag | Purpose |
272
292
  |---|---|
273
293
  | `--root <dir>` | project root holding `.archforge/` (default `.`) |
274
- | `--seed <path>` | bootstrap the root incumbent from a Spec JSON (first run) |
275
- | `--adapter <dotted.path[:Class]>` | your `HostMAS` adapter |
294
+ | `--seed <path>` | bootstrap the root incumbent from a Spec JSON (first run); defaults to `archforge_optimizer/spec.json` when present |
295
+ | `--adapter <dotted.path[:Class]>` | your `HostMAS` adapter; defaults to `archforge_optimizer.host:AppAdapter` when the scaffold is present (not on the `--provider scripted` fake path) |
276
296
  | `--provider {scripted\|anthropic\|openai\|groq\|gemini}` | LLM backing the Architect + Judge |
277
297
  | `--suite <path>` | evaluation suite JSON (default: `.archforge/suite.json`) |
278
298
  | `--tau <float>` | promotion margin τ |
@@ -338,7 +358,7 @@ archforge/
338
358
  diff.py spec_diff / format_diff (human-readable mutation deltas)
339
359
  runlog.py per-cycle run log (cards)
340
360
  models.py Spec/Node/Edge/Step/Trace/RunScore/Attempt/Change/...
341
- config.py .py / userconfig.py / config_init.py versioning + tunable resolver + `init`
361
+ config.py / userconfig.py / config_init.py versioning + tunable resolver + `init`
342
362
  ```
343
363
 
344
364
  ---
@@ -362,8 +382,6 @@ The public model surface (`archforge.models`) is the stable contract: `Spec`, `N
362
382
  - Provider SDKs (optional, install only what you run): `anthropic`, `openai`, `groq`, `google-genai`
363
383
  - For rich tracing (optional): `opentelemetry-sdk` + the per-SDK instrumentors you call
364
384
 
365
- > **Installing from source (development).** For an editable install, `pip install -e .` from a clone of this repository. A flat `pip install .` makes a *non-editable* copy in site-packages, so any later source edit won't take effect at the CLI — if a repo edit ever seems to no-op, check `python -c "import archforge; print(archforge.__file__)"` resolves to the repo, not site-packages.
366
-
367
385
  ---
368
386
 
369
387
  ## Roadmap
@@ -387,4 +405,6 @@ ArchForge is released under the **MIT License** — see [`LICENSE`](LICENSE) for
387
405
 
388
406
  ---
389
407
 
408
+ <p align="center">
390
409
  *ArchForge never patches live state — it swaps which versioned pipeline the host uses.*
410
+ </p>
@@ -53,7 +53,9 @@ from archforge.lint import lint
53
53
  from archforge.runlog import RunLog
54
54
  from archforge.stores import AttemptStore, SpecStore, TraceStore
55
55
  from archforge.suite import Suite, load_suite_file
56
- from archforge.config_init import archforge_config_text, env_example_text, _DEFAULT_SUITE_JSON
56
+ from archforge.config_init import (
57
+ archforge_config_text, _DEFAULT_SUITE_JSON, adapter_package_files,
58
+ )
57
59
 
58
60
  # System config (provider roster, CLI fixtures, PROG, .env loader) — single source
59
61
  # in archforge.config. The TUNABLE defaults (tau/delta/repeats/provider/models/…)
@@ -230,12 +232,23 @@ def _build_parser() -> argparse.ArgumentParser:
230
232
  _add_store_args(lint_p)
231
233
  lint_p.add_argument("path", help="path to a Spec JSON file")
232
234
 
233
- # --- init (scaffold user config) -----------------------------------------
235
+ # --- init (scaffold user config + generic adapter package) ----------------
234
236
  init_p = sub.add_parser("init",
235
- help="scaffold .archforge/archforge.py + .env.example for this project")
237
+ help="scaffold .archforge/archforge.py + the archforge_optimizer/ adapter package")
236
238
  _add_store_args(init_p) # --root selects where archforge.py is written
237
239
  init_p.add_argument("--force", action="store_true",
238
- help="overwrite an existing .archforge/archforge.py")
240
+ help="overwrite an existing .archforge/archforge.py + adapter package")
241
+
242
+ # --- make-spec (build spec.json from the edited adapter) ------------------
243
+ mk = sub.add_parser("make-spec",
244
+ help="build + lint archforge_optimizer/spec.json from the edited adapter app")
245
+ _add_store_args(mk)
246
+ mk.add_argument("--adapter", metavar="DOTTED.PATH[:Class]",
247
+ default="archforge_optimizer.host:AppAdapter",
248
+ help="adapter module:Class whose app.build_spec() builds the Spec "
249
+ "(default: archforge_optimizer.host:AppAdapter)")
250
+ mk.add_argument("--out", metavar="PATH", default="archforge_optimizer/spec.json",
251
+ help="where to write the Spec JSON (default: archforge_optimizer/spec.json)")
239
252
 
240
253
  return parser
241
254
 
@@ -519,6 +532,23 @@ def _install_resource_warning_quieteners() -> None:
519
532
  _install_resource_warning_quieteners()
520
533
 
521
534
 
535
+ # The scaffolded adapter package (`archforge-optimizer init`) + its make-spec output.
536
+ # `evolve` defaults to these when present (no `--adapter`/`--seed` flag needed); the
537
+ # flags stay for custom adapters / arbitrary seed paths.
538
+ _SCAFFOLD_ADAPTER = "archforge_optimizer.host:AppAdapter"
539
+ _SCAFFOLD_SPEC = "archforge_optimizer/spec.json"
540
+
541
+
542
+ def _scaffolded_adapter_available() -> bool:
543
+ """True when `init` wrote the adapter package at cwd (its host.py exists)."""
544
+ return (Path.cwd() / "archforge_optimizer" / "host.py").is_file()
545
+
546
+
547
+ def _scaffolded_spec_available() -> bool:
548
+ """True when `make-spec` produced archforge_optimizer/spec.json at cwd."""
549
+ return (Path.cwd() / "archforge_optimizer" / "spec.json").is_file()
550
+
551
+
522
552
  def _cmd_evolve(args: argparse.Namespace, *, components: Components | None,
523
553
  loop: bool) -> int:
524
554
  # Injected `components` (the test/embedding path) always win — they ARE the
@@ -527,6 +557,7 @@ def _cmd_evolve(args: argparse.Namespace, *, components: Components | None,
527
557
  # works" contract): a project with no .archforge/archforge.py gets the init hint
528
558
  # and rc 1 instead of a bogus run. No-op under the pytest gate (tests use the
529
559
  # in-memory sane template).
560
+ components_injected = components is not None
530
561
  if components is None:
531
562
  try:
532
563
  ucfg.ensure_initialized()
@@ -544,6 +575,11 @@ def _cmd_evolve(args: argparse.Namespace, *, components: Components | None,
544
575
 
545
576
  specs, atts, traces = _stores(args.root)
546
577
 
578
+ # Default `--seed` to the scaffolded spec.json when the user scaffolded +
579
+ # ran `make-spec` (no flag needed); the flag still overrides for custom seeds.
580
+ if getattr(args, "seed", None) is None and _scaffolded_spec_available():
581
+ args.seed = _SCAFFOLD_SPEC
582
+
547
583
  # zero-LLM bootstrap of the root incumbent from --seed (if none active)
548
584
  active = _ensure_incumbent(args, specs)
549
585
  if active is None:
@@ -556,7 +592,15 @@ def _cmd_evolve(args: argparse.Namespace, *, components: Components | None,
556
592
  # --adapter: swap the runtime host for an external MAS adapter (a HostMAS /
557
593
  # BaseHostAdapter) while keeping the --provider organs (Architect/Judge).
558
594
  # This is the "adapt any MAS" seam: point at an adapter class, no core edit.
595
+ # When the user scaffolded the adapter package (`init`), DEFAULT --adapter to
596
+ # its AppAdapter (no flag needed); the flag still overrides for custom adapters.
597
+ # Skipped on the scripted fake path (FakeHostMAS is the zero-cost host) and
598
+ # when `components` were injected (test/embedding path owns the host).
559
599
  adapter_path = getattr(args, "adapter", None)
600
+ if (adapter_path is None and not components_injected
601
+ and _scaffolded_adapter_available()
602
+ and (args.provider or ucfg.get("PROVIDER")) != "scripted"):
603
+ adapter_path = _SCAFFOLD_ADAPTER
560
604
  if adapter_path:
561
605
  organs = Components(host=_import_adapter(adapter_path), judge=organs.judge,
562
606
  architect=organs.architect, suite=organs.suite)
@@ -788,13 +832,48 @@ def _cmd_lint(path: str) -> int:
788
832
 
789
833
 
790
834
  def _cmd_init(args: argparse.Namespace) -> int:
791
- """Scaffold `.archforge/archforge.py` (user-editable config) + `.env.example`
792
- (repo root). Never touches a real `.env`. Refuses to clobber an existing
793
- `archforge.py` unless `--force`; never overwrites an existing `.env.example`."""
835
+ """Scaffold the ArchForge project:
836
+
837
+ - ``archforge_optimizer/`` — a generic, name-neutral LangGraph adapter skeleton
838
+ at the project root (cwd) whose ``# EDIT:`` markers the user fills for their MAS.
839
+ - ``.archforge/archforge.py`` — user-editable ACTIVE config (sane defaults).
840
+ - ``.archforge/suite.json`` — one-task eval sidecar (the user tunes the tasks).
841
+
842
+ Does NOT write ``.env.example`` or ``spec.json``. The user's provider API key goes
843
+ in the repo-root ``.env`` (gitignored); the bootstrap Spec is generated by the
844
+ separate ``make-spec`` command from the EDITED adapter (not a placeholder template).
845
+ Never touches a real ``.env``; nothing is echoed. Refuses to clobber an existing
846
+ ``archforge.py`` unless ``--force``; never overwrites ``suite.json``; repairs the
847
+ adapter per-file (only fills missing files unless ``--force``).
848
+
849
+ The adapter scaffold runs FIRST so a re-run with an existing ``archforge.py`` can
850
+ still restore/repair a missing or half-edited ``archforge_optimizer/`` without
851
+ ``--force`` — the per-file keep/``--force`` guards are independent of ``.archforge/``.
852
+ The ``archforge.py`` refuse-clobber then fires its rc=2 only AFTER the adapter is
853
+ already in place, preserving the existing contract."""
794
854
  root = Path(args.root or _DEFAULT_ROOT)
795
855
  cfg_path = root / "archforge.py"
856
+ pkg_dir = Path.cwd() / "archforge_optimizer"
857
+
858
+ # 1. archforge_optimizer/ — the generic LangGraph adapter skeleton, scaffolded at
859
+ # the project root (cwd) so `--adapter archforge_optimizer.host:AppAdapter` resolves
860
+ # (main() puts cwd on sys.path). The user EDITS their MAS details (the # EDIT:
861
+ # markers in app.py) instead of coding the wiring from scratch. Per-file clobber
862
+ # guard mirroring suite.json: never overwrite an existing file unless --force, so a
863
+ # half-edited scaffold still gets its missing files filled. Runs FIRST (independent
864
+ # of .archforge/) so a re-run can repair a deleted adapter without --force.
865
+ for relpath, content in adapter_package_files().items():
866
+ fpath = pkg_dir / relpath
867
+ if fpath.exists() and not args.force:
868
+ print(f"kept: archforge_optimizer/{relpath} (already present)")
869
+ continue
870
+ fpath.parent.mkdir(parents=True, exist_ok=True)
871
+ fpath.write_text(content, encoding="utf-8")
872
+ print(f"created: archforge_optimizer/{relpath}")
796
873
 
797
- # 1. archforge.py — refuse-clobber unless --force.
874
+ # 2. archforge.py — refuse-clobber unless --force. (rc=2 if exists, not --force.)
875
+ # Runs AFTER the adapter scaffold so a re-run that only needs to repair
876
+ # archforge_optimizer/ still gets it even when archforge.py is already present.
798
877
  if cfg_path.exists() and not args.force:
799
878
  print(f"! {cfg_path} already exists. Re-run with --force to overwrite "
800
879
  "(your edits would be lost).", file=sys.stderr)
@@ -803,14 +882,6 @@ def _cmd_init(args: argparse.Namespace) -> int:
803
882
  cfg_path.write_text(archforge_config_text(), encoding="utf-8")
804
883
  print(f"created: {cfg_path} (edit a value to change a default; the file is ACTIVE as-is)")
805
884
 
806
- # 2. .env.example — create once at the repo root (cwd), never overwrite.
807
- env_example = Path(".env.example")
808
- if env_example.exists():
809
- print(f"kept: {env_example} (already present)")
810
- else:
811
- env_example.write_text(env_example_text(), encoding="utf-8")
812
- print(f"created: {env_example}")
813
-
814
885
  # 3. suite.json — seed the eval-task sidecar next to archforge.py (so --root
815
886
  # relocations also move the seeded suite); never overwrite — the user may have
816
887
  # tuned the tasks. Byte-identical to the CLI's one-task fallback fixture.
@@ -820,10 +891,118 @@ def _cmd_init(args: argparse.Namespace) -> int:
820
891
  else:
821
892
  suite_path.write_text(_DEFAULT_SUITE_JSON, encoding="utf-8")
822
893
  print(f"created: {suite_path} (edit the tasks to change what you optimize against)")
823
- print(f"\nNext: edit {cfg_path}, then run `{PROG} evolve --seed <spec.json>`.")
894
+
895
+ spec_path = pkg_dir / "spec.json"
896
+ print(f"\nNext: put your provider API key in a root `.env` (gitignored), edit the "
897
+ f"# EDIT: markers in {pkg_dir / 'app.py'} (your MAS's node roster/edges/knobs), "
898
+ f"run `{PROG} make-spec` to build + lint `archforge_optimizer/spec.json` from "
899
+ f"your edited adapter, then `{PROG} evolve --adapter "
900
+ f"archforge_optimizer.host:AppAdapter --seed {spec_path}`.")
901
+ return 0
902
+
903
+
904
+ def _cmd_make_spec(args: argparse.Namespace) -> int:
905
+ """Build + lint + write the bootstrap Spec JSON from the EDITED adapter.
906
+
907
+ Unlike `init` (which scaffolds a placeholder), this reads the user's actual MAS
908
+ details: it imports ``--adapter``, lets its app build the bootstrap Spec, runs the
909
+ Spec Linter on the result, and — only if it passes — writes it to ``--out``
910
+ (default ``archforge_optimizer/spec.json``). So the spec.json that ``evolve --seed``
911
+ consumes is the user's real roster, not a template. A failing lint returns rc=1
912
+ WITHOUT writing (the user fixes the # EDIT: markers and re-runs). cwd is on
913
+ sys.path (``main()`` puts it there before dispatch), so
914
+ ``--adapter archforge_optimizer.host:AppAdapter`` resolves.
915
+
916
+ The app's ``build_spec()`` itself asserts-not-lint (it surfaces a malformed roster
917
+ loudly); for ``make-spec`` we want a clean rc=1 + the lint faults, not a raw
918
+ traceback. So a construction-time ``AssertionError`` is caught and its embedded
919
+ fault list is re-printed as the lint output."""
920
+ try:
921
+ _flush_adapter_cache(args.adapter) # always read the current cwd's files
922
+ host = _import_adapter(args.adapter)
923
+ except AssertionError as exc:
924
+ print(f"! {args.adapter}: built Spec fails the linter — not writing {args.out}:",
925
+ file=sys.stderr)
926
+ for fault in _parse_build_spec_assert(str(exc)):
927
+ print(f" {fault}", file=sys.stderr)
928
+ print(" edit the # EDIT: markers in your adapter app.py and re-run `make-spec`.",
929
+ file=sys.stderr)
930
+ return 1
931
+ # `app_spec()` is the adapter's published bootstrap Spec (LangGraphHostAdapter
932
+ # builds + caches it in __init__). It is not on the bare HostMAS protocol, so we
933
+ # surface a clear error for an adapter that lacks it rather than AttributeError.
934
+ build_spec = getattr(host, "app_spec", None)
935
+ if build_spec is None:
936
+ print(f"! {args.adapter} ({type(host).__name__}) exposes no app_spec() — "
937
+ f"make-spec needs a LangGraph-style adapter that builds a bootstrap Spec. "
938
+ f"Pass the host class (e.g. archforge_optimizer.host:AppAdapter).",
939
+ file=sys.stderr)
940
+ return 1
941
+ spec = build_spec()
942
+ faults = lint(spec)
943
+ if faults:
944
+ print(f"! {args.adapter}: built Spec fails the linter — not writing {args.out}:",
945
+ file=sys.stderr)
946
+ for f in faults:
947
+ loc = f" [{f.location}]" if f.location else ""
948
+ print(f" {f.code}{loc}: {f.message}", file=sys.stderr)
949
+ print(" edit the # EDIT: markers in your adapter app.py and re-run `make-spec`.",
950
+ file=sys.stderr)
951
+ return 1
952
+ out_path = Path(args.out)
953
+ out_path.parent.mkdir(parents=True, exist_ok=True)
954
+ out_path.write_text(spec.model_dump_json(indent=2), encoding="utf-8")
955
+ print(f"created: {out_path} (lint OK; run `{PROG} evolve --adapter "
956
+ f"{args.adapter} --seed {out_path}`)")
824
957
  return 0
825
958
 
826
959
 
960
+ def _parse_build_spec_assert(message: str) -> list[str]:
961
+ """Extract the per-fault strings from a ``build_spec()`` AssertionError message
962
+ (``LangGraph app Spec failed lint: ["code@loc: msg", ...]``). Falls back to the raw
963
+ tail of the message if the embedded list repr can't be parsed — never raises, so
964
+ ``make-spec``'s error path stays crash-free on a malformed assert."""
965
+ import ast
966
+ marker = "failed lint:"
967
+ idx = message.find(marker)
968
+ if idx < 0:
969
+ return [message.strip()]
970
+ tail = message[idx + len(marker):].strip()
971
+ try:
972
+ parsed = ast.literal_eval(tail)
973
+ if isinstance(parsed, (list, tuple)) and all(isinstance(x, str) for x in parsed):
974
+ return list(parsed)
975
+ except (ValueError, SyntaxError):
976
+ pass
977
+ return [tail]
978
+
979
+
980
+ def _flush_adapter_cache(dotted: str) -> None:
981
+ """Drop a ``module:Class`` adapter's module subtree from ``sys.modules`` so the NEXT
982
+ import reads the *current cwd's* files, not a module cached from an earlier cwd.
983
+
984
+ Single-process reuse (the real CLI runs one command per process, so this is a no-op
985
+ there) defeats ``make-spec``/``evolve`` when the user edits ``app.py`` and re-runs
986
+ in the SAME process (e.g. the test harness, or a long-lived embedding): the stale
987
+ ``archforge_optimizer`` — bound to a previous tmp cwd's files — would shadow the
988
+ edit. Flushing + ``importlib.invalidate_caches()`` makes \"build from the edited
989
+ adapter\" honest. Scoped to the adapter's own top-level package so core imports are
990
+ untouched."""
991
+ if ":" in dotted:
992
+ modpath = dotted.split(":", 1)[0]
993
+ else:
994
+ modpath = dotted
995
+ top = modpath.split(".", 1)[0]
996
+ stale = [name for name in list(sys.modules) if name == top or name.startswith(top + ".")]
997
+ for name in stale:
998
+ sys.modules.pop(name, None)
999
+ importlib.invalidate_caches()
1000
+
1001
+
1002
+
1003
+
1004
+
1005
+
827
1006
  # --------------------------------------------------------------------------- #
828
1007
  # entry
829
1008
  # --------------------------------------------------------------------------- #
@@ -869,6 +1048,8 @@ def main(argv: list[str] | None = None, *,
869
1048
  return _cmd_reject(args)
870
1049
  if args.command == "init":
871
1050
  return _cmd_init(args)
1051
+ if args.command == "make-spec":
1052
+ return _cmd_make_spec(args)
872
1053
  if args.command == "evolve":
873
1054
  return _cmd_evolve(args, components=components, loop=False)
874
1055
  if args.command == "evolve-loop":
@@ -29,7 +29,7 @@ from pathlib import Path
29
29
  # =========================================================================== #
30
30
  # The package version. Read by hatchling for the built distribution and
31
31
  # re-exported as `archforge.__version__` (archforge/__init__.py). One place.
32
- VERSION: str = "0.1.0"
32
+ VERSION: str = "0.2.0"
33
33
 
34
34
 
35
35
  # =========================================================================== #