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,479 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Sync unslop rules with Wikipedia's "Signs of AI writing" page.
|
|
4
|
+
|
|
5
|
+
Fetches the latest revision, parses structured data from wikitext,
|
|
6
|
+
and outputs diffs or integration prompts for Claude Code.
|
|
7
|
+
|
|
8
|
+
Subcommands:
|
|
9
|
+
check - Check for updates (exits 0 if no updates, 1 if updates available)
|
|
10
|
+
diff - Output structured JSON diff of changes since last sync
|
|
11
|
+
prompt - Output a Claude Code integration prompt with mapped changes
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
python wiki_sync.py check
|
|
15
|
+
python wiki_sync.py diff
|
|
16
|
+
python wiki_sync.py prompt
|
|
17
|
+
python wiki_sync.py prompt | claude -p --allowedTools Edit,Read,Grep,Glob,Bash
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import argparse
|
|
23
|
+
import hashlib
|
|
24
|
+
import json
|
|
25
|
+
import re
|
|
26
|
+
import sys
|
|
27
|
+
import urllib.request
|
|
28
|
+
import urllib.parse
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
from typing import TypedDict
|
|
31
|
+
|
|
32
|
+
STATE_FILE = Path(__file__).parent / ".wiki_sync_state.json"
|
|
33
|
+
WIKI_PAGE = "Wikipedia:Signs_of_AI_writing"
|
|
34
|
+
API_URL = "https://en.wikipedia.org/w/api.php"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class SyncState(TypedDict):
|
|
38
|
+
last_revision_id: int
|
|
39
|
+
last_timestamp: str
|
|
40
|
+
content_hash: str
|
|
41
|
+
last_content_snapshot: str
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ParsedSection(TypedDict):
|
|
45
|
+
title: str
|
|
46
|
+
level: int
|
|
47
|
+
content: str
|
|
48
|
+
watch_words: list[str]
|
|
49
|
+
examples: list[str]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Change(TypedDict):
|
|
53
|
+
type: str # "new_section", "new_words", "modified", "removed"
|
|
54
|
+
section: str
|
|
55
|
+
details: str
|
|
56
|
+
words: list[str]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def fetch_latest_revision() -> tuple[int, str, str]:
|
|
60
|
+
"""Fetch the latest revision content from Wikipedia.
|
|
61
|
+
|
|
62
|
+
Returns (revision_id, timestamp, wikitext).
|
|
63
|
+
"""
|
|
64
|
+
params = {
|
|
65
|
+
"action": "query",
|
|
66
|
+
"titles": WIKI_PAGE,
|
|
67
|
+
"prop": "revisions",
|
|
68
|
+
"rvslots": "*",
|
|
69
|
+
"rvprop": "ids|timestamp|content",
|
|
70
|
+
"format": "json",
|
|
71
|
+
"formatversion": "2",
|
|
72
|
+
}
|
|
73
|
+
url = f"{API_URL}?{urllib.parse.urlencode(params)}"
|
|
74
|
+
req = urllib.request.Request(url, headers={"User-Agent": "unslop-wiki-sync/1.0"})
|
|
75
|
+
|
|
76
|
+
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
77
|
+
data = json.loads(resp.read().decode("utf-8"))
|
|
78
|
+
|
|
79
|
+
page = data["query"]["pages"][0]
|
|
80
|
+
if "missing" in page:
|
|
81
|
+
print(f"Error: Page '{WIKI_PAGE}' not found.", file=sys.stderr)
|
|
82
|
+
sys.exit(2)
|
|
83
|
+
|
|
84
|
+
revision = page["revisions"][0]
|
|
85
|
+
rev_id = revision["revid"]
|
|
86
|
+
timestamp = revision["timestamp"]
|
|
87
|
+
content = revision["slots"]["main"]["content"]
|
|
88
|
+
return rev_id, timestamp, content
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def get_wikitext(from_file: str | None) -> tuple[int, str, str]:
|
|
92
|
+
"""Return (revision_id, timestamp, wikitext).
|
|
93
|
+
|
|
94
|
+
With `from_file` set, read wikitext from that local path instead of
|
|
95
|
+
hitting the network — used for offline eval/fixture runs. There is no
|
|
96
|
+
real revision for a local file, so revision_id/timestamp are placeholders.
|
|
97
|
+
"""
|
|
98
|
+
if from_file:
|
|
99
|
+
content = Path(from_file).read_text(encoding="utf-8")
|
|
100
|
+
return 0, "from-file", content
|
|
101
|
+
return fetch_latest_revision()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def load_state() -> SyncState | None:
|
|
105
|
+
if not STATE_FILE.exists():
|
|
106
|
+
return None
|
|
107
|
+
with open(STATE_FILE, "r") as f:
|
|
108
|
+
return json.load(f)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def save_state(rev_id: int, timestamp: str, content: str) -> None:
|
|
112
|
+
state: SyncState = {
|
|
113
|
+
"last_revision_id": rev_id,
|
|
114
|
+
"last_timestamp": timestamp,
|
|
115
|
+
"content_hash": hashlib.sha256(content.encode()).hexdigest(),
|
|
116
|
+
"last_content_snapshot": content,
|
|
117
|
+
}
|
|
118
|
+
with open(STATE_FILE, "w") as f:
|
|
119
|
+
json.dump(state, f, indent=2)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def parse_wikitext(wikitext: str) -> list[ParsedSection]:
|
|
123
|
+
"""Parse wikitext into structured sections with watch words and examples."""
|
|
124
|
+
sections: list[ParsedSection] = []
|
|
125
|
+
current_title = "Introduction"
|
|
126
|
+
current_level = 1
|
|
127
|
+
current_lines: list[str] = []
|
|
128
|
+
|
|
129
|
+
for line in wikitext.split("\n"):
|
|
130
|
+
header_match = re.match(r"^(={2,6})\s*(.+?)\s*\1\s*$", line)
|
|
131
|
+
if header_match:
|
|
132
|
+
if current_lines:
|
|
133
|
+
sections.append(_build_section(current_title, current_level, current_lines))
|
|
134
|
+
current_level = len(header_match.group(1))
|
|
135
|
+
current_title = header_match.group(2).strip()
|
|
136
|
+
current_lines = []
|
|
137
|
+
else:
|
|
138
|
+
current_lines.append(line)
|
|
139
|
+
|
|
140
|
+
if current_lines:
|
|
141
|
+
sections.append(_build_section(current_title, current_level, current_lines))
|
|
142
|
+
|
|
143
|
+
return sections
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _build_section(title: str, level: int, lines: list[str]) -> ParsedSection:
|
|
147
|
+
"""Build a ParsedSection from raw lines."""
|
|
148
|
+
content = "\n".join(lines).strip()
|
|
149
|
+
watch_words = _extract_watch_words(content)
|
|
150
|
+
examples = _extract_examples(content)
|
|
151
|
+
return {
|
|
152
|
+
"title": title,
|
|
153
|
+
"level": level,
|
|
154
|
+
"content": content,
|
|
155
|
+
"watch_words": watch_words,
|
|
156
|
+
"examples": examples,
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _extract_watch_words(content: str) -> list[str]:
|
|
161
|
+
"""Extract words-to-watch from tmbox templates and bold/italic markers."""
|
|
162
|
+
words: list[str] = []
|
|
163
|
+
|
|
164
|
+
# Match {{tmbox|text=...}} content
|
|
165
|
+
for match in re.finditer(r"\{\{tmbox\|[^}]*text\s*=\s*([^}]+)\}\}", content, re.DOTALL):
|
|
166
|
+
text = match.group(1)
|
|
167
|
+
# Extract bold words within tmbox
|
|
168
|
+
for bold in re.finditer(r"'''(.+?)'''", text):
|
|
169
|
+
words.append(bold.group(1).strip())
|
|
170
|
+
|
|
171
|
+
# Match bold words in "words to watch" style lists
|
|
172
|
+
for match in re.finditer(r"'''(.+?)'''", content):
|
|
173
|
+
word = match.group(1).strip()
|
|
174
|
+
if word and len(word) < 50:
|
|
175
|
+
words.append(word)
|
|
176
|
+
|
|
177
|
+
# Match items in bulleted lists
|
|
178
|
+
for match in re.finditer(r"^\*\s*'''(.+?)'''", content, re.MULTILINE):
|
|
179
|
+
words.append(match.group(1).strip())
|
|
180
|
+
|
|
181
|
+
return list(dict.fromkeys(words)) # dedupe preserving order
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _extract_examples(content: str) -> list[str]:
|
|
185
|
+
"""Extract example blocks (blockquotes, textdiff templates)."""
|
|
186
|
+
examples: list[str] = []
|
|
187
|
+
|
|
188
|
+
# {{blockquote|...}}
|
|
189
|
+
for match in re.finditer(r"\{\{blockquote\|([^}]+)\}\}", content, re.DOTALL):
|
|
190
|
+
examples.append(match.group(1).strip())
|
|
191
|
+
|
|
192
|
+
# {{textdiff|old=...|new=...}}
|
|
193
|
+
for match in re.finditer(
|
|
194
|
+
r"\{\{textdiff\|[^}]*old\s*=\s*([^|]+)\|[^}]*new\s*=\s*([^}]+)\}\}",
|
|
195
|
+
content, re.DOTALL
|
|
196
|
+
):
|
|
197
|
+
examples.append(f"BEFORE: {match.group(1).strip()}\nAFTER: {match.group(2).strip()}")
|
|
198
|
+
|
|
199
|
+
# Indented blockquote lines (: prefix in wikitext)
|
|
200
|
+
block_lines: list[str] = []
|
|
201
|
+
for line in content.split("\n"):
|
|
202
|
+
if line.startswith(":"):
|
|
203
|
+
block_lines.append(line.lstrip(": "))
|
|
204
|
+
elif block_lines:
|
|
205
|
+
examples.append("\n".join(block_lines))
|
|
206
|
+
block_lines = []
|
|
207
|
+
if block_lines:
|
|
208
|
+
examples.append("\n".join(block_lines))
|
|
209
|
+
|
|
210
|
+
return examples
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def compute_diff(old_sections: list[ParsedSection], new_sections: list[ParsedSection]) -> list[Change]:
|
|
214
|
+
"""Compute structured diff between two parsed page versions."""
|
|
215
|
+
changes: list[Change] = []
|
|
216
|
+
|
|
217
|
+
old_by_title = {s["title"]: s for s in old_sections}
|
|
218
|
+
new_by_title = {s["title"]: s for s in new_sections}
|
|
219
|
+
|
|
220
|
+
for title, new_sec in new_by_title.items():
|
|
221
|
+
if title not in old_by_title:
|
|
222
|
+
changes.append({
|
|
223
|
+
"type": "new_section",
|
|
224
|
+
"section": title,
|
|
225
|
+
"details": f"New section with {len(new_sec['watch_words'])} watch words",
|
|
226
|
+
"words": new_sec["watch_words"],
|
|
227
|
+
})
|
|
228
|
+
else:
|
|
229
|
+
old_sec = old_by_title[title]
|
|
230
|
+
new_words = set(new_sec["watch_words"]) - set(old_sec["watch_words"])
|
|
231
|
+
if new_words:
|
|
232
|
+
changes.append({
|
|
233
|
+
"type": "new_words",
|
|
234
|
+
"section": title,
|
|
235
|
+
"details": f"{len(new_words)} new watch words added",
|
|
236
|
+
"words": sorted(new_words),
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
old_hash = hashlib.sha256(old_sec["content"].encode()).hexdigest()
|
|
240
|
+
new_hash = hashlib.sha256(new_sec["content"].encode()).hexdigest()
|
|
241
|
+
if old_hash != new_hash and not new_words:
|
|
242
|
+
changes.append({
|
|
243
|
+
"type": "modified",
|
|
244
|
+
"section": title,
|
|
245
|
+
"details": "Section content changed (no new watch words)",
|
|
246
|
+
"words": [],
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
for title in old_by_title:
|
|
250
|
+
if title not in new_by_title:
|
|
251
|
+
changes.append({
|
|
252
|
+
"type": "removed",
|
|
253
|
+
"section": title,
|
|
254
|
+
"details": "Section removed from Wikipedia page",
|
|
255
|
+
"words": old_by_title[title]["watch_words"],
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
return changes
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
# Mapping from Wikipedia section themes to unslop target files/sections
|
|
262
|
+
SECTION_MAP: dict[str, dict[str, str]] = {
|
|
263
|
+
"words to watch": {
|
|
264
|
+
"file": "references/taboo-phrases.md",
|
|
265
|
+
"section": "AI Vocabulary (Additional)",
|
|
266
|
+
},
|
|
267
|
+
"promotional": {
|
|
268
|
+
"file": "references/taboo-phrases.md",
|
|
269
|
+
"section": "Promotional Language",
|
|
270
|
+
},
|
|
271
|
+
"significance": {
|
|
272
|
+
"file": "references/taboo-phrases.md",
|
|
273
|
+
"section": "Significance & Legacy Inflation",
|
|
274
|
+
},
|
|
275
|
+
"legacy": {
|
|
276
|
+
"file": "references/taboo-phrases.md",
|
|
277
|
+
"section": "Significance & Legacy Inflation",
|
|
278
|
+
},
|
|
279
|
+
"vague": {
|
|
280
|
+
"file": "references/taboo-phrases.md",
|
|
281
|
+
"section": "Vague Attributions",
|
|
282
|
+
},
|
|
283
|
+
"attribution": {
|
|
284
|
+
"file": "references/taboo-phrases.md",
|
|
285
|
+
"section": "Vague Attributions",
|
|
286
|
+
},
|
|
287
|
+
"copula": {
|
|
288
|
+
"file": "references/taboo-phrases.md",
|
|
289
|
+
"section": "Copula Avoidance",
|
|
290
|
+
},
|
|
291
|
+
"participial": {
|
|
292
|
+
"file": "references/taboo-phrases.md",
|
|
293
|
+
"section": "Superficial -ing Analyses",
|
|
294
|
+
},
|
|
295
|
+
"chatbot": {
|
|
296
|
+
"file": "references/taboo-phrases.md",
|
|
297
|
+
"section": "Communication Artifacts",
|
|
298
|
+
},
|
|
299
|
+
"jargon": {
|
|
300
|
+
"file": "references/taboo-phrases.md",
|
|
301
|
+
"section": "Business Jargon",
|
|
302
|
+
},
|
|
303
|
+
"structure": {
|
|
304
|
+
"file": "references/taboo-phrases.md",
|
|
305
|
+
"section": "Structural Patterns to Avoid",
|
|
306
|
+
},
|
|
307
|
+
"synonym": {
|
|
308
|
+
"file": "references/taboo-phrases.md",
|
|
309
|
+
"section": "Elegant Variation / Synonym Cycling",
|
|
310
|
+
},
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def map_change_to_target(change: Change) -> dict[str, str]:
|
|
315
|
+
"""Map a Wikipedia change to the appropriate unslop file and section."""
|
|
316
|
+
section_lower = change["section"].lower()
|
|
317
|
+
for keyword, target in SECTION_MAP.items():
|
|
318
|
+
if keyword in section_lower:
|
|
319
|
+
return target
|
|
320
|
+
return {
|
|
321
|
+
"file": "references/taboo-phrases.md",
|
|
322
|
+
"section": "AI Vocabulary (Additional)",
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def generate_prompt(changes: list[Change], sections: list[ParsedSection]) -> str:
|
|
327
|
+
"""Generate a Claude Code integration prompt from detected changes."""
|
|
328
|
+
if not changes:
|
|
329
|
+
return "No changes detected since last sync. Nothing to integrate."
|
|
330
|
+
|
|
331
|
+
lines = [
|
|
332
|
+
"# Wikipedia AI Writing Patterns — Integration Update",
|
|
333
|
+
"",
|
|
334
|
+
"The Wikipedia 'Signs of AI writing' page has been updated.",
|
|
335
|
+
"Below are changes that need to be integrated into the unslop skill.",
|
|
336
|
+
"",
|
|
337
|
+
"## Changes Detected",
|
|
338
|
+
"",
|
|
339
|
+
]
|
|
340
|
+
|
|
341
|
+
for i, change in enumerate(changes, 1):
|
|
342
|
+
target = map_change_to_target(change)
|
|
343
|
+
lines.append(f"### {i}. [{change['type'].upper()}] {change['section']}")
|
|
344
|
+
lines.append(f"")
|
|
345
|
+
lines.append(f"**Details:** {change['details']}")
|
|
346
|
+
lines.append(f"**Target file:** `{target['file']}`")
|
|
347
|
+
lines.append(f"**Target section:** {target['section']}")
|
|
348
|
+
if change["words"]:
|
|
349
|
+
lines.append(f"**Words/phrases to add:**")
|
|
350
|
+
for word in change["words"]:
|
|
351
|
+
lines.append(f"- \"{word}\"")
|
|
352
|
+
lines.append("")
|
|
353
|
+
|
|
354
|
+
lines.extend([
|
|
355
|
+
"## Integration Instructions",
|
|
356
|
+
"",
|
|
357
|
+
"For each change above:",
|
|
358
|
+
"",
|
|
359
|
+
"1. Read the target file",
|
|
360
|
+
"2. Check if the phrase already exists (avoid duplicates)",
|
|
361
|
+
"3. Add new phrases to the appropriate section in `references/taboo-phrases.md`",
|
|
362
|
+
"4. Mirror additions in `scripts/banned_phrase_scan.py` BANNED_PHRASES dict",
|
|
363
|
+
"5. If a new before/after example is warranted, add to `references/edit-library.md`",
|
|
364
|
+
"",
|
|
365
|
+
"After all changes:",
|
|
366
|
+
"- Run `python3 scripts/banned_phrase_scan.py < /dev/null` to verify no syntax errors",
|
|
367
|
+
"- Verify no duplicate entries in taboo-phrases.md",
|
|
368
|
+
])
|
|
369
|
+
|
|
370
|
+
return "\n".join(lines)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def cmd_check(from_file: str | None = None) -> None:
|
|
374
|
+
"""Check for updates. Exit 0 = no updates, 1 = updates available."""
|
|
375
|
+
rev_id, timestamp, content = get_wikitext(from_file)
|
|
376
|
+
state = load_state()
|
|
377
|
+
|
|
378
|
+
content_hash = hashlib.sha256(content.encode()).hexdigest()
|
|
379
|
+
|
|
380
|
+
if state is None:
|
|
381
|
+
print(f"No previous sync state. Current revision: {rev_id} ({timestamp})")
|
|
382
|
+
print("Run 'diff' or 'prompt' to see all content as new.")
|
|
383
|
+
sys.exit(1)
|
|
384
|
+
|
|
385
|
+
if content_hash == state["content_hash"]:
|
|
386
|
+
print(f"No changes. Current revision: {rev_id} ({timestamp})")
|
|
387
|
+
print(f"Last synced revision: {state['last_revision_id']} ({state['last_timestamp']})")
|
|
388
|
+
sys.exit(0)
|
|
389
|
+
|
|
390
|
+
print(f"Updates available!")
|
|
391
|
+
print(f" Last synced: revision {state['last_revision_id']} ({state['last_timestamp']})")
|
|
392
|
+
print(f" Current: revision {rev_id} ({timestamp})")
|
|
393
|
+
sys.exit(1)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def cmd_diff(from_file: str | None = None) -> None:
|
|
397
|
+
"""Output structured JSON diff of changes."""
|
|
398
|
+
rev_id, timestamp, content = get_wikitext(from_file)
|
|
399
|
+
state = load_state()
|
|
400
|
+
|
|
401
|
+
new_sections = parse_wikitext(content)
|
|
402
|
+
|
|
403
|
+
if state is None:
|
|
404
|
+
old_sections: list[ParsedSection] = []
|
|
405
|
+
else:
|
|
406
|
+
old_sections = parse_wikitext(state["last_content_snapshot"])
|
|
407
|
+
|
|
408
|
+
changes = compute_diff(old_sections, new_sections)
|
|
409
|
+
|
|
410
|
+
output = {
|
|
411
|
+
"revision_id": rev_id,
|
|
412
|
+
"timestamp": timestamp,
|
|
413
|
+
"previous_revision_id": state["last_revision_id"] if state else None,
|
|
414
|
+
"total_changes": len(changes),
|
|
415
|
+
"changes": changes,
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
print(json.dumps(output, indent=2))
|
|
419
|
+
if not from_file:
|
|
420
|
+
save_state(rev_id, timestamp, content)
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def cmd_prompt(from_file: str | None = None) -> None:
|
|
424
|
+
"""Output a Claude Code integration prompt."""
|
|
425
|
+
rev_id, timestamp, content = get_wikitext(from_file)
|
|
426
|
+
state = load_state()
|
|
427
|
+
|
|
428
|
+
new_sections = parse_wikitext(content)
|
|
429
|
+
|
|
430
|
+
if state is None:
|
|
431
|
+
old_sections: list[ParsedSection] = []
|
|
432
|
+
else:
|
|
433
|
+
old_sections = parse_wikitext(state["last_content_snapshot"])
|
|
434
|
+
|
|
435
|
+
changes = compute_diff(old_sections, new_sections)
|
|
436
|
+
prompt = generate_prompt(changes, new_sections)
|
|
437
|
+
|
|
438
|
+
print(prompt)
|
|
439
|
+
if not from_file:
|
|
440
|
+
save_state(rev_id, timestamp, content)
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def main() -> None:
|
|
444
|
+
parser = argparse.ArgumentParser(
|
|
445
|
+
description="Sync unslop rules with Wikipedia's 'Signs of AI writing' page."
|
|
446
|
+
)
|
|
447
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
448
|
+
|
|
449
|
+
commands = {
|
|
450
|
+
"check": cmd_check,
|
|
451
|
+
"diff": cmd_diff,
|
|
452
|
+
"prompt": cmd_prompt,
|
|
453
|
+
}
|
|
454
|
+
for name in commands:
|
|
455
|
+
sub = subparsers.add_parser(name)
|
|
456
|
+
sub.add_argument(
|
|
457
|
+
"--from-file",
|
|
458
|
+
metavar="PATH",
|
|
459
|
+
help=(
|
|
460
|
+
"read wikitext from PATH instead of fetching from Wikipedia "
|
|
461
|
+
"(skips the network call and skips writing sync state)"
|
|
462
|
+
),
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
if len(sys.argv) < 2:
|
|
466
|
+
print("Usage: wiki_sync.py <check|diff|prompt>", file=sys.stderr)
|
|
467
|
+
sys.exit(2)
|
|
468
|
+
|
|
469
|
+
if sys.argv[1] not in commands:
|
|
470
|
+
print(f"Unknown command: {sys.argv[1]}", file=sys.stderr)
|
|
471
|
+
print(f"Available: {', '.join(commands)}", file=sys.stderr)
|
|
472
|
+
sys.exit(2)
|
|
473
|
+
|
|
474
|
+
args = parser.parse_args()
|
|
475
|
+
commands[args.command](args.from_file)
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
if __name__ == "__main__":
|
|
479
|
+
main()
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: write-plainly
|
|
3
|
+
description: The tells three deterministic scanners count, and how to get past them on the first attempt. Use before drafting anything a human reads at the end of a Run.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Write plainly
|
|
7
|
+
|
|
8
|
+
Whatever you produce here is scanned before anybody sees it. Three scripts count
|
|
9
|
+
things: phrases from a fixed list, sentence rhythms, and the arrangement of the
|
|
10
|
+
whole document. None of them can judge whether your review is any good. They can
|
|
11
|
+
tell that it was assembled rather than written, and their report is attached to
|
|
12
|
+
your work either way, so a person sees the verdict beside the prose.
|
|
13
|
+
|
|
14
|
+
You get two rewrites, and a rewrite can only reach a phrase. The tells that
|
|
15
|
+
matter most are further down this page, in the arrangement, where no
|
|
16
|
+
find-and-replace will reach them. Spend the attempt now instead.
|
|
17
|
+
|
|
18
|
+
## Words already on a fixed list
|
|
19
|
+
|
|
20
|
+
Corporate verbs with an abstract object are the largest group: `leverage` our
|
|
21
|
+
capabilities, `harness` the power of, `foster` collaboration, `delve into` the
|
|
22
|
+
implications, `navigate` the complex landscape, `unpack` the argument. Reach for
|
|
23
|
+
the plainest verb that fits — use, build, handle, read.
|
|
24
|
+
|
|
25
|
+
Then the ones that inflate, hedge, or credit nobody:
|
|
26
|
+
|
|
27
|
+
- `robust`, `comprehensive`, `game changer`, `synergy`, `treasure trove`
|
|
28
|
+
- `plays a crucial role`, `underscore the importance`, `boasts a rich array of`
|
|
29
|
+
- `research shows` and `analysts predict`, with no person or paper named
|
|
30
|
+
- `the data tells a story`, `the numbers speak for themselves`
|
|
31
|
+
- `could potentially`, which hedges twice for one hedge's worth of doubt
|
|
32
|
+
- `in conclusion`, `at the end of the day`, `here's the thing`, `in today's`
|
|
33
|
+
- `as an AI language model`, `as of my last update`
|
|
34
|
+
|
|
35
|
+
Each of those is a sentence that has stopped saying anything. A claim about the
|
|
36
|
+
diff carries a file and a line; a claim that some part of it matters carries the
|
|
37
|
+
effect on somebody who runs this code. Say things of that kind and the report is
|
|
38
|
+
worth the minute it takes to read.
|
|
39
|
+
|
|
40
|
+
## Sentence shapes it counts
|
|
41
|
+
|
|
42
|
+
Several constructions are matched on sight, whatever they are about:
|
|
43
|
+
|
|
44
|
+
- `not only X but also Y`, and `it's not just X — it's Y`
|
|
45
|
+
- `serves as a testament to`, `functions as a solution`, `constitutes a shift`
|
|
46
|
+
- a contrast split across two sentences: `Not because X. Because Y.`
|
|
47
|
+
- a single word standing in for emphasis: `Full stop.`
|
|
48
|
+
- a question you then answer yourself: `Why does this matter?`
|
|
49
|
+
- an -ing tail bolted to the end, `, ensuring reliability`, past roughly one
|
|
50
|
+
sentence in seven
|
|
51
|
+
|
|
52
|
+
Two more are counted rather than matched, and both are about rhythm. Sentences
|
|
53
|
+
of near-identical length across a passage read as generated, so let some run
|
|
54
|
+
long and cut others to four words. Stacked short fragments read as generated
|
|
55
|
+
too — clipped, punchy, hard-boiled — and that register is itself on the list, so
|
|
56
|
+
the cure for padding is not telegraphese. Whether a given line survives is the
|
|
57
|
+
wrong question to ask. What reads evenly for ten sentences is caught by the
|
|
58
|
+
spread, not by any one of them.
|
|
59
|
+
|
|
60
|
+
## The arrangement of the whole document
|
|
61
|
+
|
|
62
|
+
This is the part a rewrite cannot reach, and the reason to get it right in the
|
|
63
|
+
draft. A single scan can burn all three of your attempts here while every
|
|
64
|
+
individual phrase in the prose passes on its own. The tell is structural, so
|
|
65
|
+
settle the structure first and let the wording follow it instead of leading.
|
|
66
|
+
|
|
67
|
+
Openers first. A paragraph beginning `However` or `Moreover` or `Additionally`
|
|
68
|
+
spends its opening words on its relationship to the paragraph above instead of
|
|
69
|
+
on its own claim. Three such paragraphs flag, and rotating through several
|
|
70
|
+
different cues flags harder than repeating one. So does narrating your own
|
|
71
|
+
structure — `First,` then `Next,` then `As mentioned above` — and so does
|
|
72
|
+
opening four sentences in a row on one word, or two paragraphs on `Every ___
|
|
73
|
+
is`.
|
|
74
|
+
|
|
75
|
+
Then the outline. Do not announce what a document will cover and then cover it.
|
|
76
|
+
An intro that lists the topics below, headings that restate that list, and body
|
|
77
|
+
paragraphs that open on the intro's words are three separate counts of a single
|
|
78
|
+
habit. The ending is counted the same way: a last paragraph that circles back to
|
|
79
|
+
the vocabulary of the first reads as a recap loop rather than a finish. Stop on
|
|
80
|
+
the most specific thing you have.
|
|
81
|
+
|
|
82
|
+
Formatting is counted too. Three or more lines of a bold label followed by a
|
|
83
|
+
colon read as a filled-in template, whatever is in them.
|
|
84
|
+
|
|
85
|
+
## Say it once
|
|
86
|
+
|
|
87
|
+
Report what changed, where, and whether it does what the Plan said. Put what
|
|
88
|
+
matters most at the top, because somebody may stop reading after a paragraph.
|
|
89
|
+
Where something does not match, quote the line and give its file. Where the Run
|
|
90
|
+
did what it promised, say so once and move on.
|
|
91
|
+
|
|
92
|
+
Prose somebody can act on tends to clear these scans without being aimed at
|
|
93
|
+
them. Every tell above is what writing does when it has run out of things to
|
|
94
|
+
say, and you have something to say: you have just read the diff.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# A fix, verified, and reported on. No Security Step: a bug fix that touches
|
|
2
|
+
# auth is a Task the Orchestrator routes to `feature` instead, which is a
|
|
3
|
+
# judgement about the Task rather than a check that can be automated here.
|
|
4
|
+
name: bugfix
|
|
5
|
+
steps:
|
|
6
|
+
- role: implementer
|
|
7
|
+
- role: tester
|
|
8
|
+
- role: reviewer
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# The default Workflow: build something that was not there before.
|
|
2
|
+
#
|
|
3
|
+
# The Reviewer speaks last because it reports on what everything before it did.
|
|
4
|
+
#
|
|
5
|
+
# No Gate is declared here on purpose: a Gate suspends the Run until it clears,
|
|
6
|
+
# and the default Workflow stopping to wait on somebody is a choice a project
|
|
7
|
+
# makes rather than one it inherits. Add `gate: human` to a step to insert
|
|
8
|
+
# yourself after it, `gate: tests` to hold it on the repository's own suite, or
|
|
9
|
+
# `gate: security` to hold it on a clean audit; the kinds are registered in
|
|
10
|
+
# `core/gates.py`.
|
|
11
|
+
name: feature
|
|
12
|
+
steps:
|
|
13
|
+
- role: implementer
|
|
14
|
+
- role: tester
|
|
15
|
+
- role: security
|
|
16
|
+
- role: reviewer
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# An audit and a write-up of a diff AgentForge did not produce: point it at a
|
|
2
|
+
# branch somebody else wrote and it reports on that.
|
|
3
|
+
#
|
|
4
|
+
# The only shipped Workflow with no Implementer, which is what makes it worth
|
|
5
|
+
# shipping twice over — it is the check on whether the runtime has quietly
|
|
6
|
+
# assumed it wrote the diff itself.
|
|
7
|
+
name: review
|
|
8
|
+
steps:
|
|
9
|
+
- role: security
|
|
10
|
+
- role: reviewer
|