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,295 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Calculate readability metrics for transformed text.
4
+
5
+ Outputs:
6
+ - Flesch-Kincaid grade level
7
+ - Sentence length variance
8
+ - Word repetition score
9
+ - Paragraph length stats
10
+
11
+ Usage:
12
+ python readability_metrics.py < input.txt
13
+ python readability_metrics.py input.txt
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import argparse
19
+ import sys
20
+ import re
21
+ import json
22
+ from collections import Counter
23
+ from typing import TypedDict
24
+
25
+
26
+ class ReadabilityMetrics(TypedDict):
27
+ flesch_kincaid_grade: float
28
+ flesch_reading_ease: float
29
+ sentence_count: int
30
+ word_count: int
31
+ avg_sentence_length: float
32
+ sentence_length_variance: float
33
+ min_sentence_length: int
34
+ max_sentence_length: int
35
+ consecutive_similar_length: int
36
+ word_repetition_score: float
37
+ top_repeated_words: list[tuple[str, int]]
38
+ paragraph_count: int
39
+ avg_paragraph_length: float
40
+ flags: list[str]
41
+
42
+
43
+ def count_syllables(word: str) -> int:
44
+ """Count syllables in a word (approximation)."""
45
+ word = word.lower().strip()
46
+ if not word:
47
+ return 0
48
+
49
+ # Handle special cases
50
+ if len(word) <= 3:
51
+ return 1
52
+
53
+ # Count vowel groups
54
+ vowels = "aeiouy"
55
+ count = 0
56
+ prev_is_vowel = False
57
+
58
+ for char in word:
59
+ is_vowel = char in vowels
60
+ if is_vowel and not prev_is_vowel:
61
+ count += 1
62
+ prev_is_vowel = is_vowel
63
+
64
+ # Adjust for silent e
65
+ if word.endswith('e') and count > 1:
66
+ count -= 1
67
+
68
+ # Adjust for -le endings
69
+ if word.endswith('le') and len(word) > 2 and word[-3] not in vowels:
70
+ count += 1
71
+
72
+ return max(1, count)
73
+
74
+
75
+ def split_sentences(text: str) -> list[str]:
76
+ """Split text into sentences."""
77
+ parts = re.split(r'([.!?]["”]?)\s+(?=["“]?[A-Z])', text)
78
+ sentences = []
79
+ current = ""
80
+ for part in parts:
81
+ if re.match(r'[.!?]["”]?$', part):
82
+ current += part
83
+ sentences.append(current.strip())
84
+ current = ""
85
+ else:
86
+ current += part
87
+ if current.strip():
88
+ sentences.append(current.strip())
89
+ # Filter empty and clean up
90
+ return [s.strip() for s in sentences if s.strip()]
91
+
92
+
93
+ def split_staccato_units(text: str) -> list[str]:
94
+ units = re.split(r'\s*[—;]\s*|(?<=[.!?])\s+', text)
95
+ return [u.strip() for u in units if u.strip()]
96
+
97
+
98
+ def split_words(text: str) -> list[str]:
99
+ """Split text into words."""
100
+ # Remove punctuation except hyphens in words. Numeric tokens are real words
101
+ # (a data table is not "empty text"), so keep them.
102
+ text = re.sub(r'[^\w\s-]', ' ', text)
103
+ words = text.lower().split()
104
+ return [w for w in words if w]
105
+
106
+
107
+ def calculate_metrics(text: str) -> ReadabilityMetrics:
108
+ """Calculate all readability metrics."""
109
+ sentences = split_sentences(text)
110
+ words = split_words(text)
111
+ paragraphs = [p.strip() for p in text.split('\n\n') if p.strip()]
112
+
113
+ if not sentences or not words:
114
+ return {
115
+ "flesch_kincaid_grade": 0,
116
+ "flesch_reading_ease": 0,
117
+ "sentence_count": 0,
118
+ "word_count": 0,
119
+ "avg_sentence_length": 0,
120
+ "sentence_length_variance": 0,
121
+ "min_sentence_length": 0,
122
+ "max_sentence_length": 0,
123
+ "consecutive_similar_length": 0,
124
+ "word_repetition_score": 0,
125
+ "top_repeated_words": [],
126
+ "paragraph_count": len(paragraphs),
127
+ "avg_paragraph_length": 0,
128
+ "flags": ["Empty or invalid text"]
129
+ }
130
+
131
+ # Basic counts
132
+ sentence_count = len(sentences)
133
+ word_count = len(words)
134
+ syllable_count = sum(count_syllables(w) for w in words)
135
+
136
+ # Sentence lengths
137
+ sentence_lengths = [len(split_words(s)) for s in sentences]
138
+ avg_sentence_length = word_count / sentence_count if sentence_count else 0
139
+
140
+ # Variance calculation
141
+ if len(sentence_lengths) > 1:
142
+ mean = sum(sentence_lengths) / len(sentence_lengths)
143
+ variance = sum((x - mean) ** 2 for x in sentence_lengths) / len(sentence_lengths)
144
+ else:
145
+ variance = 0
146
+
147
+ # Consecutive similar length sentences
148
+ consecutive_similar = 0
149
+ max_consecutive = 0
150
+ for i in range(1, len(sentence_lengths)):
151
+ # "Similar" = within 3 words of each other
152
+ if abs(sentence_lengths[i] - sentence_lengths[i-1]) <= 3:
153
+ consecutive_similar += 1
154
+ max_consecutive = max(max_consecutive, consecutive_similar)
155
+ else:
156
+ consecutive_similar = 0
157
+
158
+ # Longest run of tiny (<=5-word) sentence-like units — the staccato "anti-slop" cadence.
159
+ # Tracked independently of sentence_count so short de-slopped outputs don't slip.
160
+ staccato_lengths = [len(split_words(s)) for s in split_staccato_units(text)]
161
+ staccato_run = 0
162
+ max_staccato_run = 0
163
+ for length in staccato_lengths:
164
+ if length <= 5:
165
+ staccato_run += 1
166
+ max_staccato_run = max(max_staccato_run, staccato_run)
167
+ else:
168
+ staccato_run = 0
169
+
170
+ # Flesch-Kincaid calculations
171
+ avg_syllables_per_word = syllable_count / word_count if word_count else 0
172
+
173
+ flesch_reading_ease = (
174
+ 206.835
175
+ - (1.015 * avg_sentence_length)
176
+ - (84.6 * avg_syllables_per_word)
177
+ )
178
+
179
+ flesch_kincaid_grade = (
180
+ (0.39 * avg_sentence_length)
181
+ + (11.8 * avg_syllables_per_word)
182
+ - 15.59
183
+ )
184
+
185
+ # Word repetition (excluding common words)
186
+ stop_words = {
187
+ 'the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for',
188
+ 'of', 'with', 'by', 'from', 'is', 'are', 'was', 'were', 'be', 'been',
189
+ 'being', 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would',
190
+ 'could', 'should', 'may', 'might', 'must', 'shall', 'can', 'need',
191
+ 'it', 'its', 'this', 'that', 'these', 'those', 'i', 'you', 'he', 'she',
192
+ 'we', 'they', 'what', 'which', 'who', 'when', 'where', 'why', 'how',
193
+ 'not', 'no', 'yes', 'if', 'then', 'else', 'so', 'as', 'than', 'just'
194
+ }
195
+
196
+ content_words = [w for w in words if w not in stop_words and len(w) > 2]
197
+ word_freq = Counter(content_words)
198
+
199
+ # Repetition score: percentage of content words appearing 3+ times
200
+ repeated_words = {w: c for w, c in word_freq.items() if c >= 3}
201
+ repetition_score = (
202
+ sum(repeated_words.values()) / len(content_words) * 100
203
+ if content_words else 0
204
+ )
205
+
206
+ top_repeated = sorted(word_freq.items(), key=lambda x: x[1], reverse=True)[:10]
207
+
208
+ # Paragraph stats
209
+ para_lengths = [len(split_words(p)) for p in paragraphs]
210
+ avg_para_length = sum(para_lengths) / len(para_lengths) if para_lengths else 0
211
+
212
+ # Generate flags
213
+ flags: list[str] = []
214
+
215
+ if flesch_kincaid_grade > 12:
216
+ flags.append(f"High reading level ({flesch_kincaid_grade:.1f} grade)")
217
+
218
+ if variance < 10 and sentence_count > 3:
219
+ flags.append("Low sentence length variance (monotonous rhythm)")
220
+
221
+ if max_consecutive >= 3:
222
+ flags.append(f"{max_consecutive}+ consecutive similar-length sentences")
223
+
224
+ if repetition_score > 15:
225
+ flags.append(f"High word repetition ({repetition_score:.1f}%)")
226
+
227
+ if avg_sentence_length > 25:
228
+ flags.append(f"Long average sentence length ({avg_sentence_length:.1f} words)")
229
+
230
+ if avg_sentence_length < 8 and sentence_count > 3:
231
+ flags.append("Very short sentences (may feel choppy)")
232
+
233
+ # Staccato cadence is the loudest "anti-slop AI" signature; flag it even on
234
+ # short outputs (no sentence_count gate) so de-slopped text can't hide it.
235
+ if max_staccato_run >= 3:
236
+ flags.append(f"Staccato cadence: {max_staccato_run} consecutive tiny sentences (anti-slop AI tell)")
237
+ elif avg_sentence_length < 6 and sentence_count >= 2:
238
+ flags.append(f"Staccato cadence (avg {avg_sentence_length:.1f} words/sentence — anti-slop AI tell)")
239
+
240
+ if max(sentence_lengths) - min(sentence_lengths) < 5 and sentence_count > 5:
241
+ flags.append("Sentences all similar length (AI tell)")
242
+
243
+ return {
244
+ "flesch_kincaid_grade": round(flesch_kincaid_grade, 1),
245
+ "flesch_reading_ease": round(flesch_reading_ease, 1),
246
+ "sentence_count": sentence_count,
247
+ "word_count": word_count,
248
+ "avg_sentence_length": round(avg_sentence_length, 1),
249
+ "sentence_length_variance": round(variance, 1),
250
+ "min_sentence_length": min(sentence_lengths) if sentence_lengths else 0,
251
+ "max_sentence_length": max(sentence_lengths) if sentence_lengths else 0,
252
+ "consecutive_similar_length": max_consecutive,
253
+ "word_repetition_score": round(repetition_score, 1),
254
+ "top_repeated_words": top_repeated,
255
+ "paragraph_count": len(paragraphs),
256
+ "avg_paragraph_length": round(avg_para_length, 1),
257
+ "flags": flags
258
+ }
259
+
260
+
261
+ def parse_args(argv: list[str]) -> argparse.Namespace:
262
+ parser = argparse.ArgumentParser(
263
+ description="Calculate readability metrics for transformed text."
264
+ )
265
+ parser.add_argument("path", nargs="?", help="Path to input text file (default: read stdin)")
266
+ return parser.parse_args(argv)
267
+
268
+
269
+ def main() -> None:
270
+ args = parse_args(sys.argv[1:])
271
+
272
+ # Read input
273
+ if args.path:
274
+ try:
275
+ with open(args.path, 'r', errors="replace") as f:
276
+ text = f.read()
277
+ except OSError as e:
278
+ print(json.dumps({"error": f"Could not read input: {e}"}))
279
+ sys.exit(2)
280
+ else:
281
+ text = sys.stdin.buffer.read().decode("utf-8", errors="replace")
282
+
283
+ if not text.strip():
284
+ print(json.dumps({"error": "No input provided"}))
285
+ sys.exit(1)
286
+
287
+ metrics = calculate_metrics(text)
288
+ print(json.dumps(metrics, indent=2))
289
+
290
+ # Exit with 1 if any flags (warnings)
291
+ sys.exit(1 if metrics["flags"] else 0)
292
+
293
+
294
+ if __name__ == "__main__":
295
+ main()
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Report staleness of the three inputs the adversarial-refresh promise depends
4
+ on: the Wikipedia wiki-sync state, the model-parity bench, and the newest
5
+ adversarial-eval row. Pure stdlib, network-free, always exits 0 — this is a
6
+ reporter an agent reads before deciding what to run next
7
+ (see references/refresh.md), not a gate that blocks anything itself.
8
+
9
+ Usage:
10
+ python3 scripts/refresh_status.py
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ import re
17
+ import subprocess
18
+ import sys
19
+ from datetime import date, datetime, timezone
20
+ from pathlib import Path
21
+
22
+ ROOT = Path(__file__).resolve().parent.parent
23
+ WIKI_STATE_FILE = ROOT / "scripts" / ".wiki_sync_state.json"
24
+ PIPELINE_DOC = ROOT / "references" / "pipeline.md"
25
+ ADVERSARIAL_EVALS = ROOT / "evals" / "adversarial-evals.json"
26
+
27
+ # Guidance thresholds, not hard limits -- the numbers below are the point at
28
+ # which an agent should proactively check, not a required cadence. Every
29
+ # field is a plain reporter value; nothing here fails the build.
30
+ #
31
+ # Wikipedia's "Signs of AI writing" page is a slow-moving reference page;
32
+ # quarterly is enough to catch drift without chasing noise.
33
+ WIKI_STALE_DAYS = 90
34
+ # The GPT/Anthropic model spectrums shift on the order of months, and the
35
+ # rule in references/pipeline.md already forces a re-run on any co-writer,
36
+ # mimic, or detector-pack change regardless of this threshold -- this is a
37
+ # backstop for the case where no such change happened but time still passed.
38
+ BENCH_STALE_DAYS = 180
39
+ # The adversarial-eval catalog is the product's growth signal (docs/PRODUCT.md,
40
+ # Growth); a two-month gap with no new row is worth a look even if nothing
41
+ # else prompted one.
42
+ ROW_STALE_DAYS = 60
43
+
44
+ # The convention this script relies on in references/pipeline.md: the
45
+ # recorded parity date is written as "Live matrix recorded **YYYY-MM-DD**."
46
+ # If that sentence's wording changes, this regex needs to change with it.
47
+ PIPELINE_DATE_RE = re.compile(
48
+ r"Live matrix recorded \*\*(\d{4}-\d{2}-\d{2})\*\*"
49
+ )
50
+
51
+
52
+ def _today() -> date:
53
+ return datetime.now(timezone.utc).date()
54
+
55
+
56
+ def _days_since(d: date | None) -> int | None:
57
+ if d is None:
58
+ return None
59
+ return (_today() - d).days
60
+
61
+
62
+ def _status(last: date | None, stale_days: int) -> dict:
63
+ days = _days_since(last)
64
+ stale = days is None or days > stale_days
65
+ return {
66
+ "last": last.isoformat() if last else None,
67
+ "days": days,
68
+ "stale": stale,
69
+ }
70
+
71
+
72
+ def _parse_iso_date(value: str) -> date | None:
73
+ """Best-effort parse of an ISO-8601 timestamp (MediaWiki style, 'Z'
74
+ suffix included) down to a plain date."""
75
+ try:
76
+ return datetime.fromisoformat(value.replace("Z", "+00:00")).date()
77
+ except (ValueError, AttributeError):
78
+ return None
79
+
80
+
81
+ def wiki_sync_status() -> dict:
82
+ if not WIKI_STATE_FILE.exists():
83
+ # Never synced. Treat as stale so it surfaces for attention.
84
+ return _status(None, WIKI_STALE_DAYS)
85
+
86
+ last: date | None = None
87
+ try:
88
+ state = json.loads(WIKI_STATE_FILE.read_text())
89
+ last = _parse_iso_date(state.get("last_timestamp", ""))
90
+ except (json.JSONDecodeError, OSError):
91
+ last = None
92
+
93
+ if last is None:
94
+ # Content didn't parse; fall back to the state file's mtime.
95
+ try:
96
+ mtime = WIKI_STATE_FILE.stat().st_mtime
97
+ last = datetime.fromtimestamp(mtime, tz=timezone.utc).date()
98
+ except OSError:
99
+ last = None
100
+
101
+ return _status(last, WIKI_STALE_DAYS)
102
+
103
+
104
+ def parity_bench_status() -> dict:
105
+ last: date | None = None
106
+ try:
107
+ text = PIPELINE_DOC.read_text()
108
+ match = PIPELINE_DATE_RE.search(text)
109
+ if match:
110
+ last = date.fromisoformat(match.group(1))
111
+ except OSError:
112
+ last = None
113
+
114
+ return _status(last, BENCH_STALE_DAYS)
115
+
116
+
117
+ def newest_pattern_row_status() -> dict:
118
+ last: date | None = None
119
+ try:
120
+ result = subprocess.run(
121
+ [
122
+ "git",
123
+ "log",
124
+ "-1",
125
+ "--format=%cs",
126
+ "--",
127
+ str(ADVERSARIAL_EVALS),
128
+ ],
129
+ cwd=str(ROOT),
130
+ capture_output=True,
131
+ text=True,
132
+ timeout=10,
133
+ )
134
+ out = result.stdout.strip()
135
+ if result.returncode == 0 and out:
136
+ last = date.fromisoformat(out)
137
+ except (OSError, subprocess.SubprocessError, ValueError):
138
+ last = None
139
+
140
+ return _status(last, ROW_STALE_DAYS)
141
+
142
+
143
+ def main() -> None:
144
+ output = {
145
+ "wiki_sync": wiki_sync_status(),
146
+ "parity_bench": parity_bench_status(),
147
+ "newest_pattern_row": newest_pattern_row_status(),
148
+ }
149
+ print(json.dumps(output, indent=2))
150
+ sys.exit(0)
151
+
152
+
153
+ if __name__ == "__main__":
154
+ main()