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,201 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Extract must-preserve constraints from input text.
4
+
5
+ Identifies facts, names, dates, URLs, numbers that must survive transformation.
6
+ Outputs JSON with constraint spans for validation.
7
+
8
+ Usage:
9
+ python extract_constraints.py < input.txt
10
+ python extract_constraints.py input.txt
11
+ echo "Text here" | python extract_constraints.py
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import argparse
17
+ import sys
18
+ import re
19
+ import json
20
+ from typing import TypedDict
21
+
22
+
23
+ class Constraint(TypedDict):
24
+ type: str
25
+ value: str
26
+ start: int
27
+ end: int
28
+
29
+
30
+ PATTERNS: dict[str, str] = {
31
+ # Currency with amounts
32
+ "currency": r"\$[\d,]+\.?\d*[KMBkmb]?(?:\s*(?:million|billion|thousand))?",
33
+
34
+ # Percentages
35
+ "percentage": r"\d+\.?\d*%",
36
+
37
+ # ISO dates
38
+ "date_iso": r"\d{4}-\d{2}-\d{2}",
39
+
40
+ # Quarter dates
41
+ "date_quarter": r"Q[1-4]\s+\d{4}",
42
+
43
+ # Natural dates
44
+ "date_natural": r"(?:January|February|March|April|May|June|July|August|September|October|November|December|Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]*\.?\s+\d{1,2}(?:st|nd|rd|th)?,?\s+\d{4}",
45
+
46
+ # Years (standalone, 1900-2099)
47
+ "year": r"\b(?:19|20)\d{2}\b",
48
+
49
+ # Times
50
+ "time": r"\d{1,2}:\d{2}(?::\d{2})?\s*(?:AM|PM|am|pm|UTC|PST|EST|CST|MST|GMT)?",
51
+
52
+ # Numeric quantities with magnitude words
53
+ "magnitude_number": r"\b\d[\d,]*\.?\d*\s+(?:thousand|million|billion|trillion)\b",
54
+
55
+ # Measurements with units
56
+ "measurement": r"\d+\.?\d*\s*(?:°C|°F|degrees?\s*(?:C|F|Celsius|Fahrenheit)?|ms|s|sec|min|hr|hour|day|week|month|year|KB|MB|GB|TB|PB|kg|g|lb|oz|m|km|mi|ft|in|cm|mm|px|em|rem|%)\b",
57
+
58
+ # Phone numbers (capture the whole number, before "range" can grab a slice)
59
+ "phone": r"\b(?:\+?1[-.\s]?)?(?:\(\d{3}\)\s*|\d{3}[-.\s])\d{3}[-.\s]\d{4}\b",
60
+
61
+ # Ranges (numeric)
62
+ "range": r"\d+\.?\d*\s*[-–]\s*\d+\.?\d*(?:\s*(?:K|M|B|%|years?|months?|days?))?",
63
+
64
+ # URLs
65
+ "url": r"https?://[^\s\)\]\>\"\']+",
66
+
67
+ # Email addresses
68
+ "email": r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}",
69
+
70
+ # Code references (backticked)
71
+ "code": r"`[^`]+`",
72
+
73
+ # Direct quotes (longer than 10 chars)
74
+ "quote": r'["“][^"“”]{10,}["”]',
75
+
76
+ # Cited references (Section 12(b), Article 5, Figure 2, Table 1, Eq. 3 …) —
77
+ # must-preserve per fact-preservation.md and easy to silently drop.
78
+ "reference": r"\b(?:Sections?|Sec\.|§|Articles?|Clauses?|Paragraphs?|Para\.|Figures?|Fig\.|Tables?|Appendix|Appendices|Schedule|Exhibit|Equations?|Eq\.|Chapters?|Rules?|Items?)\s+\d+[A-Za-z]?(?:\([a-z0-9]+\))?(?:[.\-]\d+)*",
79
+
80
+ # Version numbers
81
+ "version": r"v?\d+\.\d+(?:\.\d+)?(?:-[a-zA-Z0-9]+)?",
82
+
83
+ # API endpoints
84
+ "api_endpoint": r"(?<![\w])/(?:api|v\d+)(?:/[\w-]+)+|(?<![\w])/[\w-]+(?:/[\w-]+){2,}",
85
+
86
+ # Inclusive disjunction — collapsing and/or to a plain conjunction changes scope
87
+ "and_or": r"\band/or\b",
88
+
89
+ # Numeric counts with context
90
+ "count": r"\b\d+(?:,\d{3})*\s+(?:users?|customers?|employees?|companies?|teams?|people|engineers?|developers?|items?|products?|orders?|transactions?|requests?|queries?|rows?|records?)\b",
91
+ }
92
+
93
+ # Proper noun patterns (simplified - real implementation would use NER)
94
+ PROPER_NOUN_INDICATORS = [
95
+ r"\b[A-Z][a-z]+(?:\s+[A-Z][a-z]+)+\b", # Multi-word capitalized (John Smith)
96
+ r"\b[A-Z][a-z]+\s+(?:Inc|Corp|LLC|Ltd|Co)\b\.?", # Company suffixes
97
+ r"\b(?:Dr|Mr|Ms|Mrs|Prof)\.?\s+[A-Z][a-z]+\b", # Titles with names
98
+ ]
99
+
100
+
101
+ def extract_constraints(text: str) -> list[Constraint]:
102
+ """Extract all must-preserve constraints from text."""
103
+ constraints: list[Constraint] = []
104
+ seen_spans: set[tuple[int, int]] = set()
105
+
106
+ # Extract pattern-based constraints
107
+ for constraint_type, pattern in PATTERNS.items():
108
+ for match in re.finditer(pattern, text, re.IGNORECASE if constraint_type.startswith("date") else 0):
109
+ span = (match.start(), match.end())
110
+ if span not in seen_spans:
111
+ seen_spans.add(span)
112
+ constraints.append({
113
+ "type": constraint_type,
114
+ "value": match.group(),
115
+ "start": match.start(),
116
+ "end": match.end()
117
+ })
118
+
119
+ # Extract proper nouns (simplified)
120
+ for pattern in PROPER_NOUN_INDICATORS:
121
+ for match in re.finditer(pattern, text):
122
+ span = (match.start(), match.end())
123
+ # Don't add if overlapping with existing constraint
124
+ overlaps = any(
125
+ not (span[1] <= existing[0] or span[0] >= existing[1])
126
+ for existing in seen_spans
127
+ )
128
+ if not overlaps:
129
+ seen_spans.add(span)
130
+ constraints.append({
131
+ "type": "proper_noun",
132
+ "value": match.group(),
133
+ "start": match.start(),
134
+ "end": match.end()
135
+ })
136
+
137
+ # Bare quantities: comma-grouped numbers or integers of 4+ digits. These are
138
+ # real facts (50000 fans, 2,500 signups) that the unit/count patterns miss.
139
+ # Skip any that overlap a constraint already claimed (years, currency, phones)
140
+ # so we don't double-count or shred a captured value.
141
+ number_pattern = r"(?<![\d.,])(?:\d{1,3}(?:,\d{3})+|\d{4,})(?![\d.,])"
142
+ for match in re.finditer(number_pattern, text):
143
+ span = (match.start(), match.end())
144
+ overlaps = any(
145
+ not (span[1] <= existing[0] or span[0] >= existing[1])
146
+ for existing in seen_spans
147
+ )
148
+ if not overlaps:
149
+ seen_spans.add(span)
150
+ constraints.append({
151
+ "type": "number",
152
+ "value": match.group(),
153
+ "start": match.start(),
154
+ "end": match.end()
155
+ })
156
+
157
+ # Sort by position
158
+ constraints.sort(key=lambda c: c["start"])
159
+
160
+ return constraints
161
+
162
+
163
+ def parse_args(argv: list[str]) -> argparse.Namespace:
164
+ parser = argparse.ArgumentParser(
165
+ description="Extract must-preserve constraints from input text."
166
+ )
167
+ parser.add_argument("path", nargs="?", help="Path to input text file (default: read stdin)")
168
+ return parser.parse_args(argv)
169
+
170
+
171
+ def main() -> None:
172
+ args = parse_args(sys.argv[1:])
173
+
174
+ # Read input
175
+ if args.path:
176
+ try:
177
+ with open(args.path, 'r', errors="replace") as f:
178
+ text = f.read()
179
+ except OSError as e:
180
+ print(json.dumps({"error": f"Could not read input: {e}", "constraints": []}))
181
+ sys.exit(2)
182
+ else:
183
+ text = sys.stdin.buffer.read().decode("utf-8", errors="replace")
184
+
185
+ if not text.strip():
186
+ print(json.dumps({"error": "No input provided", "constraints": []}))
187
+ sys.exit(1)
188
+
189
+ constraints = extract_constraints(text)
190
+
191
+ output = {
192
+ "input_length": len(text),
193
+ "constraint_count": len(constraints),
194
+ "constraints": constraints
195
+ }
196
+
197
+ print(json.dumps(output, indent=2))
198
+
199
+
200
+ if __name__ == "__main__":
201
+ main()
@@ -0,0 +1,223 @@
1
+ #!/usr/bin/env python3
2
+ """Classify harvested candidates into situation/register coverage cells."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import re
9
+ import sys
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ HERE = Path(__file__).resolve().parent
14
+ sys.path.insert(0, str(HERE))
15
+
16
+ from harvest_samples import DATE_FLOOR, recency_value # noqa: E402
17
+
18
+
19
+ CELLS = [
20
+ "numbers_data",
21
+ "question_addressed",
22
+ "anecdote_markers",
23
+ "disagreement",
24
+ "openings_closings",
25
+ ]
26
+
27
+
28
+ def load_candidates(path: Path) -> list[dict[str, Any]]:
29
+ data = json.loads(path.read_text())
30
+ if isinstance(data, dict):
31
+ return data.get("candidates", [])
32
+ if isinstance(data, list):
33
+ return data
34
+ raise ValueError("candidates file must contain a list or {candidates: [...]}")
35
+
36
+
37
+ def cells_for(text: str) -> list[str]:
38
+ lowered = text.lower()
39
+ cells = []
40
+ if re.search(r"\b\d+(?:[,.]\d+)?%?\b", lowered):
41
+ cells.append("numbers_data")
42
+ if "?" in text or re.search(r"\b(?:why|how|what|when|where|which)\b", lowered):
43
+ cells.append("question_addressed")
44
+ if re.search(r"\b(?:last quarter|yesterday|once|during|when we|i noticed|i remember)\b", lowered):
45
+ cells.append("anecdote_markers")
46
+ if re.search(r"\b(?:disagree|however|instead|not convinced|push back)\b", lowered):
47
+ cells.append("disagreement")
48
+ if re.search(r"\b(?:hi|thanks|best|regards|closing|opening|first off)\b", lowered):
49
+ cells.append("openings_closings")
50
+ return cells
51
+
52
+
53
+ def quality_for(candidate: dict[str, Any], cells: list[str]) -> int:
54
+ words = int(candidate.get("words") or len(re.findall(r"\w+", candidate.get("text", ""))))
55
+ score = 3
56
+ if 40 <= words <= 220:
57
+ score += 1
58
+ if cells:
59
+ score += 1
60
+ if candidate.get("suspect_ai"):
61
+ score -= 2
62
+ if candidate.get("dictated"):
63
+ score -= 1
64
+ return max(1, min(5, score))
65
+
66
+
67
+ def candidate_id(candidate: dict[str, Any], index: int) -> Any:
68
+ return candidate.get("id", index)
69
+
70
+
71
+ def source_position(candidate: dict[str, Any]) -> str:
72
+ source = candidate.get("source", {})
73
+ return str(source.get("line", source.get("offset", "")))
74
+
75
+
76
+ def coverage_from(candidates: list[dict[str, Any]]) -> dict[str, int]:
77
+ coverage = {cell: 0 for cell in CELLS}
78
+ for candidate in candidates:
79
+ for cell in candidate.get("cells", []):
80
+ coverage[cell] = coverage.get(cell, 0) + 1
81
+ return coverage
82
+
83
+
84
+ def rank_enriched(candidates: list[dict[str, Any]]) -> list[int]:
85
+ seen_empty = set()
86
+ rank_rows = []
87
+ for idx, candidate in enumerate(candidates):
88
+ cells = candidate.get("cells", [])
89
+ fills_empty = any(cell not in seen_empty for cell in cells)
90
+ seen_empty.update(cells)
91
+ rank_rows.append((idx, fills_empty))
92
+ ranked = sorted(
93
+ rank_rows,
94
+ key=lambda row: (
95
+ not row[1],
96
+ -int(candidates[row[0]].get("quality") or 0),
97
+ -recency_value(candidates[row[0]]),
98
+ bool(candidates[row[0]].get("suspect_ai")),
99
+ bool(candidates[row[0]].get("dictated")),
100
+ str(candidates[row[0]].get("source", {}).get("path", "")),
101
+ source_position(candidates[row[0]]),
102
+ row[0],
103
+ ),
104
+ )
105
+ return [candidate_id(candidates[idx], idx) for idx, _ in ranked]
106
+
107
+
108
+ def heuristic(candidates: list[dict[str, Any]]) -> dict[str, Any]:
109
+ enriched = []
110
+ seen_empty = set()
111
+ for idx, candidate in enumerate(candidates):
112
+ cells = cells_for(candidate.get("text", ""))
113
+ quality = quality_for(candidate, cells)
114
+ fills_empty = any(cell not in seen_empty for cell in cells)
115
+ seen_empty.update(cells)
116
+ enriched.append({
117
+ "index": idx,
118
+ "id": candidate.get("id", idx),
119
+ "cells": cells,
120
+ "quality": quality,
121
+ "why": "lexical heuristic matched " + (", ".join(cells) if cells else "no named cell"),
122
+ "fills_empty_coverage_cell": fills_empty,
123
+ "source": candidate.get("source", {}),
124
+ "suspect_ai": candidate.get("suspect_ai"),
125
+ "dictated": candidate.get("dictated"),
126
+ })
127
+ return {
128
+ "coverage_matrix": coverage_from(enriched),
129
+ "candidates": enriched,
130
+ "ranking": rank_enriched(enriched),
131
+ }
132
+
133
+
134
+ def write_agent_tasks(candidates: list[dict[str, Any]], out_dir: Path) -> dict[str, Any]:
135
+ out_dir.mkdir(parents=True, exist_ok=True)
136
+ chunks = []
137
+ prompt = (
138
+ "Classify each candidate into WP10b situation/register cells. "
139
+ "Return JSONL rows with candidate_index, cells, quality 1-5, and one-line why. "
140
+ "Cells include numbers_data, question_addressed, anecdote_markers, "
141
+ "disagreement, openings_closings, plus any clearly justified additional cell."
142
+ )
143
+ for start in range(0, len(candidates), 10):
144
+ chunk = candidates[start:start + 10]
145
+ path = out_dir / f"harvest-classify-{start // 10 + 1:03d}.json"
146
+ payload = {
147
+ "contract": "tier-1-pack-detector",
148
+ "prompt": prompt,
149
+ "candidates": [
150
+ {"candidate_index": start + i, "text": c.get("text", ""), "source": c.get("source", {})}
151
+ for i, c in enumerate(chunk)
152
+ ],
153
+ }
154
+ path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n")
155
+ chunks.append(str(path))
156
+ return {"task_files": chunks, "chunk_size": 10}
157
+
158
+
159
+ def result_candidate_key(row: dict[str, Any]) -> Any:
160
+ for key in ("candidate_id", "id", "candidate_index", "index"):
161
+ if key in row:
162
+ return row[key]
163
+ raise ValueError("result row missing candidate id")
164
+
165
+
166
+ def merge_results(candidates_path: Path, results_path: Path) -> dict[str, Any]:
167
+ candidates = load_candidates(candidates_path)
168
+ rows_by_id: dict[Any, dict[str, Any]] = {}
169
+ for line in results_path.read_text().splitlines():
170
+ if line.strip():
171
+ row = json.loads(line)
172
+ rows_by_id[result_candidate_key(row)] = row
173
+
174
+ merged = []
175
+ for idx, candidate in enumerate(candidates):
176
+ cid = candidate_id(candidate, idx)
177
+ row = rows_by_id.get(cid)
178
+ if row is None and idx in rows_by_id:
179
+ row = rows_by_id[idx]
180
+ cells = list(row.get("cells", [])) if row else []
181
+ quality = int(row.get("quality")) if row and row.get("quality") is not None else quality_for(candidate, cells)
182
+ why = str(row.get("why", "no classifier result")) if row else "no classifier result"
183
+ merged.append({
184
+ **candidate,
185
+ "id": cid,
186
+ "cells": cells,
187
+ "quality": max(1, min(5, quality)),
188
+ "why": why,
189
+ })
190
+
191
+ return {
192
+ "coverage_matrix": coverage_from(merged),
193
+ "candidates": merged,
194
+ "ranking": rank_enriched(merged),
195
+ }
196
+
197
+
198
+ def parse_args(argv: list[str]) -> argparse.Namespace:
199
+ parser = argparse.ArgumentParser(description=__doc__)
200
+ parser.add_argument("--candidates", required=True)
201
+ parser.add_argument("--mode", choices=["heuristic", "agent"], default="heuristic")
202
+ parser.add_argument("--out-dir", default="harvest-agent-tasks")
203
+ parser.add_argument("--merge")
204
+ return parser.parse_args(argv)
205
+
206
+
207
+ def main(argv: list[str]) -> int:
208
+ args = parse_args(argv)
209
+ if args.merge:
210
+ print(json.dumps(merge_results(Path(args.candidates), Path(args.merge)), indent=2, sort_keys=True))
211
+ return 0
212
+ candidates = load_candidates(Path(args.candidates))
213
+ if args.mode == "heuristic":
214
+ print(json.dumps(heuristic(candidates), indent=2, sort_keys=True))
215
+ else:
216
+ print(json.dumps(write_agent_tasks(candidates, Path(args.out_dir)), indent=2, sort_keys=True))
217
+ return 0
218
+
219
+
220
+ if __name__ == "__main__":
221
+ import sys
222
+
223
+ raise SystemExit(main(sys.argv[1:]))