atomadic-forge 0.3.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- atomadic_forge/__init__.py +12 -0
- atomadic_forge/__main__.py +5 -0
- atomadic_forge/a0_qk_constants/__init__.py +1 -0
- atomadic_forge/a0_qk_constants/agent_plan_schema.py +120 -0
- atomadic_forge/a0_qk_constants/commandsmith_types.py +49 -0
- atomadic_forge/a0_qk_constants/config_defaults.py +38 -0
- atomadic_forge/a0_qk_constants/emergent_types.py +77 -0
- atomadic_forge/a0_qk_constants/error_codes.py +296 -0
- atomadic_forge/a0_qk_constants/forge_types.py +89 -0
- atomadic_forge/a0_qk_constants/gen_language.py +116 -0
- atomadic_forge/a0_qk_constants/lang_extensions.py +150 -0
- atomadic_forge/a0_qk_constants/policy_schema.py +48 -0
- atomadic_forge/a0_qk_constants/receipt_schema.py +311 -0
- atomadic_forge/a0_qk_constants/roi_constants.py +96 -0
- atomadic_forge/a0_qk_constants/semantic_types.py +61 -0
- atomadic_forge/a0_qk_constants/sidecar_schema.py +81 -0
- atomadic_forge/a0_qk_constants/synergy_types.py +62 -0
- atomadic_forge/a0_qk_constants/tier_names.py +47 -0
- atomadic_forge/a1_at_functions/__init__.py +1 -0
- atomadic_forge/a1_at_functions/agent_context_pack.py +193 -0
- atomadic_forge/a1_at_functions/agent_memory.py +139 -0
- atomadic_forge/a1_at_functions/agent_plan_emitter.py +324 -0
- atomadic_forge/a1_at_functions/agent_summary.py +277 -0
- atomadic_forge/a1_at_functions/body_extractor.py +306 -0
- atomadic_forge/a1_at_functions/card_renderer.py +210 -0
- atomadic_forge/a1_at_functions/certify_checks.py +445 -0
- atomadic_forge/a1_at_functions/chat_context.py +170 -0
- atomadic_forge/a1_at_functions/cherry_pick.py +71 -0
- atomadic_forge/a1_at_functions/classify_tier.py +115 -0
- atomadic_forge/a1_at_functions/commandsmith_discover.py +167 -0
- atomadic_forge/a1_at_functions/commandsmith_render.py +267 -0
- atomadic_forge/a1_at_functions/compiler_feedback.py +94 -0
- atomadic_forge/a1_at_functions/compliance_checker.py +228 -0
- atomadic_forge/a1_at_functions/config_io.py +68 -0
- atomadic_forge/a1_at_functions/cs1_renderer.py +588 -0
- atomadic_forge/a1_at_functions/doc_synthesizer.py +205 -0
- atomadic_forge/a1_at_functions/emergent_compose.py +192 -0
- atomadic_forge/a1_at_functions/emergent_rank.py +116 -0
- atomadic_forge/a1_at_functions/emergent_signature_extract.py +242 -0
- atomadic_forge/a1_at_functions/emergent_synthesize.py +88 -0
- atomadic_forge/a1_at_functions/enforce_planner.py +208 -0
- atomadic_forge/a1_at_functions/error_hints.py +105 -0
- atomadic_forge/a1_at_functions/evolution_log.py +94 -0
- atomadic_forge/a1_at_functions/forge_feedback.py +433 -0
- atomadic_forge/a1_at_functions/generation_quality.py +322 -0
- atomadic_forge/a1_at_functions/import_repair.py +211 -0
- atomadic_forge/a1_at_functions/import_smoke.py +102 -0
- atomadic_forge/a1_at_functions/js_parser.py +539 -0
- atomadic_forge/a1_at_functions/lineage_chain.py +144 -0
- atomadic_forge/a1_at_functions/lineage_reader.py +107 -0
- atomadic_forge/a1_at_functions/llm_client.py +554 -0
- atomadic_forge/a1_at_functions/local_signer.py +134 -0
- atomadic_forge/a1_at_functions/lsp_protocol.py +379 -0
- atomadic_forge/a1_at_functions/manifest_diff.py +314 -0
- atomadic_forge/a1_at_functions/mcp_protocol.py +1066 -0
- atomadic_forge/a1_at_functions/patch_scorer.py +267 -0
- atomadic_forge/a1_at_functions/plan_adapter.py +75 -0
- atomadic_forge/a1_at_functions/policy_loader.py +107 -0
- atomadic_forge/a1_at_functions/preflight_change.py +227 -0
- atomadic_forge/a1_at_functions/progress_reporter.py +81 -0
- atomadic_forge/a1_at_functions/provider_detect.py +157 -0
- atomadic_forge/a1_at_functions/provider_resolver.py +48 -0
- atomadic_forge/a1_at_functions/receipt_emitter.py +291 -0
- atomadic_forge/a1_at_functions/recipes.py +186 -0
- atomadic_forge/a1_at_functions/repo_explainer.py +124 -0
- atomadic_forge/a1_at_functions/roi_calculator.py +265 -0
- atomadic_forge/a1_at_functions/rollback_planner.py +147 -0
- atomadic_forge/a1_at_functions/sbom_emitter.py +155 -0
- atomadic_forge/a1_at_functions/scaffold_js.py +55 -0
- atomadic_forge/a1_at_functions/scaffold_pyproject.py +62 -0
- atomadic_forge/a1_at_functions/scaffold_starter.py +94 -0
- atomadic_forge/a1_at_functions/scout_walk.py +309 -0
- atomadic_forge/a1_at_functions/sidecar_parser.py +161 -0
- atomadic_forge/a1_at_functions/sidecar_validator.py +202 -0
- atomadic_forge/a1_at_functions/stub_detector.py +158 -0
- atomadic_forge/a1_at_functions/synergy_detect.py +166 -0
- atomadic_forge/a1_at_functions/synergy_render.py +252 -0
- atomadic_forge/a1_at_functions/synergy_surface_extract.py +163 -0
- atomadic_forge/a1_at_functions/test_runner.py +196 -0
- atomadic_forge/a1_at_functions/test_selector.py +122 -0
- atomadic_forge/a1_at_functions/tier_init_rebuild.py +122 -0
- atomadic_forge/a1_at_functions/tool_composer.py +130 -0
- atomadic_forge/a1_at_functions/transcript_log.py +70 -0
- atomadic_forge/a1_at_functions/wire_check.py +260 -0
- atomadic_forge/a2_mo_composites/__init__.py +1 -0
- atomadic_forge/a2_mo_composites/lineage_chain_store.py +122 -0
- atomadic_forge/a2_mo_composites/manifest_store.py +46 -0
- atomadic_forge/a2_mo_composites/plan_store.py +164 -0
- atomadic_forge/a2_mo_composites/receipt_signer.py +231 -0
- atomadic_forge/a3_og_features/__init__.py +1 -0
- atomadic_forge/a3_og_features/commandsmith_feature.py +267 -0
- atomadic_forge/a3_og_features/demo_packages/mixed_py_js/src/mixed_pkg/__init__.py +3 -0
- atomadic_forge/a3_og_features/demo_packages/mixed_py_js/src/mixed_pkg/a0_qk_constants/__init__.py +4 -0
- atomadic_forge/a3_og_features/demo_packages/mixed_py_js/src/mixed_pkg/a1_at_functions/__init__.py +14 -0
- atomadic_forge/a3_og_features/demo_packages/mixed_py_js/tests/conftest.py +10 -0
- atomadic_forge/a3_og_features/demo_packages/mixed_py_js/tests/test_mixed.py +18 -0
- atomadic_forge/a3_og_features/demo_runner.py +502 -0
- atomadic_forge/a3_og_features/emergent_feature.py +95 -0
- atomadic_forge/a3_og_features/emergent_pipeline_integration.py +154 -0
- atomadic_forge/a3_og_features/forge_enforce.py +107 -0
- atomadic_forge/a3_og_features/forge_evolve.py +176 -0
- atomadic_forge/a3_og_features/forge_loop.py +528 -0
- atomadic_forge/a3_og_features/forge_pipeline.py +295 -0
- atomadic_forge/a3_og_features/forge_plan_apply.py +222 -0
- atomadic_forge/a3_og_features/lsp_server.py +98 -0
- atomadic_forge/a3_og_features/mcp_server.py +160 -0
- atomadic_forge/a3_og_features/setup_wizard.py +337 -0
- atomadic_forge/a3_og_features/synergy_feature.py +65 -0
- atomadic_forge/a4_sy_orchestration/__init__.py +1 -0
- atomadic_forge/a4_sy_orchestration/cli.py +1284 -0
- atomadic_forge/commands/__init__.py +1 -0
- atomadic_forge/commands/_registry.py +36 -0
- atomadic_forge/commands/audit.py +142 -0
- atomadic_forge/commands/chat.py +133 -0
- atomadic_forge/commands/commandsmith.py +178 -0
- atomadic_forge/commands/config_cmd.py +145 -0
- atomadic_forge/commands/demo.py +142 -0
- atomadic_forge/commands/emergent.py +124 -0
- atomadic_forge/commands/emergent_then_synergy.py +70 -0
- atomadic_forge/commands/evolve.py +122 -0
- atomadic_forge/commands/evolve_then_iterate.py +70 -0
- atomadic_forge/commands/feature_then_emergent.py +111 -0
- atomadic_forge/commands/iterate.py +140 -0
- atomadic_forge/commands/synergy.py +96 -0
- atomadic_forge/commands/synergy_then_emergent.py +70 -0
- atomadic_forge-0.3.2.dist-info/METADATA +471 -0
- atomadic_forge-0.3.2.dist-info/RECORD +131 -0
- atomadic_forge-0.3.2.dist-info/WHEEL +5 -0
- atomadic_forge-0.3.2.dist-info/entry_points.txt +3 -0
- atomadic_forge-0.3.2.dist-info/licenses/LICENSE +15 -0
- atomadic_forge-0.3.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Tier a1 — append-only evolution log.
|
|
2
|
+
|
|
3
|
+
Every successful evolve / demo run appends a row to a shared
|
|
4
|
+
``.atomadic-forge/EVOLVE_LOG.md`` markdown table at the project root,
|
|
5
|
+
plus a JSONL line for machine consumption. Forge documents its own
|
|
6
|
+
history so operators can see every artifact that has ever been produced.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import datetime as _dt
|
|
12
|
+
import json
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
_TABLE_HEADER = (
|
|
17
|
+
"| When (UTC) | Preset / Intent | Package | LLM | Rounds | Trajectory | Final | Verdict |\n"
|
|
18
|
+
"|------------|-----------------|---------|-----|-------:|------------|------:|---------|"
|
|
19
|
+
)
|
|
20
|
+
_LOG_FILENAME = "EVOLVE_LOG.md"
|
|
21
|
+
_JSONL_FILENAME = "evolve_log.jsonl"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _ensure_log_dir(project_root: Path) -> Path:
|
|
25
|
+
d = project_root / ".atomadic-forge"
|
|
26
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
27
|
+
return d
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _intent_short(intent: str, max_len: int = 70) -> str:
|
|
31
|
+
one_line = " ".join(intent.split())
|
|
32
|
+
if len(one_line) <= max_len:
|
|
33
|
+
return one_line
|
|
34
|
+
return one_line[:max_len - 1] + "…"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def append_evolve_run(
|
|
38
|
+
*,
|
|
39
|
+
project_root: Path,
|
|
40
|
+
package: str,
|
|
41
|
+
intent: str,
|
|
42
|
+
llm_name: str,
|
|
43
|
+
rounds_completed: int,
|
|
44
|
+
score_trajectory: list[float],
|
|
45
|
+
final_score: float,
|
|
46
|
+
converged: bool,
|
|
47
|
+
halt_reason: str = "",
|
|
48
|
+
extra: dict[str, Any] | None = None,
|
|
49
|
+
) -> Path:
|
|
50
|
+
"""Append a row to the evolve log markdown + jsonl."""
|
|
51
|
+
log_dir = _ensure_log_dir(project_root)
|
|
52
|
+
md_path = log_dir / _LOG_FILENAME
|
|
53
|
+
jsonl_path = log_dir / _JSONL_FILENAME
|
|
54
|
+
ts = _dt.datetime.now(_dt.timezone.utc).isoformat(timespec="seconds")
|
|
55
|
+
arc = " → ".join(f"{s:.0f}" for s in score_trajectory)
|
|
56
|
+
verdict = (
|
|
57
|
+
"PASS" if (converged and final_score >= 75)
|
|
58
|
+
else "REFINE" if final_score >= 50
|
|
59
|
+
else "STAGNATED" if halt_reason == "stagnation"
|
|
60
|
+
else "FAIL"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
if not md_path.exists():
|
|
64
|
+
md_path.write_text(
|
|
65
|
+
"# Atomadic Forge — Evolution Log\n\n"
|
|
66
|
+
"Auto-appended by every `forge evolve` / `forge demo` run.\n\n"
|
|
67
|
+
f"{_TABLE_HEADER}\n",
|
|
68
|
+
encoding="utf-8",
|
|
69
|
+
)
|
|
70
|
+
row = (
|
|
71
|
+
f"| {ts} | {_intent_short(intent)} | `{package}` | `{llm_name}` "
|
|
72
|
+
f"| {rounds_completed} | `{arc}` | **{final_score:.0f}** | {verdict} |\n"
|
|
73
|
+
)
|
|
74
|
+
with md_path.open("a", encoding="utf-8") as f:
|
|
75
|
+
f.write(row)
|
|
76
|
+
|
|
77
|
+
entry = {
|
|
78
|
+
"schema_version": "atomadic-forge.evolve_log/v1",
|
|
79
|
+
"ts_utc": ts,
|
|
80
|
+
"package": package,
|
|
81
|
+
"intent": intent,
|
|
82
|
+
"llm": llm_name,
|
|
83
|
+
"rounds_completed": rounds_completed,
|
|
84
|
+
"score_trajectory": score_trajectory,
|
|
85
|
+
"final_score": final_score,
|
|
86
|
+
"converged": converged,
|
|
87
|
+
"halt_reason": halt_reason,
|
|
88
|
+
"verdict": verdict,
|
|
89
|
+
"extra": extra or {},
|
|
90
|
+
}
|
|
91
|
+
with jsonl_path.open("a", encoding="utf-8") as f:
|
|
92
|
+
f.write(json.dumps(entry, default=str) + "\n")
|
|
93
|
+
|
|
94
|
+
return md_path
|
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
"""Tier a1 — pure feedback packer.
|
|
2
|
+
|
|
3
|
+
Takes the structured output of wire / certify / emergent / scout and packs
|
|
4
|
+
it into a markdown-shaped string that an LLM can act on. Forge feeds this
|
|
5
|
+
back to the LLM each iteration of the generate-enforce loop.
|
|
6
|
+
|
|
7
|
+
The packing is designed to make the LLM's job easy:
|
|
8
|
+
* concrete file paths
|
|
9
|
+
* exact line content of violating imports
|
|
10
|
+
* explicit "fix this by …" hints when the rule is mechanical
|
|
11
|
+
* a single ``CHANGE_REQUEST`` block at the bottom describing what to emit next
|
|
12
|
+
|
|
13
|
+
System prompts come in language-specific variants (Python / JavaScript /
|
|
14
|
+
TypeScript) — the structural feedback (wire violations, score gaps, reuse
|
|
15
|
+
ratio) is language-agnostic, but the emit-format and import-style
|
|
16
|
+
instructions differ per language.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
_SYSTEM_PROMPT_PYTHON = """\
|
|
24
|
+
You are a code-generation engine constrained by Atomadic Forge's 5-tier
|
|
25
|
+
monadic architecture.
|
|
26
|
+
|
|
27
|
+
Forge has ALREADY scaffolded the surrounding package for you:
|
|
28
|
+
- ``pyproject.toml`` (PEP-621, with a console_script entry pointing at
|
|
29
|
+
``a4_sy_orchestration.cli:main``)
|
|
30
|
+
- ``README.md`` describing the intent and layout
|
|
31
|
+
- ``.gitignore``
|
|
32
|
+
- ``tests/`` directory with ``conftest.py`` adding ``src/`` to ``sys.path``
|
|
33
|
+
- All five tier directories with ``__init__.py``
|
|
34
|
+
|
|
35
|
+
You should focus on emitting the actual ``.py`` files (and ``test_*.py``
|
|
36
|
+
files in ``tests/``). DO NOT re-emit pyproject.toml or README.md — they
|
|
37
|
+
are already correct.
|
|
38
|
+
|
|
39
|
+
The 5-tier law (compose UPWARD only):
|
|
40
|
+
a0_qk_constants — constants, enums, TypedDicts. Imports nothing.
|
|
41
|
+
a1_at_functions — pure stateless functions. Imports a0 only.
|
|
42
|
+
a2_mo_composites — stateful classes. Imports a0, a1.
|
|
43
|
+
a3_og_features — feature orchestrators. Imports a0, a1, a2.
|
|
44
|
+
a4_sy_orchestration — CLI / entry points. Imports a0–a3.
|
|
45
|
+
|
|
46
|
+
When you emit code, you MUST:
|
|
47
|
+
1. Place each new file under the correct ``a{N}_*/`` directory.
|
|
48
|
+
2. Never import upward (lower tier importing from higher tier).
|
|
49
|
+
3. One responsibility per file.
|
|
50
|
+
4. Module docstrings of the form: \"\"\"Tier aN — <one-line>.\"\"\"
|
|
51
|
+
5. Write COMPLETE function bodies — no `pass`, no `NotImplementedError`,
|
|
52
|
+
no `# TODO`, no `# Implement me!`. If you don't know how to implement
|
|
53
|
+
something, choose a simpler design rather than emit a stub.
|
|
54
|
+
6. Use ABSOLUTE imports rooted at the package: `from <pkg>.a1_at_functions.foo
|
|
55
|
+
import foo` — not relative imports, not bare names.
|
|
56
|
+
7. Output as a JSON array of file objects ONLY — no prose, no fences:
|
|
57
|
+
[{"path": "src/<pkg>/aN_…/foo.py", "content": "…"}, ...]
|
|
58
|
+
8. SUBSTITUTE the actual package name into every `<pkg>` placeholder.
|
|
59
|
+
9. Emit at least one ``test_*.py`` file under ``tests/`` exercising the
|
|
60
|
+
public callables you just wrote. Use simple ``assert`` statements
|
|
61
|
+
and import via the absolute package path.
|
|
62
|
+
|
|
63
|
+
Example (intent: "calc with add() at a1, CLI at a4", package: "calc"):
|
|
64
|
+
|
|
65
|
+
[
|
|
66
|
+
{"path": "src/calc/a1_at_functions/add.py",
|
|
67
|
+
"content": "\\"\\"\\"Tier a1 — pure addition.\\"\\"\\"\\n\\ndef add(a: int, b: int) -> int:\\n return a + b\\n"},
|
|
68
|
+
{"path": "src/calc/a4_sy_orchestration/cli.py",
|
|
69
|
+
"content": "\\"\\"\\"Tier a4 — CLI entry.\\"\\"\\"\\nimport argparse\\nfrom calc.a1_at_functions.add import add\\n\\ndef main():\\n p = argparse.ArgumentParser()\\n p.add_argument('a', type=int); p.add_argument('b', type=int)\\n args = p.parse_args()\\n print(add(args.a, args.b))\\n"}
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
Forge then materialises your output, runs wire + certify + import-smoke,
|
|
73
|
+
and feeds any violations back to you on the next turn. An importable
|
|
74
|
+
package with real bodies scores high; stubs and broken imports lose points.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
_SYSTEM_PROMPT_JS = """\
|
|
79
|
+
You are a code-generation engine constrained by Atomadic Forge's 5-tier
|
|
80
|
+
monadic architecture, emitting JavaScript (ES modules) for Cloudflare
|
|
81
|
+
Workers / Node 20+ / browsers.
|
|
82
|
+
|
|
83
|
+
Forge has ALREADY scaffolded the surrounding package for you:
|
|
84
|
+
- ``package.json`` with ``"type": "module"`` so ES6 imports resolve
|
|
85
|
+
- ``README.md`` describing the intent and layout
|
|
86
|
+
- ``.gitignore``
|
|
87
|
+
- All five tier directories (no ``__init__.py`` — ES modules don't need them)
|
|
88
|
+
|
|
89
|
+
You should focus on emitting the actual ``.js`` files. DO NOT re-emit
|
|
90
|
+
``package.json`` or ``README.md`` — they are already correct.
|
|
91
|
+
|
|
92
|
+
The 5-tier law (compose UPWARD only):
|
|
93
|
+
a0_qk_constants — exported constants, enums, type-shape JSDoc. Imports nothing.
|
|
94
|
+
a1_at_functions — pure stateless functions. Imports a0 only.
|
|
95
|
+
a2_mo_composites — stateful classes / clients / stores. Imports a0, a1.
|
|
96
|
+
a3_og_features — feature orchestrators. Imports a0, a1, a2.
|
|
97
|
+
a4_sy_orchestration — Worker entry / CLI / top-level dispatch. Imports a0–a3.
|
|
98
|
+
|
|
99
|
+
When you emit code, you MUST:
|
|
100
|
+
1. Place each new file under the correct ``a{N}_*/`` directory.
|
|
101
|
+
2. Never import upward (lower tier importing from higher tier).
|
|
102
|
+
3. One responsibility per file.
|
|
103
|
+
4. File-leading docstring comment of the form: ``// Tier aN — <one-line>.``
|
|
104
|
+
5. Write COMPLETE function bodies — no `throw new Error("not impl")`,
|
|
105
|
+
no `// TODO`, no empty bodies that just return undefined. If you
|
|
106
|
+
don't know how to implement something, choose a simpler design.
|
|
107
|
+
6. Use ES module syntax — ``import { x } from "./other.js"`` and
|
|
108
|
+
``export function …`` / ``export const …`` / ``export default …``.
|
|
109
|
+
Cross-tier imports MUST include the full relative path with the
|
|
110
|
+
``.js`` extension: ``import { foo } from "../a1_at_functions/foo.js"``.
|
|
111
|
+
No bare specifiers, no CommonJS ``require()``, no default-only exports
|
|
112
|
+
for libraries (default-export the Worker handler at a4 ONLY).
|
|
113
|
+
7. Output as a JSON array of file objects ONLY — no prose, no fences:
|
|
114
|
+
[{"path": "<pkg>/aN_…/foo.js", "content": "…"}, ...]
|
|
115
|
+
8. SUBSTITUTE the actual package name into every `<pkg>` placeholder.
|
|
116
|
+
Note: there is NO ``src/`` prefix — the package directory sits at
|
|
117
|
+
the output root.
|
|
118
|
+
9. For a Cloudflare Worker, the a4 entry file exports a ``default``
|
|
119
|
+
object with ``fetch(request, env, ctx)`` and/or ``scheduled(event,
|
|
120
|
+
env, ctx)``. Helpers it calls live in a1–a3.
|
|
121
|
+
|
|
122
|
+
Example (intent: "counter with increment() at a1, Worker at a4", package: "counter"):
|
|
123
|
+
|
|
124
|
+
[
|
|
125
|
+
{"path": "counter/a1_at_functions/increment.js",
|
|
126
|
+
"content": "// Tier a1 — pure increment.\\nexport function increment(n) { return n + 1; }\\n"},
|
|
127
|
+
{"path": "counter/a4_sy_orchestration/worker.js",
|
|
128
|
+
"content": "// Tier a4 — Worker entry.\\nimport { increment } from \\"../a1_at_functions/increment.js\\";\\nexport default {\\n async fetch(request) {\\n const url = new URL(request.url);\\n const n = Number(url.searchParams.get(\\"n\\")) || 0;\\n return new Response(String(increment(n)));\\n }\\n};\\n"}
|
|
129
|
+
]
|
|
130
|
+
|
|
131
|
+
Forge then materialises your output, runs wire + certify, and feeds any
|
|
132
|
+
violations back to you on the next turn. Clean ES-module imports score
|
|
133
|
+
high; broken imports and upward-tier violations lose points.
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def system_prompt(language: str = "python") -> str:
|
|
138
|
+
"""Return the language-appropriate system prompt for the LLM.
|
|
139
|
+
|
|
140
|
+
``language`` accepts ``"python"`` (default), ``"javascript"``, or
|
|
141
|
+
``"typescript"`` (typescript reuses the JS prompt today; tsconfig
|
|
142
|
+
polish is on the 0.3 roadmap).
|
|
143
|
+
"""
|
|
144
|
+
if language in ("javascript", "typescript"):
|
|
145
|
+
return _SYSTEM_PROMPT_JS
|
|
146
|
+
return _SYSTEM_PROMPT_PYTHON
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def pack_initial_intent(intent: str, *, package: str = "absorbed",
|
|
150
|
+
seed_catalog: list[dict] | None = None,
|
|
151
|
+
language: str = "python") -> str:
|
|
152
|
+
"""Build the first-turn prompt: the user's intent + (optional) seed material."""
|
|
153
|
+
if language in ("javascript", "typescript"):
|
|
154
|
+
target_path = f"`{package}/` — emit files under the 5-tier layout below."
|
|
155
|
+
else:
|
|
156
|
+
target_path = f"`src/{package}/` — emit files under the 5-tier layout below."
|
|
157
|
+
parts = [
|
|
158
|
+
"# Intent",
|
|
159
|
+
"",
|
|
160
|
+
intent.strip(),
|
|
161
|
+
"",
|
|
162
|
+
"# Target package",
|
|
163
|
+
target_path,
|
|
164
|
+
"",
|
|
165
|
+
]
|
|
166
|
+
if seed_catalog:
|
|
167
|
+
# Deduplicate by qualname and prefer top-level symbols (no dots except class.method).
|
|
168
|
+
seen: set[str] = set()
|
|
169
|
+
unique_catalog: list[dict] = []
|
|
170
|
+
for s in seed_catalog:
|
|
171
|
+
qn = s.get("qualname", "")
|
|
172
|
+
# Skip method-level duplicates; prefer class-level or module-level.
|
|
173
|
+
base = qn.split(".")[0] if "." in qn else qn
|
|
174
|
+
if base not in seen:
|
|
175
|
+
seen.add(base)
|
|
176
|
+
unique_catalog.append(s)
|
|
177
|
+
MAX_SEEDS = 30
|
|
178
|
+
parts.append(f"# Available building blocks ({len(seed_catalog)} symbols — {len(unique_catalog)} unique top-level; showing {min(MAX_SEEDS, len(unique_catalog))})")
|
|
179
|
+
parts.append("")
|
|
180
|
+
for s in unique_catalog[:MAX_SEEDS]:
|
|
181
|
+
parts.append(
|
|
182
|
+
f"- `{s.get('qualname','?')}` "
|
|
183
|
+
f"(tier `{s.get('tier_guess','?')}`, "
|
|
184
|
+
f"effects {s.get('effects', [])})"
|
|
185
|
+
)
|
|
186
|
+
if len(unique_catalog) > MAX_SEEDS:
|
|
187
|
+
parts.append(f"- … {len(unique_catalog) - MAX_SEEDS} more unique symbols available")
|
|
188
|
+
parts.append("")
|
|
189
|
+
if language in ("javascript", "typescript"):
|
|
190
|
+
parts.append("Reuse these where possible — import from the "
|
|
191
|
+
f"corresponding `{package}/aN_*/<file>.js` paths "
|
|
192
|
+
"with relative ES-module specifiers.")
|
|
193
|
+
else:
|
|
194
|
+
parts.append("Reuse these where possible by importing from "
|
|
195
|
+
f"`{package}.aN_…` paths.")
|
|
196
|
+
parts.append("")
|
|
197
|
+
parts.extend([
|
|
198
|
+
"# CHANGE_REQUEST",
|
|
199
|
+
"",
|
|
200
|
+
"Emit the initial set of files needed to satisfy the intent. Output",
|
|
201
|
+
"a JSON array of `{path, content}` objects only — no prose around it.",
|
|
202
|
+
])
|
|
203
|
+
return "\n".join(parts)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def compute_reuse_stats(scout_report: dict[str, Any] | None,
|
|
207
|
+
seed_catalog: list[dict] | None) -> dict[str, Any]:
|
|
208
|
+
"""Compute how much of the LLM's emitted symbols overlap with the seed catalog.
|
|
209
|
+
|
|
210
|
+
A high reuse ratio = the LLM correctly composed existing pieces. A low
|
|
211
|
+
reuse ratio = it generated from scratch when it should have been
|
|
212
|
+
importing. This is the soft signal that distinguishes Forge's loop
|
|
213
|
+
from Cursor / Devin / etc. — none of them feed reuse stats back to the
|
|
214
|
+
generator.
|
|
215
|
+
"""
|
|
216
|
+
if not scout_report or not seed_catalog:
|
|
217
|
+
return {"reuse_ratio": 0.0, "novel_symbols": [],
|
|
218
|
+
"reused_symbols": [], "available_unused": []}
|
|
219
|
+
available = {s["qualname"]: s for s in seed_catalog}
|
|
220
|
+
emitted = {s["qualname"]: s for s in scout_report.get("symbols", [])}
|
|
221
|
+
reused = sorted(emitted.keys() & available.keys())
|
|
222
|
+
novel = sorted(set(emitted.keys()) - set(available.keys()))
|
|
223
|
+
available_unused = sorted(set(available.keys()) - set(emitted.keys()))
|
|
224
|
+
total_emitted = max(1, len(emitted))
|
|
225
|
+
return {
|
|
226
|
+
"reuse_ratio": round(len(reused) / total_emitted, 3),
|
|
227
|
+
"reused_symbols": reused[:20],
|
|
228
|
+
"novel_symbols": novel[:20],
|
|
229
|
+
"available_unused": available_unused[:20],
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def pack_feedback(*, wire_report: dict[str, Any] | None = None,
|
|
234
|
+
certify_report: dict[str, Any] | None = None,
|
|
235
|
+
emergent_overlay: dict[str, Any] | None = None,
|
|
236
|
+
reuse_stats: dict[str, Any] | None = None,
|
|
237
|
+
iteration: int = 0) -> str:
|
|
238
|
+
"""Pack the next-turn prompt: violations + score gaps + emergent + reuse.
|
|
239
|
+
|
|
240
|
+
The 3-way constraint-satisfaction signal that distinguishes Forge's loop
|
|
241
|
+
from Cursor / Devin / Cognition: hard constraint (wire), score gap
|
|
242
|
+
(certify), and compositional signal (emergent + reuse).
|
|
243
|
+
"""
|
|
244
|
+
parts = [f"# Forge feedback (iteration {iteration})", ""]
|
|
245
|
+
|
|
246
|
+
if wire_report:
|
|
247
|
+
v_count = wire_report.get("violation_count", 0)
|
|
248
|
+
verdict = wire_report.get("verdict", "?")
|
|
249
|
+
parts.append(f"## Wire scan: **{verdict}** ({v_count} violation(s))")
|
|
250
|
+
parts.append("")
|
|
251
|
+
for v in wire_report.get("violations", [])[:20]:
|
|
252
|
+
parts.append(
|
|
253
|
+
f"- `{v['file']}` imports `{v['imported']}` "
|
|
254
|
+
f"from tier `{v['to_tier']}` while sitting at tier "
|
|
255
|
+
f"`{v['from_tier']}` — **upward import, illegal**."
|
|
256
|
+
)
|
|
257
|
+
if v_count == 0:
|
|
258
|
+
parts.append("All imports compose upward only — no fixes needed here.")
|
|
259
|
+
parts.append("")
|
|
260
|
+
|
|
261
|
+
if certify_report:
|
|
262
|
+
score = certify_report.get("score", 0)
|
|
263
|
+
parts.append(f"## Certify score: **{score}/100**")
|
|
264
|
+
parts.append("")
|
|
265
|
+
for issue in certify_report.get("issues", []):
|
|
266
|
+
parts.append(f"- {issue}")
|
|
267
|
+
for rec in certify_report.get("recommendations", []):
|
|
268
|
+
parts.append(f" → {rec}")
|
|
269
|
+
# Per-file stub callouts — actionable feedback the LLM can target.
|
|
270
|
+
stubs = (certify_report.get("detail") or {}).get("stubs") or {}
|
|
271
|
+
findings = stubs.get("findings") or []
|
|
272
|
+
if findings:
|
|
273
|
+
parts.append("")
|
|
274
|
+
parts.append("**Stub bodies — replace with real implementations:**")
|
|
275
|
+
for f in findings[:10]:
|
|
276
|
+
parts.append(
|
|
277
|
+
f" · `{f['file']}` line {f['lineno']} → "
|
|
278
|
+
f"`{f['qualname']}` ({f['kind']}): `{f['excerpt']}`"
|
|
279
|
+
)
|
|
280
|
+
# Runtime import error — paste the actual traceback so the LLM
|
|
281
|
+
# can fix the exact failing import / syntax / path issue.
|
|
282
|
+
smoke = (certify_report.get("detail") or {}).get("import_smoke")
|
|
283
|
+
if smoke and not smoke.get("importable", True):
|
|
284
|
+
parts.append("")
|
|
285
|
+
parts.append("**Runtime import FAILED — package does not load:**")
|
|
286
|
+
parts.append(f" · error: `{smoke.get('error_kind')}` — "
|
|
287
|
+
f"{smoke.get('error_message')}")
|
|
288
|
+
tb = smoke.get("traceback_excerpt") or ""
|
|
289
|
+
if tb:
|
|
290
|
+
parts.append("")
|
|
291
|
+
parts.append("```text")
|
|
292
|
+
parts.append(tb.strip())
|
|
293
|
+
parts.append("```")
|
|
294
|
+
parts.append("")
|
|
295
|
+
parts.append("Fix the import error. Common causes:")
|
|
296
|
+
parts.append(" - missing module file or wrong tier directory")
|
|
297
|
+
parts.append(" - relative import path uses old/wrong package name")
|
|
298
|
+
parts.append(" - syntax error in an emitted file")
|
|
299
|
+
|
|
300
|
+
# Behavioral test failures — the breakthrough signal. Identity-
|
|
301
|
+
# function stubs pass wire+import but fail tests. Pasting the
|
|
302
|
+
# pytest output lets the LLM see exactly which assertion broke.
|
|
303
|
+
test_run = (certify_report.get("detail") or {}).get("test_run")
|
|
304
|
+
if test_run and test_run.get("ran") and test_run.get("failed"):
|
|
305
|
+
parts.append("")
|
|
306
|
+
parts.append(
|
|
307
|
+
f"**Tests FAILED: {test_run['failed']} of "
|
|
308
|
+
f"{test_run['total']} "
|
|
309
|
+
f"({test_run.get('pass_ratio', 0):.0%} pass-ratio)**"
|
|
310
|
+
)
|
|
311
|
+
for fid in (test_run.get("failure_excerpts") or [])[:5]:
|
|
312
|
+
parts.append(f" · `{fid}`")
|
|
313
|
+
summary = (test_run.get("pytest_summary") or "").strip()
|
|
314
|
+
if summary:
|
|
315
|
+
parts.append("")
|
|
316
|
+
parts.append("```text")
|
|
317
|
+
parts.append(summary[:1500])
|
|
318
|
+
parts.append("```")
|
|
319
|
+
parts.append("")
|
|
320
|
+
parts.append("Tests are the behavior gate. An identity-function "
|
|
321
|
+
"implementation (e.g. `def f(x): return x`) passes "
|
|
322
|
+
"wire and import but fails its own tests. Replace "
|
|
323
|
+
"stubs with implementations that satisfy the "
|
|
324
|
+
"assertions.")
|
|
325
|
+
parts.append("")
|
|
326
|
+
|
|
327
|
+
if reuse_stats:
|
|
328
|
+
ratio = reuse_stats.get("reuse_ratio", 0.0)
|
|
329
|
+
unused = reuse_stats.get("available_unused", [])
|
|
330
|
+
parts.append(f"## Reuse signal: **{ratio:.0%}** of emitted symbols "
|
|
331
|
+
"match the seed catalog")
|
|
332
|
+
if ratio < 0.3 and unused:
|
|
333
|
+
parts.append("")
|
|
334
|
+
parts.append("**Low reuse — these existing symbols would likely "
|
|
335
|
+
"satisfy your needs without re-implementation:**")
|
|
336
|
+
for q in unused[:8]:
|
|
337
|
+
parts.append(f"- `{q}`")
|
|
338
|
+
parts.append("")
|
|
339
|
+
parts.append("Compose existing symbols by importing them; only "
|
|
340
|
+
"emit new code where no existing symbol fits.")
|
|
341
|
+
parts.append("")
|
|
342
|
+
|
|
343
|
+
if emergent_overlay and emergent_overlay.get("candidates"):
|
|
344
|
+
parts.append("## Emergent compositions (chains you could wire)")
|
|
345
|
+
parts.append("")
|
|
346
|
+
for c in emergent_overlay["candidates"][:5]:
|
|
347
|
+
chain_str = " → ".join(c["chain"]["chain"][:4])
|
|
348
|
+
parts.append(
|
|
349
|
+
f"- **{c['name']}** (score {c['score']:.0f}): `{chain_str}`"
|
|
350
|
+
)
|
|
351
|
+
parts.append("")
|
|
352
|
+
|
|
353
|
+
parts.extend([
|
|
354
|
+
"# CHANGE_REQUEST",
|
|
355
|
+
"",
|
|
356
|
+
"Fix the violations and score gaps above. Output a JSON array of",
|
|
357
|
+
"`{path, content}` objects representing files to write or overwrite.",
|
|
358
|
+
"Prefer composing existing symbols over emitting new ones. Empty",
|
|
359
|
+
"array means you have finished.",
|
|
360
|
+
])
|
|
361
|
+
return "\n".join(parts)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def parse_files_from_response(response: str) -> list[dict[str, str]]:
|
|
365
|
+
"""Best-effort: pull ``[{"path": …, "content": …}, …]`` out of an LLM reply.
|
|
366
|
+
|
|
367
|
+
Tolerant of:
|
|
368
|
+
- Triple-backtick fences (```json ... ```)
|
|
369
|
+
- Prose before/after the JSON array
|
|
370
|
+
- Truncated responses (extracts complete objects before the cutoff)
|
|
371
|
+
Returns [] when no valid file dicts are found.
|
|
372
|
+
"""
|
|
373
|
+
import json as _json
|
|
374
|
+
import re
|
|
375
|
+
|
|
376
|
+
text = response.strip()
|
|
377
|
+
|
|
378
|
+
# Strategy 1: find JSON inside a code fence (greedy match for full array).
|
|
379
|
+
fence = re.search(r"```(?:json|python)?\s*(\[[\s\S]*?\])\s*```", text, re.DOTALL)
|
|
380
|
+
if fence:
|
|
381
|
+
candidate = fence.group(1)
|
|
382
|
+
try:
|
|
383
|
+
data = _json.loads(candidate)
|
|
384
|
+
if isinstance(data, list):
|
|
385
|
+
return [e for e in data
|
|
386
|
+
if isinstance(e, dict)
|
|
387
|
+
and isinstance(e.get("path"), str)
|
|
388
|
+
and isinstance(e.get("content"), str)]
|
|
389
|
+
except _json.JSONDecodeError:
|
|
390
|
+
pass
|
|
391
|
+
|
|
392
|
+
# Strategy 2: balanced-bracket scan starting at first '['.
|
|
393
|
+
start = text.find("[")
|
|
394
|
+
if start != -1:
|
|
395
|
+
depth = 0
|
|
396
|
+
end = -1
|
|
397
|
+
for i, ch in enumerate(text[start:], start=start):
|
|
398
|
+
if ch == "[":
|
|
399
|
+
depth += 1
|
|
400
|
+
elif ch == "]":
|
|
401
|
+
depth -= 1
|
|
402
|
+
if depth == 0:
|
|
403
|
+
end = i
|
|
404
|
+
break
|
|
405
|
+
if end != -1:
|
|
406
|
+
try:
|
|
407
|
+
data = _json.loads(text[start:end + 1])
|
|
408
|
+
if isinstance(data, list):
|
|
409
|
+
out = [e for e in data
|
|
410
|
+
if isinstance(e, dict)
|
|
411
|
+
and isinstance(e.get("path"), str)
|
|
412
|
+
and isinstance(e.get("content"), str)]
|
|
413
|
+
if out:
|
|
414
|
+
return out
|
|
415
|
+
except _json.JSONDecodeError:
|
|
416
|
+
pass
|
|
417
|
+
|
|
418
|
+
# Strategy 3: tolerant extraction — grab every complete {"path":…,"content":…} object
|
|
419
|
+
# even from a truncated/malformed array. Uses a regex to find object boundaries.
|
|
420
|
+
objects = []
|
|
421
|
+
# Match objects that have at least "path" and "content" keys.
|
|
422
|
+
obj_pattern = re.compile(
|
|
423
|
+
r'\{\s*"path"\s*:\s*"((?:[^"\\]|\\.)*)"\s*,\s*"content"\s*:\s*"((?:[^"\\]|\\.)*?)"\s*\}',
|
|
424
|
+
re.DOTALL,
|
|
425
|
+
)
|
|
426
|
+
for m in obj_pattern.finditer(text):
|
|
427
|
+
try:
|
|
428
|
+
path = _json.loads(f'"{m.group(1)}"')
|
|
429
|
+
content = _json.loads(f'"{m.group(2)}"')
|
|
430
|
+
objects.append({"path": path, "content": content})
|
|
431
|
+
except _json.JSONDecodeError:
|
|
432
|
+
continue
|
|
433
|
+
return objects
|