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,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