agentforge-framework 0.2.0__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.
- agentforge_framework/.claude-plugin/plugin.json +4 -0
- agentforge_framework/__init__.py +3 -0
- agentforge_framework/agents/__init__.py +92 -0
- agentforge_framework/agents/architect.py +146 -0
- agentforge_framework/agents/implementer.py +162 -0
- agentforge_framework/agents/orchestrator.py +588 -0
- agentforge_framework/agents/reviewer.py +335 -0
- agentforge_framework/agents/security.py +138 -0
- agentforge_framework/agents/tester.py +125 -0
- agentforge_framework/cli.py +461 -0
- agentforge_framework/context/__init__.py +1 -0
- agentforge_framework/context/extractors/__init__.py +76 -0
- agentforge_framework/context/extractors/base.py +47 -0
- agentforge_framework/context/extractors/python.py +65 -0
- agentforge_framework/context/extractors/sql.py +121 -0
- agentforge_framework/context/extractors/yaml.py +59 -0
- agentforge_framework/context/prompt.py +104 -0
- agentforge_framework/context/resolver.py +185 -0
- agentforge_framework/core/__init__.py +1 -0
- agentforge_framework/core/commands.py +170 -0
- agentforge_framework/core/config.py +90 -0
- agentforge_framework/core/contracts.py +875 -0
- agentforge_framework/core/gates.py +333 -0
- agentforge_framework/core/issues.py +697 -0
- agentforge_framework/core/plan_format.py +272 -0
- agentforge_framework/core/process.py +141 -0
- agentforge_framework/core/project.py +262 -0
- agentforge_framework/core/registry.py +455 -0
- agentforge_framework/core/repo.py +185 -0
- agentforge_framework/core/router.py +1 -0
- agentforge_framework/core/runtime.py +639 -0
- agentforge_framework/core/skills.py +255 -0
- agentforge_framework/core/workflow.py +215 -0
- agentforge_framework/plugins/__init__.py +35 -0
- agentforge_framework/plugins/databricks/__init__.py +86 -0
- agentforge_framework/plugins/pyspark/__init__.py +57 -0
- agentforge_framework/plugins/python/__init__.py +45 -0
- agentforge_framework/plugins/sql/__init__.py +377 -0
- agentforge_framework/providers/__init__.py +48 -0
- agentforge_framework/providers/base.py +248 -0
- agentforge_framework/providers/claude.py +159 -0
- agentforge_framework/providers/codex.py +139 -0
- agentforge_framework/skills/MANIFEST.yaml +157 -0
- agentforge_framework/skills/NOTICE +49 -0
- agentforge_framework/skills/domain-modeling/ADR-FORMAT.md +47 -0
- agentforge_framework/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
- agentforge_framework/skills/domain-modeling/SKILL.md +74 -0
- agentforge_framework/skills/domain-modeling/agents/openai.yaml +3 -0
- agentforge_framework/skills/grill-with-docs/SKILL.md +76 -0
- agentforge_framework/skills/grilling/SKILL.md +28 -0
- agentforge_framework/skills/grilling/agents/openai.yaml +3 -0
- agentforge_framework/skills/to-spec/SKILL.md +75 -0
- agentforge_framework/skills/to-spec/agents/openai.yaml +5 -0
- agentforge_framework/skills/to-tickets/SKILL.md +105 -0
- agentforge_framework/skills/to-tickets/agents/openai.yaml +5 -0
- agentforge_framework/skills/unslop/SKILL.md +131 -0
- agentforge_framework/skills/unslop/evals/fixtures/silhouette/human_reference.json +66 -0
- agentforge_framework/skills/unslop/scripts/_lang.py +106 -0
- agentforge_framework/skills/unslop/scripts/banned_phrase_scan.py +784 -0
- agentforge_framework/skills/unslop/scripts/calibrate_pairs.py +580 -0
- agentforge_framework/skills/unslop/scripts/calibrate_score.py +273 -0
- agentforge_framework/skills/unslop/scripts/check_packs.py +80 -0
- agentforge_framework/skills/unslop/scripts/check_suggestions.py +225 -0
- agentforge_framework/skills/unslop/scripts/contribute.py +373 -0
- agentforge_framework/skills/unslop/scripts/diff_check.py +139 -0
- agentforge_framework/skills/unslop/scripts/extract_constraints.py +201 -0
- agentforge_framework/skills/unslop/scripts/harvest_classify.py +223 -0
- agentforge_framework/skills/unslop/scripts/harvest_samples.py +534 -0
- agentforge_framework/skills/unslop/scripts/readability_metrics.py +295 -0
- agentforge_framework/skills/unslop/scripts/refresh_status.py +154 -0
- agentforge_framework/skills/unslop/scripts/silhouette_scan.py +390 -0
- agentforge_framework/skills/unslop/scripts/structure_scan.py +322 -0
- agentforge_framework/skills/unslop/scripts/suggest.py +211 -0
- agentforge_framework/skills/unslop/scripts/validate_preservation.py +409 -0
- agentforge_framework/skills/unslop/scripts/voice_card.py +496 -0
- agentforge_framework/skills/unslop/scripts/voice_profile.py +194 -0
- agentforge_framework/skills/unslop/scripts/voice_score.py +271 -0
- agentforge_framework/skills/unslop/scripts/wiki_sync.py +479 -0
- agentforge_framework/skills/write-plainly/SKILL.md +94 -0
- agentforge_framework/workflows/bugfix.yaml +8 -0
- agentforge_framework/workflows/feature.yaml +16 -0
- agentforge_framework/workflows/review.yaml +10 -0
- agentforge_framework-0.2.0.dist-info/METADATA +321 -0
- agentforge_framework-0.2.0.dist-info/RECORD +89 -0
- agentforge_framework-0.2.0.dist-info/WHEEL +5 -0
- agentforge_framework-0.2.0.dist-info/entry_points.txt +3 -0
- agentforge_framework-0.2.0.dist-info/licenses/LICENSE +202 -0
- agentforge_framework-0.2.0.dist-info/licenses/src/agentforge_framework/skills/NOTICE +49 -0
- agentforge_framework-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Score a candidate against a voice profile.
|
|
3
|
+
|
|
4
|
+
Lower composite means more user-like. The composite is half GI-rank penalty and
|
|
5
|
+
half clipped, weighted impostor z-distance using the WP10a research weights.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import json
|
|
12
|
+
import math
|
|
13
|
+
import random
|
|
14
|
+
import statistics
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
import voice_profile
|
|
19
|
+
|
|
20
|
+
WEIGHTS = {
|
|
21
|
+
"char3": 0.30,
|
|
22
|
+
"delta": 0.25,
|
|
23
|
+
"sentence_emd": 0.10,
|
|
24
|
+
"punctuation": 0.08,
|
|
25
|
+
"contraction": 0.07,
|
|
26
|
+
"mtld": 0.10,
|
|
27
|
+
"word_length": 0.10,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def cosine_distance(a, b, keys=None):
|
|
32
|
+
keys = list(keys) if keys is not None else sorted(set(a) | set(b))
|
|
33
|
+
dot = sum(a.get(k, 0.0) * b.get(k, 0.0) for k in keys)
|
|
34
|
+
na = math.sqrt(sum(a.get(k, 0.0) ** 2 for k in keys))
|
|
35
|
+
nb = math.sqrt(sum(b.get(k, 0.0) ** 2 for k in keys))
|
|
36
|
+
if not na or not nb:
|
|
37
|
+
return 1.0
|
|
38
|
+
return 1 - (dot / (na * nb))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def z_function_vector(freqs, bg, top):
|
|
42
|
+
keys = sorted(bg, key=lambda k: bg[k].get("mean", 0), reverse=True)[:top]
|
|
43
|
+
return {k: (freqs.get(k, 0.0) - bg[k]["mean"]) / (bg[k]["std"] or 0.0001) for k in keys}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def emd(a, b):
|
|
47
|
+
if not a or not b:
|
|
48
|
+
return 0.0
|
|
49
|
+
max_len = max(max(a), max(b))
|
|
50
|
+
ca = cb = dist = 0.0
|
|
51
|
+
for i in range(1, max_len + 1):
|
|
52
|
+
ca += sum(1 for x in a if x == i) / len(a)
|
|
53
|
+
cb += sum(1 for x in b if x == i) / len(b)
|
|
54
|
+
dist += abs(ca - cb)
|
|
55
|
+
return dist
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def l1(a, b, keys):
|
|
59
|
+
return sum(abs(a.get(k, 0.0) - b.get(k, 0.0)) for k in keys)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def distances(profile, feats, subset=None):
|
|
63
|
+
subset = set(subset or WEIGHTS)
|
|
64
|
+
out = {}
|
|
65
|
+
if "char3" in subset:
|
|
66
|
+
keys = set(profile["char3"]) | set(feats["char3"])
|
|
67
|
+
out["char3"] = cosine_distance(profile["char3"], feats["char3"], keys)
|
|
68
|
+
if "delta" in subset:
|
|
69
|
+
top = 50 if feats["total_words"] < 300 else 200
|
|
70
|
+
bg = profile["function_word_background"]
|
|
71
|
+
pv = z_function_vector(profile["function_words"], bg, top)
|
|
72
|
+
cv = z_function_vector(feats["function_words"], bg, top)
|
|
73
|
+
out["delta"] = cosine_distance(pv, cv, pv.keys())
|
|
74
|
+
if "sentence_emd" in subset:
|
|
75
|
+
med = profile["sentence_lengths"].get("median") or 1.0
|
|
76
|
+
out["sentence_emd"] = emd(profile["sentence_lengths"]["lengths"], feats["sentence_lengths"]["lengths"]) / med
|
|
77
|
+
if "punctuation" in subset:
|
|
78
|
+
out["punctuation"] = l1(profile["punctuation"], feats["punctuation"], voice_profile.PUNCT)
|
|
79
|
+
if "contraction" in subset:
|
|
80
|
+
out["contraction"] = abs(profile["contraction_rate"] - feats["contraction_rate"])
|
|
81
|
+
if "mtld" in subset:
|
|
82
|
+
out["mtld"] = abs(profile["mtld"] - feats["mtld"]) / (profile["mtld"] or 1.0)
|
|
83
|
+
if "word_length" in subset:
|
|
84
|
+
keys = [str(i) for i in range(1, 16)]
|
|
85
|
+
out["word_length"] = l1(profile["word_length_histogram"], feats["word_length_histogram"], keys)
|
|
86
|
+
return out
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def weighted_sum(dists):
|
|
90
|
+
return sum(WEIGHTS[k] * dists.get(k, 0.0) for k in WEIGHTS)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def impostor_features(root):
|
|
94
|
+
feats = []
|
|
95
|
+
for path in voice_profile.iter_docs(root):
|
|
96
|
+
feats.append((str(path), voice_profile.feature_bundle(path.read_text(errors="replace"))))
|
|
97
|
+
return feats
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def zscores(candidate, impostor_rows):
|
|
101
|
+
out = {}
|
|
102
|
+
for key in WEIGHTS:
|
|
103
|
+
vals = [row[key] for row in impostor_rows if key in row]
|
|
104
|
+
if key not in candidate or not vals:
|
|
105
|
+
out[key] = None
|
|
106
|
+
continue
|
|
107
|
+
mean = statistics.mean(vals)
|
|
108
|
+
std = statistics.pstdev(vals) or 0.0001
|
|
109
|
+
out[key] = (candidate[key] - mean) / std
|
|
110
|
+
return out
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def gi_score(profile, cand_feats, impostors, seed):
|
|
114
|
+
rng = random.Random(seed)
|
|
115
|
+
keys = list(WEIGHTS)
|
|
116
|
+
wins = 0
|
|
117
|
+
trials = 64
|
|
118
|
+
# Per-key distances depend only on (profile, feats) -- not on which subset
|
|
119
|
+
# of keys a given trial happens to draw -- so compute every key's distance
|
|
120
|
+
# ONCE per candidate/impostor here, then have each trial do a subset-
|
|
121
|
+
# weighted sum over the precomputed values instead of recomputing
|
|
122
|
+
# distances() from scratch on every trial. Arithmetically exact: the
|
|
123
|
+
# trial loop still consumes rng.random()/rng.sample() in the same order,
|
|
124
|
+
# and summing WEIGHTS[k]*value only over the drawn subset is the same
|
|
125
|
+
# float sequence weighted_sum(distances(..., subset)) produced (excluded
|
|
126
|
+
# keys contributed an exact 0.0 term either way).
|
|
127
|
+
cand_dists = distances(profile, cand_feats)
|
|
128
|
+
impostor_dists = [(name, distances(profile, imp)) for name, imp in impostors]
|
|
129
|
+
for _ in range(trials):
|
|
130
|
+
subset = [k for k in keys if rng.random() < 0.5] or [rng.choice(keys)]
|
|
131
|
+
cand = sum(WEIGHTS[k] * cand_dists.get(k, 0.0) for k in subset)
|
|
132
|
+
sampled = rng.sample(impostor_dists, k=min(len(impostor_dists), max(1, len(impostor_dists) // 2)))
|
|
133
|
+
if all(cand < sum(WEIGHTS[k] * imp_dists.get(k, 0.0) for k in subset)
|
|
134
|
+
for _, imp_dists in sampled):
|
|
135
|
+
wins += 1
|
|
136
|
+
return wins / trials
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def ngrams(tokens, n=4):
|
|
140
|
+
return set(tuple(tokens[i:i + n]) for i in range(max(0, len(tokens) - n + 1)))
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
LCS_THRESHOLD = 120
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def has_common_substring_over(a: str, b: str, min_length: int) -> bool:
|
|
147
|
+
"""Rolling-hash check: do ``a`` and ``b`` share a contiguous substring of at
|
|
148
|
+
least ``min_length`` characters?
|
|
149
|
+
|
|
150
|
+
O(len(a) + len(b)) expected time, vs. the O(len(a) * len(b)) classic DP a
|
|
151
|
+
true longest-common-substring computation needs. This only answers the
|
|
152
|
+
threshold question the copy gate actually asks ("is there a shared run
|
|
153
|
+
longer than N chars"); it does not recover the true longest common
|
|
154
|
+
substring length. Every hash match is verified against the source
|
|
155
|
+
characters before being trusted, so a hash collision never produces a
|
|
156
|
+
false positive.
|
|
157
|
+
"""
|
|
158
|
+
if min_length <= 0:
|
|
159
|
+
return bool(a) and bool(b)
|
|
160
|
+
if len(a) < min_length or len(b) < min_length:
|
|
161
|
+
return False
|
|
162
|
+
|
|
163
|
+
base = 257
|
|
164
|
+
mod = (1 << 61) - 1
|
|
165
|
+
high_power = pow(base, min_length - 1, mod)
|
|
166
|
+
|
|
167
|
+
def window_hashes(s: str) -> dict[int, list[int]]:
|
|
168
|
+
table: dict[int, list[int]] = {}
|
|
169
|
+
h = 0
|
|
170
|
+
for i in range(min_length):
|
|
171
|
+
h = (h * base + ord(s[i])) % mod
|
|
172
|
+
table.setdefault(h, []).append(0)
|
|
173
|
+
for i in range(min_length, len(s)):
|
|
174
|
+
h = ((h - ord(s[i - min_length]) * high_power) * base + ord(s[i])) % mod
|
|
175
|
+
table.setdefault(h, []).append(i - min_length + 1)
|
|
176
|
+
return table
|
|
177
|
+
|
|
178
|
+
table_a = window_hashes(a)
|
|
179
|
+
table_b = window_hashes(b)
|
|
180
|
+
for h, starts_b in table_b.items():
|
|
181
|
+
starts_a = table_a.get(h)
|
|
182
|
+
if not starts_a:
|
|
183
|
+
continue
|
|
184
|
+
for sb in starts_b:
|
|
185
|
+
window_b = b[sb:sb + min_length]
|
|
186
|
+
for sa in starts_a:
|
|
187
|
+
if a[sa:sa + min_length] == window_b:
|
|
188
|
+
return True
|
|
189
|
+
return False
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def copy_gate(candidate_text, samples_dir):
|
|
193
|
+
cand_grams = ngrams(voice_profile.words(candidate_text))
|
|
194
|
+
max_overlap = 0.0
|
|
195
|
+
lcs_violation = False
|
|
196
|
+
for path in voice_profile.iter_docs(samples_dir):
|
|
197
|
+
text = path.read_text(errors="replace")
|
|
198
|
+
sample_grams = ngrams(voice_profile.words(text))
|
|
199
|
+
if cand_grams:
|
|
200
|
+
max_overlap = max(max_overlap, len(cand_grams & sample_grams) / len(cand_grams))
|
|
201
|
+
if has_common_substring_over(candidate_text, text, LCS_THRESHOLD + 1):
|
|
202
|
+
lcs_violation = True
|
|
203
|
+
# "longest_common_substring" is reported for backward-compatible shape only:
|
|
204
|
+
# nothing pins its exact value (checked evals/adversarial-evals.json and
|
|
205
|
+
# evals/check_*.py for max_lcs/longest_common_substring assertions; none
|
|
206
|
+
# exist). Its semantics changed from "the true LCS length" to "the
|
|
207
|
+
# matched threshold window length when a violation-length run was found,
|
|
208
|
+
# else 0" -- computing the true max cheaply isn't possible without the
|
|
209
|
+
# same O(n*m) cost this function exists to avoid.
|
|
210
|
+
max_lcs = LCS_THRESHOLD + 1 if lcs_violation else 0
|
|
211
|
+
return {
|
|
212
|
+
"max_overlap": max_overlap,
|
|
213
|
+
"longest_common_substring": max_lcs,
|
|
214
|
+
"violation": max_overlap > 0.35 or lcs_violation,
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def parse_args(argv):
|
|
219
|
+
parser = argparse.ArgumentParser()
|
|
220
|
+
parser.add_argument("--profile", required=True)
|
|
221
|
+
parser.add_argument("--impostors", required=True)
|
|
222
|
+
parser.add_argument("--seed", required=True, type=int)
|
|
223
|
+
parser.add_argument("--samples")
|
|
224
|
+
parser.add_argument("candidate_file")
|
|
225
|
+
return parser.parse_args(argv)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def read_candidate(path):
|
|
229
|
+
if path == "-":
|
|
230
|
+
return sys.stdin.buffer.read().decode("utf-8", errors="replace")
|
|
231
|
+
p = Path(path)
|
|
232
|
+
if not p.exists():
|
|
233
|
+
raise FileNotFoundError(path)
|
|
234
|
+
return p.read_text(errors="replace")
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def main(argv):
|
|
238
|
+
args = parse_args(argv)
|
|
239
|
+
if not Path(args.profile).exists() or not Path(args.impostors).is_dir():
|
|
240
|
+
print("missing profile or impostors", file=sys.stderr)
|
|
241
|
+
return 2
|
|
242
|
+
try:
|
|
243
|
+
text = read_candidate(args.candidate_file)
|
|
244
|
+
except FileNotFoundError as e:
|
|
245
|
+
print(f"missing candidate: {e}", file=sys.stderr)
|
|
246
|
+
return 2
|
|
247
|
+
profile = json.loads(Path(args.profile).read_text())
|
|
248
|
+
feats = voice_profile.feature_bundle(text)
|
|
249
|
+
low = feats["total_words"] < 150
|
|
250
|
+
impostors = impostor_features(args.impostors)
|
|
251
|
+
dist = distances(profile, feats)
|
|
252
|
+
imp_dist = [distances(profile, f) for _, f in impostors]
|
|
253
|
+
zs = zscores(dist, imp_dist)
|
|
254
|
+
zsum = sum(WEIGHTS[k] * max(-3.0, min(3.0, zs[k] if zs[k] is not None else 0.0)) for k in WEIGHTS)
|
|
255
|
+
gi = gi_score(profile, feats, impostors, args.seed) if impostors else 0.0
|
|
256
|
+
result = {
|
|
257
|
+
"candidate_words": feats["total_words"],
|
|
258
|
+
"low_confidence": low,
|
|
259
|
+
"distances": {k: (None if low and k in {"sentence_emd", "mtld"} else v) for k, v in dist.items()},
|
|
260
|
+
"z_scores": {k: (None if low and k in {"sentence_emd", "mtld"} else v) for k, v in zs.items()},
|
|
261
|
+
"gi": gi,
|
|
262
|
+
"composite": 0.5 * (1 - gi) + 0.5 * zsum,
|
|
263
|
+
}
|
|
264
|
+
if args.samples:
|
|
265
|
+
result["copy_gate"] = copy_gate(text, args.samples)
|
|
266
|
+
print(json.dumps(result, indent=2, sort_keys=True))
|
|
267
|
+
return 0
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
if __name__ == "__main__":
|
|
271
|
+
raise SystemExit(main(sys.argv[1:]))
|