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.
Files changed (89) hide show
  1. agentforge_framework/.claude-plugin/plugin.json +4 -0
  2. agentforge_framework/__init__.py +3 -0
  3. agentforge_framework/agents/__init__.py +92 -0
  4. agentforge_framework/agents/architect.py +146 -0
  5. agentforge_framework/agents/implementer.py +162 -0
  6. agentforge_framework/agents/orchestrator.py +588 -0
  7. agentforge_framework/agents/reviewer.py +335 -0
  8. agentforge_framework/agents/security.py +138 -0
  9. agentforge_framework/agents/tester.py +125 -0
  10. agentforge_framework/cli.py +461 -0
  11. agentforge_framework/context/__init__.py +1 -0
  12. agentforge_framework/context/extractors/__init__.py +76 -0
  13. agentforge_framework/context/extractors/base.py +47 -0
  14. agentforge_framework/context/extractors/python.py +65 -0
  15. agentforge_framework/context/extractors/sql.py +121 -0
  16. agentforge_framework/context/extractors/yaml.py +59 -0
  17. agentforge_framework/context/prompt.py +104 -0
  18. agentforge_framework/context/resolver.py +185 -0
  19. agentforge_framework/core/__init__.py +1 -0
  20. agentforge_framework/core/commands.py +170 -0
  21. agentforge_framework/core/config.py +90 -0
  22. agentforge_framework/core/contracts.py +875 -0
  23. agentforge_framework/core/gates.py +333 -0
  24. agentforge_framework/core/issues.py +697 -0
  25. agentforge_framework/core/plan_format.py +272 -0
  26. agentforge_framework/core/process.py +141 -0
  27. agentforge_framework/core/project.py +262 -0
  28. agentforge_framework/core/registry.py +455 -0
  29. agentforge_framework/core/repo.py +185 -0
  30. agentforge_framework/core/router.py +1 -0
  31. agentforge_framework/core/runtime.py +639 -0
  32. agentforge_framework/core/skills.py +255 -0
  33. agentforge_framework/core/workflow.py +215 -0
  34. agentforge_framework/plugins/__init__.py +35 -0
  35. agentforge_framework/plugins/databricks/__init__.py +86 -0
  36. agentforge_framework/plugins/pyspark/__init__.py +57 -0
  37. agentforge_framework/plugins/python/__init__.py +45 -0
  38. agentforge_framework/plugins/sql/__init__.py +377 -0
  39. agentforge_framework/providers/__init__.py +48 -0
  40. agentforge_framework/providers/base.py +248 -0
  41. agentforge_framework/providers/claude.py +159 -0
  42. agentforge_framework/providers/codex.py +139 -0
  43. agentforge_framework/skills/MANIFEST.yaml +157 -0
  44. agentforge_framework/skills/NOTICE +49 -0
  45. agentforge_framework/skills/domain-modeling/ADR-FORMAT.md +47 -0
  46. agentforge_framework/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
  47. agentforge_framework/skills/domain-modeling/SKILL.md +74 -0
  48. agentforge_framework/skills/domain-modeling/agents/openai.yaml +3 -0
  49. agentforge_framework/skills/grill-with-docs/SKILL.md +76 -0
  50. agentforge_framework/skills/grilling/SKILL.md +28 -0
  51. agentforge_framework/skills/grilling/agents/openai.yaml +3 -0
  52. agentforge_framework/skills/to-spec/SKILL.md +75 -0
  53. agentforge_framework/skills/to-spec/agents/openai.yaml +5 -0
  54. agentforge_framework/skills/to-tickets/SKILL.md +105 -0
  55. agentforge_framework/skills/to-tickets/agents/openai.yaml +5 -0
  56. agentforge_framework/skills/unslop/SKILL.md +131 -0
  57. agentforge_framework/skills/unslop/evals/fixtures/silhouette/human_reference.json +66 -0
  58. agentforge_framework/skills/unslop/scripts/_lang.py +106 -0
  59. agentforge_framework/skills/unslop/scripts/banned_phrase_scan.py +784 -0
  60. agentforge_framework/skills/unslop/scripts/calibrate_pairs.py +580 -0
  61. agentforge_framework/skills/unslop/scripts/calibrate_score.py +273 -0
  62. agentforge_framework/skills/unslop/scripts/check_packs.py +80 -0
  63. agentforge_framework/skills/unslop/scripts/check_suggestions.py +225 -0
  64. agentforge_framework/skills/unslop/scripts/contribute.py +373 -0
  65. agentforge_framework/skills/unslop/scripts/diff_check.py +139 -0
  66. agentforge_framework/skills/unslop/scripts/extract_constraints.py +201 -0
  67. agentforge_framework/skills/unslop/scripts/harvest_classify.py +223 -0
  68. agentforge_framework/skills/unslop/scripts/harvest_samples.py +534 -0
  69. agentforge_framework/skills/unslop/scripts/readability_metrics.py +295 -0
  70. agentforge_framework/skills/unslop/scripts/refresh_status.py +154 -0
  71. agentforge_framework/skills/unslop/scripts/silhouette_scan.py +390 -0
  72. agentforge_framework/skills/unslop/scripts/structure_scan.py +322 -0
  73. agentforge_framework/skills/unslop/scripts/suggest.py +211 -0
  74. agentforge_framework/skills/unslop/scripts/validate_preservation.py +409 -0
  75. agentforge_framework/skills/unslop/scripts/voice_card.py +496 -0
  76. agentforge_framework/skills/unslop/scripts/voice_profile.py +194 -0
  77. agentforge_framework/skills/unslop/scripts/voice_score.py +271 -0
  78. agentforge_framework/skills/unslop/scripts/wiki_sync.py +479 -0
  79. agentforge_framework/skills/write-plainly/SKILL.md +94 -0
  80. agentforge_framework/workflows/bugfix.yaml +8 -0
  81. agentforge_framework/workflows/feature.yaml +16 -0
  82. agentforge_framework/workflows/review.yaml +10 -0
  83. agentforge_framework-0.2.0.dist-info/METADATA +321 -0
  84. agentforge_framework-0.2.0.dist-info/RECORD +89 -0
  85. agentforge_framework-0.2.0.dist-info/WHEEL +5 -0
  86. agentforge_framework-0.2.0.dist-info/entry_points.txt +3 -0
  87. agentforge_framework-0.2.0.dist-info/licenses/LICENSE +202 -0
  88. agentforge_framework-0.2.0.dist-info/licenses/src/agentforge_framework/skills/NOTICE +49 -0
  89. 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:]))