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,267 @@
|
|
|
1
|
+
"""Tier a1 — pure unified-diff risk scorer.
|
|
2
|
+
|
|
3
|
+
Codex's 'Copilot's Copilot' primitive #3:
|
|
4
|
+
|
|
5
|
+
> score_patch({diff}) — returns architecture risk, test risk,
|
|
6
|
+
> public API risk, release risk, 'needs human review?' boolean,
|
|
7
|
+
> suggested validation commands. Turns Forge into a PR reviewer
|
|
8
|
+
> before the PR exists.
|
|
9
|
+
|
|
10
|
+
Pure: parses a unified-diff string, classifies per-file changes,
|
|
11
|
+
returns a structured risk report. Heuristic — false positives are
|
|
12
|
+
expected; the value is the structured prompt for the agent to
|
|
13
|
+
think about before applying the diff.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import re
|
|
18
|
+
from typing import TypedDict
|
|
19
|
+
|
|
20
|
+
SCHEMA_VERSION_PATCH_SCORE_V1 = "atomadic-forge.patch_score/v1"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
_FILE_HEADER_RE = re.compile(r"^\+\+\+ b/(.+?)(?:\t|\s|$)", re.MULTILINE)
|
|
24
|
+
_TIER_NAMES = (
|
|
25
|
+
"a0_qk_constants", "a1_at_functions", "a2_mo_composites",
|
|
26
|
+
"a3_og_features", "a4_sy_orchestration",
|
|
27
|
+
)
|
|
28
|
+
_PUBLIC_API_PATHS = ("__init__.py",)
|
|
29
|
+
_RELEASE_FILES = (
|
|
30
|
+
"pyproject.toml", "setup.py", "setup.cfg", "package.json",
|
|
31
|
+
"Cargo.toml", "version.py", "VERSION", "_version.py",
|
|
32
|
+
"CHANGELOG.md", "LICENSE",
|
|
33
|
+
)
|
|
34
|
+
_FORBIDDEN_BY_TIER: dict[str, tuple[str, ...]] = {
|
|
35
|
+
"a0_qk_constants": (
|
|
36
|
+
"a1_at_functions", "a2_mo_composites",
|
|
37
|
+
"a3_og_features", "a4_sy_orchestration",
|
|
38
|
+
),
|
|
39
|
+
"a1_at_functions": (
|
|
40
|
+
"a2_mo_composites", "a3_og_features", "a4_sy_orchestration",
|
|
41
|
+
),
|
|
42
|
+
"a2_mo_composites": ("a3_og_features", "a4_sy_orchestration"),
|
|
43
|
+
"a3_og_features": ("a4_sy_orchestration",),
|
|
44
|
+
"a4_sy_orchestration": (),
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class PatchFileScore(TypedDict, total=False):
|
|
49
|
+
path: str
|
|
50
|
+
detected_tier: str | None
|
|
51
|
+
added_lines: int
|
|
52
|
+
removed_lines: int
|
|
53
|
+
architectural_risk: bool
|
|
54
|
+
public_api_risk: bool
|
|
55
|
+
release_risk: bool
|
|
56
|
+
notes: list[str]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class PatchScore(TypedDict, total=False):
|
|
60
|
+
schema_version: str
|
|
61
|
+
file_count: int
|
|
62
|
+
total_added: int
|
|
63
|
+
total_removed: int
|
|
64
|
+
architectural_risk: bool
|
|
65
|
+
test_risk: bool
|
|
66
|
+
public_api_risk: bool
|
|
67
|
+
release_risk: bool
|
|
68
|
+
needs_human_review: bool
|
|
69
|
+
files: list[PatchFileScore]
|
|
70
|
+
suggested_validation_commands: list[str]
|
|
71
|
+
notes: list[str]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _split_diff_per_file(diff: str) -> list[tuple[str, str]]:
|
|
75
|
+
"""Return [(path, file_diff_text), ...] for a unified diff."""
|
|
76
|
+
chunks: list[tuple[str, str]] = []
|
|
77
|
+
current_path: str | None = None
|
|
78
|
+
current: list[str] = []
|
|
79
|
+
for line in diff.splitlines():
|
|
80
|
+
if line.startswith("+++ b/"):
|
|
81
|
+
# Flush prior chunk
|
|
82
|
+
if current_path is not None and current:
|
|
83
|
+
chunks.append((current_path, "\n".join(current)))
|
|
84
|
+
current_path = line[len("+++ b/"):].split("\t", 1)[0].strip()
|
|
85
|
+
current = []
|
|
86
|
+
else:
|
|
87
|
+
current.append(line)
|
|
88
|
+
if current_path is not None and current:
|
|
89
|
+
chunks.append((current_path, "\n".join(current)))
|
|
90
|
+
return chunks
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _detect_tier(path: str) -> str | None:
|
|
94
|
+
for part in path.split("/"):
|
|
95
|
+
if part in _TIER_NAMES:
|
|
96
|
+
return part
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _check_added_imports_against_tier(
|
|
101
|
+
path: str, tier: str | None, diff_text: str,
|
|
102
|
+
) -> list[str]:
|
|
103
|
+
"""Look at '+' lines for upward imports against the file's tier."""
|
|
104
|
+
if not tier:
|
|
105
|
+
return []
|
|
106
|
+
forbidden = _FORBIDDEN_BY_TIER.get(tier, ())
|
|
107
|
+
if not forbidden:
|
|
108
|
+
return []
|
|
109
|
+
issues: list[str] = []
|
|
110
|
+
for raw in diff_text.splitlines():
|
|
111
|
+
if not raw.startswith("+") or raw.startswith("+++"):
|
|
112
|
+
continue
|
|
113
|
+
body = raw[1:].lstrip()
|
|
114
|
+
if not body.startswith(("import ", "from ")):
|
|
115
|
+
continue
|
|
116
|
+
for f in forbidden:
|
|
117
|
+
if f in body:
|
|
118
|
+
issues.append(
|
|
119
|
+
f"new import in {path} references forbidden tier "
|
|
120
|
+
f"{f!r}: {body[:120]}"
|
|
121
|
+
)
|
|
122
|
+
break
|
|
123
|
+
return issues
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _file_score(path: str, diff_text: str) -> PatchFileScore:
|
|
127
|
+
added = sum(1 for line in diff_text.splitlines()
|
|
128
|
+
if line.startswith("+") and not line.startswith("+++"))
|
|
129
|
+
removed = sum(1 for line in diff_text.splitlines()
|
|
130
|
+
if line.startswith("-") and not line.startswith("---"))
|
|
131
|
+
tier = _detect_tier(path)
|
|
132
|
+
notes: list[str] = []
|
|
133
|
+
arch_risk = False
|
|
134
|
+
api_risk = False
|
|
135
|
+
rel_risk = False
|
|
136
|
+
|
|
137
|
+
upward = _check_added_imports_against_tier(path, tier, diff_text)
|
|
138
|
+
if upward:
|
|
139
|
+
arch_risk = True
|
|
140
|
+
notes.extend(upward)
|
|
141
|
+
|
|
142
|
+
if path.endswith(_PUBLIC_API_PATHS):
|
|
143
|
+
api_risk = True
|
|
144
|
+
notes.append(
|
|
145
|
+
f"{path} is a public-export surface; an unintended "
|
|
146
|
+
"deletion can break downstream callers"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
base = path.split("/")[-1]
|
|
150
|
+
if base in _RELEASE_FILES or path.endswith(_RELEASE_FILES):
|
|
151
|
+
rel_risk = True
|
|
152
|
+
notes.append(
|
|
153
|
+
f"{path} is a release-control file; review version bump "
|
|
154
|
+
"+ changelog impact before merging"
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
return PatchFileScore(
|
|
158
|
+
path=path,
|
|
159
|
+
detected_tier=tier,
|
|
160
|
+
added_lines=added,
|
|
161
|
+
removed_lines=removed,
|
|
162
|
+
architectural_risk=arch_risk,
|
|
163
|
+
public_api_risk=api_risk,
|
|
164
|
+
release_risk=rel_risk,
|
|
165
|
+
notes=notes,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def score_patch(diff: str, *, project_root: object | None = None) -> PatchScore:
|
|
170
|
+
"""Score a unified-diff string. Returns the patch_score/v1 shape.
|
|
171
|
+
|
|
172
|
+
Empty or unparseable diffs return a low-risk report with a note
|
|
173
|
+
instead of raising — the agent should never see a traceback when
|
|
174
|
+
asking 'is this safe?'.
|
|
175
|
+
|
|
176
|
+
Codex-6: when ``project_root`` is provided AND that project has a
|
|
177
|
+
[tool.forge.agent] policy with protected_files, any diff touching
|
|
178
|
+
a protected file is flagged as ``needs_human_review``.
|
|
179
|
+
"""
|
|
180
|
+
if not diff or not diff.strip():
|
|
181
|
+
return PatchScore(
|
|
182
|
+
schema_version=SCHEMA_VERSION_PATCH_SCORE_V1,
|
|
183
|
+
file_count=0, total_added=0, total_removed=0,
|
|
184
|
+
architectural_risk=False, test_risk=False,
|
|
185
|
+
public_api_risk=False, release_risk=False,
|
|
186
|
+
needs_human_review=False, files=[],
|
|
187
|
+
suggested_validation_commands=[],
|
|
188
|
+
notes=["empty diff — nothing to score"],
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
chunks = _split_diff_per_file(diff)
|
|
192
|
+
files = [_file_score(p, txt) for p, txt in chunks]
|
|
193
|
+
|
|
194
|
+
arch_risk = any(f["architectural_risk"] for f in files)
|
|
195
|
+
api_risk = any(f["public_api_risk"] for f in files)
|
|
196
|
+
rel_risk = any(f["release_risk"] for f in files)
|
|
197
|
+
|
|
198
|
+
src_paths = [f for f in files
|
|
199
|
+
if not f["path"].startswith(("tests/", "test/"))
|
|
200
|
+
and not f["path"].endswith(("_test.py",))]
|
|
201
|
+
test_paths = [f for f in files
|
|
202
|
+
if f["path"].startswith(("tests/", "test/"))
|
|
203
|
+
or f["path"].endswith(("_test.py",))]
|
|
204
|
+
test_risk = bool(src_paths) and not test_paths
|
|
205
|
+
|
|
206
|
+
notes: list[str] = []
|
|
207
|
+
if test_risk:
|
|
208
|
+
notes.append(
|
|
209
|
+
"code changed without tests touched — add coverage or "
|
|
210
|
+
"explicitly justify why existing tests are sufficient"
|
|
211
|
+
)
|
|
212
|
+
if not chunks:
|
|
213
|
+
notes.append(
|
|
214
|
+
"diff parsed but no '+++ b/<path>' headers found — "
|
|
215
|
+
"unified-diff format expected"
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
# Codex-6: respect [tool.forge.agent] protected_files when a
|
|
219
|
+
# project_root is provided. Lazy import to avoid a circular path.
|
|
220
|
+
protected_hits: list[str] = []
|
|
221
|
+
if project_root is not None:
|
|
222
|
+
try:
|
|
223
|
+
from pathlib import Path as _Path
|
|
224
|
+
|
|
225
|
+
from .policy_loader import file_is_protected, load_policy
|
|
226
|
+
policy = load_policy(_Path(str(project_root)))
|
|
227
|
+
for f in files:
|
|
228
|
+
if file_is_protected(f["path"], policy):
|
|
229
|
+
protected_hits.append(f["path"])
|
|
230
|
+
f.setdefault("notes", []).append(
|
|
231
|
+
"policy: file listed in protected_files — needs review"
|
|
232
|
+
)
|
|
233
|
+
except Exception: # noqa: BLE001
|
|
234
|
+
pass
|
|
235
|
+
|
|
236
|
+
needs_review = arch_risk or api_risk or rel_risk or bool(protected_hits) or (
|
|
237
|
+
sum(f["added_lines"] for f in files) > 200
|
|
238
|
+
)
|
|
239
|
+
if protected_hits:
|
|
240
|
+
notes.append(
|
|
241
|
+
f"diff touches {len(protected_hits)} file(s) listed in "
|
|
242
|
+
"[tool.forge.agent] protected_files — needs_human_review=True"
|
|
243
|
+
)
|
|
244
|
+
if needs_review and "review" not in " ".join(notes):
|
|
245
|
+
notes.append(
|
|
246
|
+
"needs_human_review=True because of architectural / "
|
|
247
|
+
"public-API / release / large-patch risk — do NOT auto-merge"
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
return PatchScore(
|
|
251
|
+
schema_version=SCHEMA_VERSION_PATCH_SCORE_V1,
|
|
252
|
+
file_count=len(files),
|
|
253
|
+
total_added=sum(f["added_lines"] for f in files),
|
|
254
|
+
total_removed=sum(f["removed_lines"] for f in files),
|
|
255
|
+
architectural_risk=arch_risk,
|
|
256
|
+
test_risk=test_risk,
|
|
257
|
+
public_api_risk=api_risk,
|
|
258
|
+
release_risk=rel_risk,
|
|
259
|
+
needs_human_review=needs_review,
|
|
260
|
+
files=files,
|
|
261
|
+
suggested_validation_commands=[
|
|
262
|
+
"forge wire src --fail-on-violations",
|
|
263
|
+
"python -m pytest",
|
|
264
|
+
"forge certify . --fail-under 75",
|
|
265
|
+
],
|
|
266
|
+
notes=notes,
|
|
267
|
+
)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Tier a1 — adapt_plan: capability-aware card filtering (Codex #8).
|
|
2
|
+
|
|
3
|
+
Codex's prescription:
|
|
4
|
+
|
|
5
|
+
> Different agents have different strengths: some can edit, some
|
|
6
|
+
> can only review, some can run commands, some cannot access
|
|
7
|
+
> network. adapt_plan({agent_capabilities}) tailors action cards:
|
|
8
|
+
> 'agent can apply', 'agent should ask human', 'agent should
|
|
9
|
+
> delegate', 'agent should only report'.
|
|
10
|
+
|
|
11
|
+
Pure: takes an existing AgentPlan + capability set, returns a new
|
|
12
|
+
plan with each card's recommended_handling field populated.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from copy import deepcopy
|
|
17
|
+
|
|
18
|
+
SCHEMA_VERSION_ADAPTED_PLAN_V1 = "atomadic-forge.agent_plan_adapted/v1"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# Known agent capability slugs.
|
|
22
|
+
_KNOWN_CAPABILITIES: frozenset[str] = frozenset({
|
|
23
|
+
"edit_files", # can write to disk
|
|
24
|
+
"run_commands", # can shell out
|
|
25
|
+
"network", # can reach external HTTP
|
|
26
|
+
"review", # can analyse but not modify
|
|
27
|
+
"delegate", # can spawn other agents
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _handling_for(card: dict, *, capabilities: set[str]) -> str:
|
|
32
|
+
"""Map a card to one of:
|
|
33
|
+
apply — agent has every capability the card needs
|
|
34
|
+
delegate — agent should spawn / invoke another tool
|
|
35
|
+
ask_human — card needs out-of-band judgement
|
|
36
|
+
report_only — agent should surface, not act
|
|
37
|
+
"""
|
|
38
|
+
applyable = bool(card.get("applyable"))
|
|
39
|
+
if not applyable:
|
|
40
|
+
return "ask_human"
|
|
41
|
+
kind = card.get("kind", "")
|
|
42
|
+
risk = card.get("risk", "high")
|
|
43
|
+
if kind == "synthesis" and "delegate" in capabilities:
|
|
44
|
+
return "delegate"
|
|
45
|
+
if kind in ("operational", "architectural"):
|
|
46
|
+
if "edit_files" in capabilities and "run_commands" in capabilities:
|
|
47
|
+
return "apply"
|
|
48
|
+
if "edit_files" in capabilities:
|
|
49
|
+
return "apply"
|
|
50
|
+
return "report_only"
|
|
51
|
+
if risk == "high":
|
|
52
|
+
return "ask_human"
|
|
53
|
+
return "report_only" if "edit_files" not in capabilities else "apply"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def adapt_plan(plan: dict, *, agent_capabilities: list[str]) -> dict:
|
|
57
|
+
"""Return a new plan with per-card 'recommended_handling' set.
|
|
58
|
+
|
|
59
|
+
Pure: input is not mutated.
|
|
60
|
+
"""
|
|
61
|
+
caps = {c for c in agent_capabilities or [] if c in _KNOWN_CAPABILITIES}
|
|
62
|
+
out = deepcopy(plan)
|
|
63
|
+
for card in out.get("top_actions", []):
|
|
64
|
+
card["recommended_handling"] = _handling_for(card, capabilities=caps)
|
|
65
|
+
out["schema_version"] = SCHEMA_VERSION_ADAPTED_PLAN_V1
|
|
66
|
+
out["agent_capabilities"] = sorted(caps)
|
|
67
|
+
out["unknown_capabilities"] = sorted(
|
|
68
|
+
c for c in (agent_capabilities or []) if c not in _KNOWN_CAPABILITIES
|
|
69
|
+
)
|
|
70
|
+
counts: dict[str, int] = {}
|
|
71
|
+
for card in out.get("top_actions", []):
|
|
72
|
+
h = card["recommended_handling"]
|
|
73
|
+
counts[h] = counts.get(h, 0) + 1
|
|
74
|
+
out["handling_counts"] = counts
|
|
75
|
+
return out
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Tier a1 — pure pyproject.toml [tool.forge.agent] policy reader.
|
|
2
|
+
|
|
3
|
+
Codex #10. Reads the policy block (if present) and returns an
|
|
4
|
+
AgentPolicy dict with defaults filled in for any missing fields.
|
|
5
|
+
|
|
6
|
+
Pure: one bounded read of pyproject.toml; no other I/O. Returns the
|
|
7
|
+
default policy unchanged when no pyproject / no [tool.forge.agent]
|
|
8
|
+
section exists — Forge never errors on policy absence.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from ..a0_qk_constants.policy_schema import (
|
|
17
|
+
DEFAULT_MAX_FILES_PER_PATCH,
|
|
18
|
+
DEFAULT_PROTECTED_FILES,
|
|
19
|
+
DEFAULT_RELEASE_GATE,
|
|
20
|
+
DEFAULT_REQUIRE_HUMAN_REVIEW_FOR,
|
|
21
|
+
SCHEMA_VERSION_POLICY_V1,
|
|
22
|
+
AgentPolicy,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# Python 3.11+ has stdlib tomllib; older versions need a fallback.
|
|
26
|
+
if sys.version_info >= (3, 11):
|
|
27
|
+
import tomllib as _toml # type: ignore[import-not-found]
|
|
28
|
+
else: # pragma: no cover — Forge requires 3.10+, so this is rare path
|
|
29
|
+
try:
|
|
30
|
+
import tomli as _toml # type: ignore[import-not-found]
|
|
31
|
+
except ImportError:
|
|
32
|
+
_toml = None # type: ignore[assignment]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def default_policy() -> AgentPolicy:
|
|
36
|
+
"""The lenient baseline policy applied when no [tool.forge.agent]
|
|
37
|
+
section is declared. Every field is mutable — caller can layer
|
|
38
|
+
user overrides on top."""
|
|
39
|
+
return AgentPolicy(
|
|
40
|
+
schema_version=SCHEMA_VERSION_POLICY_V1,
|
|
41
|
+
protected_files=list(DEFAULT_PROTECTED_FILES),
|
|
42
|
+
release_gate=list(DEFAULT_RELEASE_GATE),
|
|
43
|
+
max_files_per_patch=DEFAULT_MAX_FILES_PER_PATCH,
|
|
44
|
+
require_human_review_for=list(DEFAULT_REQUIRE_HUMAN_REVIEW_FOR),
|
|
45
|
+
extra={},
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def load_policy(project_root: Path) -> AgentPolicy:
|
|
50
|
+
"""Read [tool.forge.agent] from project_root/pyproject.toml.
|
|
51
|
+
|
|
52
|
+
Returns the default policy with any user-declared values merged
|
|
53
|
+
on top. Missing pyproject or missing section → defaults.
|
|
54
|
+
Malformed TOML → defaults + a note in extra['_load_error'].
|
|
55
|
+
"""
|
|
56
|
+
project_root = Path(project_root).resolve()
|
|
57
|
+
policy = default_policy()
|
|
58
|
+
|
|
59
|
+
pp = project_root / "pyproject.toml"
|
|
60
|
+
if not pp.exists() or _toml is None:
|
|
61
|
+
return policy
|
|
62
|
+
try:
|
|
63
|
+
data = _toml.loads(pp.read_text(encoding="utf-8"))
|
|
64
|
+
except Exception as exc: # noqa: BLE001
|
|
65
|
+
policy["extra"] = {"_load_error": f"{type(exc).__name__}: {exc}"}
|
|
66
|
+
return policy
|
|
67
|
+
|
|
68
|
+
section = (data.get("tool") or {}).get("forge", {}).get("agent")
|
|
69
|
+
if not isinstance(section, dict):
|
|
70
|
+
return policy
|
|
71
|
+
|
|
72
|
+
if isinstance(section.get("protected_files"), list):
|
|
73
|
+
policy["protected_files"] = [str(s) for s in section["protected_files"]]
|
|
74
|
+
if isinstance(section.get("release_gate"), list):
|
|
75
|
+
policy["release_gate"] = [str(s) for s in section["release_gate"]]
|
|
76
|
+
if isinstance(section.get("max_files_per_patch"), int):
|
|
77
|
+
policy["max_files_per_patch"] = int(section["max_files_per_patch"])
|
|
78
|
+
if isinstance(section.get("require_human_review_for"), list):
|
|
79
|
+
policy["require_human_review_for"] = [
|
|
80
|
+
str(s) for s in section["require_human_review_for"]
|
|
81
|
+
]
|
|
82
|
+
# Forward-compat: any unrecognised keys preserved in extra.
|
|
83
|
+
known = {
|
|
84
|
+
"protected_files", "release_gate", "max_files_per_patch",
|
|
85
|
+
"require_human_review_for",
|
|
86
|
+
}
|
|
87
|
+
extra: dict[str, Any] = {}
|
|
88
|
+
for k, v in section.items():
|
|
89
|
+
if k not in known:
|
|
90
|
+
extra[k] = v
|
|
91
|
+
if extra:
|
|
92
|
+
policy["extra"] = extra
|
|
93
|
+
return policy
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def file_is_protected(path: str, policy: AgentPolicy) -> bool:
|
|
97
|
+
"""True when ``path`` matches any protected_files entry.
|
|
98
|
+
|
|
99
|
+
Matching is exact-or-suffix: 'pyproject.toml' matches
|
|
100
|
+
'pyproject.toml' and 'src/pkg/pyproject.toml' but NOT
|
|
101
|
+
'src/pyproject_helper.py'.
|
|
102
|
+
"""
|
|
103
|
+
protected = policy.get("protected_files") or []
|
|
104
|
+
for p in protected:
|
|
105
|
+
if path == p or path.endswith("/" + p) or Path(path).name == p:
|
|
106
|
+
return True
|
|
107
|
+
return False
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""Tier a1 — pure preflight check for proposed changes.
|
|
2
|
+
|
|
3
|
+
Codex's 'Copilot's Copilot' primitive #2:
|
|
4
|
+
|
|
5
|
+
> preflight_change({intent, proposed_files}) — returns where the
|
|
6
|
+
> code should live, forbidden imports, tests likely affected,
|
|
7
|
+
> files the agent should read first, whether the write scope is
|
|
8
|
+
> too broad. Most agent mistakes happen before code is written.
|
|
9
|
+
|
|
10
|
+
Pure: no I/O beyond optional file-read for write_scope-too-broad
|
|
11
|
+
size hints. The classifier reads the proposed file paths only.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import TypedDict
|
|
17
|
+
|
|
18
|
+
SCHEMA_VERSION_PREFLIGHT_V1 = "atomadic-forge.preflight/v1"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
_TIER_NAMES = (
|
|
22
|
+
"a0_qk_constants",
|
|
23
|
+
"a1_at_functions",
|
|
24
|
+
"a2_mo_composites",
|
|
25
|
+
"a3_og_features",
|
|
26
|
+
"a4_sy_orchestration",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# Per tier: the tier names this tier may NOT import (forbidden).
|
|
30
|
+
_FORBIDDEN_BY_TIER: dict[str, tuple[str, ...]] = {
|
|
31
|
+
"a0_qk_constants": (
|
|
32
|
+
"a0_qk_constants", # may not import siblings either
|
|
33
|
+
"a1_at_functions", "a2_mo_composites",
|
|
34
|
+
"a3_og_features", "a4_sy_orchestration",
|
|
35
|
+
),
|
|
36
|
+
"a1_at_functions": (
|
|
37
|
+
"a2_mo_composites", "a3_og_features", "a4_sy_orchestration",
|
|
38
|
+
),
|
|
39
|
+
"a2_mo_composites": ("a3_og_features", "a4_sy_orchestration"),
|
|
40
|
+
"a3_og_features": ("a4_sy_orchestration",),
|
|
41
|
+
"a4_sy_orchestration": (),
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
_DEFAULT_SCOPE_TOO_BROAD = 8
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class PreflightFile(TypedDict, total=False):
|
|
48
|
+
path: str
|
|
49
|
+
detected_tier: str | None
|
|
50
|
+
forbidden_imports: list[str]
|
|
51
|
+
likely_tests: list[str]
|
|
52
|
+
siblings_to_read: list[str]
|
|
53
|
+
notes: list[str]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class PreflightReport(TypedDict, total=False):
|
|
57
|
+
schema_version: str
|
|
58
|
+
intent: str
|
|
59
|
+
project_root: str
|
|
60
|
+
proposed_files: list[PreflightFile]
|
|
61
|
+
write_scope_too_broad: bool
|
|
62
|
+
write_scope_size: int
|
|
63
|
+
write_scope_threshold: int
|
|
64
|
+
overall_notes: list[str]
|
|
65
|
+
suggested_validation_commands: list[str]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _detect_tier(path: str) -> str | None:
|
|
69
|
+
parts = Path(path).parts
|
|
70
|
+
for p in parts:
|
|
71
|
+
if p in _TIER_NAMES:
|
|
72
|
+
return p
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _likely_tests_for(path: str) -> list[str]:
|
|
77
|
+
"""Mirror-style test path heuristic.
|
|
78
|
+
|
|
79
|
+
src/pkg/a1_at_functions/foo.py -> tests/test_foo.py,
|
|
80
|
+
tests/a1_at_functions/test_foo.py
|
|
81
|
+
"""
|
|
82
|
+
p = Path(path)
|
|
83
|
+
stem = p.stem
|
|
84
|
+
if stem.startswith("test_") or stem.endswith("_test"):
|
|
85
|
+
return [path] # the file IS a test
|
|
86
|
+
candidates: list[str] = []
|
|
87
|
+
candidates.append(f"tests/test_{stem}.py")
|
|
88
|
+
candidates.append(f"tests/{stem}_test.py")
|
|
89
|
+
parent_tier = _detect_tier(path)
|
|
90
|
+
if parent_tier:
|
|
91
|
+
candidates.append(f"tests/{parent_tier}/test_{stem}.py")
|
|
92
|
+
return candidates
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _siblings_to_read(path: str, *, project_root: Path) -> list[str]:
|
|
96
|
+
"""When the agent edits a file, sibling files in the same tier
|
|
97
|
+
directory are the most likely places to share patterns or to
|
|
98
|
+
accidentally diverge — read them first."""
|
|
99
|
+
p = Path(path)
|
|
100
|
+
if not p.parent.parts:
|
|
101
|
+
return []
|
|
102
|
+
sib_dir = project_root / p.parent
|
|
103
|
+
if not sib_dir.is_dir():
|
|
104
|
+
return []
|
|
105
|
+
out: list[str] = []
|
|
106
|
+
for sib in sorted(sib_dir.glob("*.py")):
|
|
107
|
+
if sib.name == p.name:
|
|
108
|
+
continue
|
|
109
|
+
if sib.name.startswith("_"):
|
|
110
|
+
continue
|
|
111
|
+
out.append(str(sib.relative_to(project_root).as_posix()))
|
|
112
|
+
if len(out) >= 5:
|
|
113
|
+
break
|
|
114
|
+
return out
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _file_preflight(
|
|
118
|
+
path: str,
|
|
119
|
+
*,
|
|
120
|
+
project_root: Path,
|
|
121
|
+
) -> PreflightFile:
|
|
122
|
+
notes: list[str] = []
|
|
123
|
+
tier = _detect_tier(path)
|
|
124
|
+
forbidden = list(_FORBIDDEN_BY_TIER.get(tier, ())) if tier else []
|
|
125
|
+
likely_tests = _likely_tests_for(path)
|
|
126
|
+
siblings = _siblings_to_read(path, project_root=project_root)
|
|
127
|
+
if tier is None:
|
|
128
|
+
notes.append(
|
|
129
|
+
"no tier directory in this path — file likely belongs in "
|
|
130
|
+
"a tier under src/<package>/aN_*/"
|
|
131
|
+
)
|
|
132
|
+
if Path(path).name == "__init__.py":
|
|
133
|
+
notes.append(
|
|
134
|
+
"__init__.py edits affect tier re-exports; run "
|
|
135
|
+
"tier_init_rebuild after a structural change"
|
|
136
|
+
)
|
|
137
|
+
return PreflightFile(
|
|
138
|
+
path=path,
|
|
139
|
+
detected_tier=tier,
|
|
140
|
+
forbidden_imports=forbidden,
|
|
141
|
+
likely_tests=likely_tests,
|
|
142
|
+
siblings_to_read=siblings,
|
|
143
|
+
notes=notes,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def preflight_change(
|
|
148
|
+
*,
|
|
149
|
+
intent: str,
|
|
150
|
+
proposed_files: list[str],
|
|
151
|
+
project_root: Path,
|
|
152
|
+
scope_threshold: int = _DEFAULT_SCOPE_TOO_BROAD,
|
|
153
|
+
) -> PreflightReport:
|
|
154
|
+
"""Heuristic preflight for a proposed code change.
|
|
155
|
+
|
|
156
|
+
Pure: takes paths + intent, walks the local filesystem only to
|
|
157
|
+
check sibling presence. Does NOT read the proposed_files
|
|
158
|
+
themselves — they may not exist yet.
|
|
159
|
+
|
|
160
|
+
Codex-6: when [tool.forge.agent] is declared in the project's
|
|
161
|
+
pyproject.toml, ``max_files_per_patch`` overrides the default
|
|
162
|
+
threshold and ``protected_files`` adds per-file warnings.
|
|
163
|
+
"""
|
|
164
|
+
from .policy_loader import file_is_protected, load_policy
|
|
165
|
+
project_root = Path(project_root).resolve()
|
|
166
|
+
policy = load_policy(project_root)
|
|
167
|
+
if scope_threshold == _DEFAULT_SCOPE_TOO_BROAD and \
|
|
168
|
+
isinstance(policy.get("max_files_per_patch"), int):
|
|
169
|
+
scope_threshold = int(policy["max_files_per_patch"])
|
|
170
|
+
files: list[PreflightFile] = [
|
|
171
|
+
_file_preflight(p, project_root=project_root)
|
|
172
|
+
for p in proposed_files
|
|
173
|
+
]
|
|
174
|
+
for f in files:
|
|
175
|
+
if file_is_protected(f["path"], policy):
|
|
176
|
+
notes = list(f.get("notes") or [])
|
|
177
|
+
notes.append(
|
|
178
|
+
"policy: this file is in [tool.forge.agent] "
|
|
179
|
+
"protected_files — agent should request human review"
|
|
180
|
+
)
|
|
181
|
+
f["notes"] = notes
|
|
182
|
+
overall: list[str] = []
|
|
183
|
+
protected_in_scope = [f["path"] for f in files
|
|
184
|
+
if any("protected_files" in n
|
|
185
|
+
for n in f.get("notes", []))]
|
|
186
|
+
if protected_in_scope:
|
|
187
|
+
overall.append(
|
|
188
|
+
f"{len(protected_in_scope)} protected file(s) in scope "
|
|
189
|
+
f"(per [tool.forge.agent]): {protected_in_scope[:5]} — "
|
|
190
|
+
"request human review before applying."
|
|
191
|
+
)
|
|
192
|
+
too_broad = len(proposed_files) > scope_threshold
|
|
193
|
+
if too_broad:
|
|
194
|
+
overall.append(
|
|
195
|
+
f"write_scope has {len(proposed_files)} files (> "
|
|
196
|
+
f"{scope_threshold} threshold). Consider splitting the "
|
|
197
|
+
"change or asking for human review."
|
|
198
|
+
)
|
|
199
|
+
detected_tiers = {f.get("detected_tier") for f in files}
|
|
200
|
+
detected_tiers.discard(None)
|
|
201
|
+
if len(detected_tiers) > 1:
|
|
202
|
+
overall.append(
|
|
203
|
+
f"change spans {len(detected_tiers)} tiers "
|
|
204
|
+
f"({sorted(detected_tiers)}); cross-tier edits should be "
|
|
205
|
+
"split into per-tier patches when possible."
|
|
206
|
+
)
|
|
207
|
+
if any(f.get("path", "").startswith("tests/") for f in files) and \
|
|
208
|
+
not any(not f.get("path", "").startswith("tests/") for f in files):
|
|
209
|
+
overall.append(
|
|
210
|
+
"test-only change — verify it pins existing behaviour and "
|
|
211
|
+
"is not silently masking a regression."
|
|
212
|
+
)
|
|
213
|
+
return PreflightReport(
|
|
214
|
+
schema_version=SCHEMA_VERSION_PREFLIGHT_V1,
|
|
215
|
+
intent=intent,
|
|
216
|
+
project_root=str(project_root),
|
|
217
|
+
proposed_files=files,
|
|
218
|
+
write_scope_too_broad=too_broad,
|
|
219
|
+
write_scope_size=len(proposed_files),
|
|
220
|
+
write_scope_threshold=scope_threshold,
|
|
221
|
+
overall_notes=overall,
|
|
222
|
+
suggested_validation_commands=[
|
|
223
|
+
"forge wire src --fail-on-violations",
|
|
224
|
+
"python -m pytest",
|
|
225
|
+
"forge certify . --fail-under 75",
|
|
226
|
+
],
|
|
227
|
+
)
|