harness-fleet 0.4.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 (74) hide show
  1. harness_fleet/__init__.py +186 -0
  2. harness_fleet/bundler.py +257 -0
  3. harness_fleet/calibrate.py +410 -0
  4. harness_fleet/candidates.py +147 -0
  5. harness_fleet/catalog.py +747 -0
  6. harness_fleet/cli.py +2025 -0
  7. harness_fleet/dag.py +555 -0
  8. harness_fleet/data/routes.seed.json +320 -0
  9. harness_fleet/discover.py +2701 -0
  10. harness_fleet/engine.py +712 -0
  11. harness_fleet/eval.py +409 -0
  12. harness_fleet/export.py +355 -0
  13. harness_fleet/grounding.py +425 -0
  14. harness_fleet/input_data.py +539 -0
  15. harness_fleet/mcp_server.py +445 -0
  16. harness_fleet/migrations/001_control_plane.sql +151 -0
  17. harness_fleet/migrations/002_intelligence_and_policy.sql +53 -0
  18. harness_fleet/migrations/003_profiles.sql +18 -0
  19. harness_fleet/migrations/004_studio_settings.sql +11 -0
  20. harness_fleet/models.py +1199 -0
  21. harness_fleet/packer.py +61 -0
  22. harness_fleet/partner.py +97 -0
  23. harness_fleet/profile.py +70 -0
  24. harness_fleet/providers/antigravity.py +61 -0
  25. harness_fleet/providers/base.py +115 -0
  26. harness_fleet/providers/claude.py +60 -0
  27. harness_fleet/providers/codex.py +126 -0
  28. harness_fleet/providers/cursor.py +69 -0
  29. harness_fleet/providers/demo.py +168 -0
  30. harness_fleet/providers/grok.py +68 -0
  31. harness_fleet/providers/harness.py +383 -0
  32. harness_fleet/providers/muse.py +143 -0
  33. harness_fleet/providers/openai_compatible.py +261 -0
  34. harness_fleet/providers/opencode.py +175 -0
  35. harness_fleet/providers/openrouter.py +232 -0
  36. harness_fleet/providers/registry.py +99 -0
  37. harness_fleet/resources/account_skill/SKILL.md +208 -0
  38. harness_fleet/resources/account_skill/references/discovery-playbook.md +168 -0
  39. harness_fleet/resources/account_skill/references/icp-decomposition.md +62 -0
  40. harness_fleet/resources/account_skill/references/icp-interview.md +192 -0
  41. harness_fleet/resources/account_skill/references/mcp-recipes.md +111 -0
  42. harness_fleet/resources/account_skill/references/scoring-rubric-guide.md +107 -0
  43. harness_fleet/resources/examples/account_research/sample_accounts.csv +11 -0
  44. harness_fleet/resources/examples/account_research/task.json +35 -0
  45. harness_fleet/resources/harness_skill/SKILL.md +57 -0
  46. harness_fleet/resources/harness_skill/agents/openai.yaml +6 -0
  47. harness_fleet/resources/harness_skill/references/operations.md +190 -0
  48. harness_fleet/resources/harness_skill/references/task-contracts.md +72 -0
  49. harness_fleet/resources/partner_skill/SKILL.md +215 -0
  50. harness_fleet/resources/partner_skill/references/discovery-playbook.md +116 -0
  51. harness_fleet/resources/partner_skill/references/ipp-decomposition.md +60 -0
  52. harness_fleet/resources/partner_skill/references/ipp-interview.md +148 -0
  53. harness_fleet/resources/partner_skill/references/mcp-recipes.md +77 -0
  54. harness_fleet/resources/partner_skill/references/scoring-rubric-guide.md +110 -0
  55. harness_fleet/resources/studio/index.html +320 -0
  56. harness_fleet/scoring.py +251 -0
  57. harness_fleet/sessions.py +106 -0
  58. harness_fleet/setup.py +231 -0
  59. harness_fleet/slicer.py +83 -0
  60. harness_fleet/store.py +1863 -0
  61. harness_fleet/studio.py +475 -0
  62. harness_fleet/task.py +640 -0
  63. harness_fleet/ui.py +118 -0
  64. harness_fleet-0.4.0.data/data/share/harness-fleet/schemas/candidate-output-v1.schema.json +123 -0
  65. harness_fleet-0.4.0.data/data/share/harness-fleet/schemas/input-item-v1.schema.json +77 -0
  66. harness_fleet-0.4.0.data/data/share/harness-fleet/schemas/output-v2.schema.json +148 -0
  67. harness_fleet-0.4.0.data/data/share/harness-fleet/schemas/packet-v2.schema.json +738 -0
  68. harness_fleet-0.4.0.data/data/share/harness-fleet/schemas/task-v1.schema.json +139 -0
  69. harness_fleet-0.4.0.dist-info/METADATA +591 -0
  70. harness_fleet-0.4.0.dist-info/RECORD +74 -0
  71. harness_fleet-0.4.0.dist-info/WHEEL +5 -0
  72. harness_fleet-0.4.0.dist-info/entry_points.txt +2 -0
  73. harness_fleet-0.4.0.dist-info/licenses/LICENSE +21 -0
  74. harness_fleet-0.4.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,186 @@
1
+ """Harness Fleet: Coordinated CLI-harness worker fleet with closed fields and exact source evidence."""
2
+ __version__ = "0.4.0"
3
+ import uuid
4
+ from collections.abc import Iterable, Mapping
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from .catalog import RouteCatalog
9
+ from .discover import fetch_text, run_discovery
10
+ from .engine import Engine
11
+ from .export import export_clean_packet
12
+ from .grounding import verify_grounding
13
+ from .input_data import iter_input_items, load_input_items
14
+ from .models import (
15
+ CleanPacket,
16
+ ExtractedItem,
17
+ InputItem,
18
+ ModelOutput,
19
+ QuoteRef,
20
+ TaskSpec,
21
+ )
22
+ from .packer import pack_items
23
+ from .profile import IdealCompanyProfile
24
+ from .slicer import slice_document
25
+ from .store import HarnessStore
26
+ from .task import load_task_spec
27
+
28
+
29
+ def read_packet(packet_path: str) -> dict:
30
+ """Convenience helper for downstream trusted applications to safely load a clean packet."""
31
+ p = Path(packet_path).resolve()
32
+ if not p.exists():
33
+ raise FileNotFoundError(f"Packet file not found: {packet_path}")
34
+ return CleanPacket.model_validate_json(p.read_text(encoding="utf-8")).model_dump(mode="json", by_alias=True)
35
+
36
+ def process(
37
+ task: str | TaskSpec | Path,
38
+ input: str | Path | Iterable[InputItem | dict[str, Any]],
39
+ run_id: str | None = None,
40
+ concurrency: int = 4,
41
+ max_attempts: int = 300,
42
+ output: str | Path | None = None,
43
+ policy=None,
44
+ profile: IdealCompanyProfile | str | Path | None = None,
45
+ db: str | Path | None = None,
46
+ use_active_profile: bool = False,
47
+ **load_kwargs,
48
+ ) -> dict:
49
+ """High-level Python SDK: `harness_fleet.process(task, input, ...) -> packet`.
50
+
51
+ Example:
52
+ import harness_fleet
53
+ packet = harness_fleet.process("my-task", "data.csv", concurrency=8)
54
+ df = packet["records"] # or use export helpers
55
+
56
+ Supports local paths (.csv/.jsonl/.json/.txt/.html/.pdf), lists of InputItem,
57
+ or pandas DataFrames (if installed) via `input=df`.
58
+ Profiles are opt-in: pass `profile=...` or set `use_active_profile=True`.
59
+ """
60
+ import time as _time
61
+
62
+ # Resolve task
63
+ from pathlib import Path as _P # noqa: N814
64
+
65
+ store = HarnessStore(_P(db)) if db is not None else HarnessStore()
66
+ if isinstance(task, TaskSpec):
67
+ spec = task
68
+ store.register_task(spec)
69
+ elif isinstance(task, (str, _P)) and _P(str(task)).is_file():
70
+ spec = load_task_spec(str(task))
71
+ store.register_task(spec)
72
+ else:
73
+ spec = store.get_task(str(task))
74
+
75
+ # Resolve input items (DataFrame support optional)
76
+ items: Any
77
+ raw_items_factory = None
78
+ input_path_str = str(input) if isinstance(input, (str, _P)) else "python-sdk"
79
+ if isinstance(input, list):
80
+ # Keep caller-provided lists lazy through the packer; it validates each
81
+ # record at the same trust boundary as file-backed input.
82
+ items = input
83
+ elif isinstance(input, (str, _P)) and _P(str(input)).is_file():
84
+ source = _P(str(input))
85
+ input_options = dict(load_kwargs)
86
+ def raw_items_factory():
87
+ return iter_input_items(source, **input_options)
88
+ items = raw_items_factory()
89
+ else:
90
+ # Try DataFrame-like (has to_dict)
91
+ try:
92
+ if hasattr(input, "to_dict") and hasattr(input, "columns"):
93
+ # pandas DataFrame: itertuples avoids creating a second full
94
+ # list of row dictionaries. The fallback keeps support for
95
+ # DataFrame-like objects that only expose to_dict().
96
+ def dataframe_items():
97
+ columns = list(input.columns)
98
+ if hasattr(input, "itertuples"):
99
+ rows = (
100
+ dict(zip(columns, values, strict=False))
101
+ for values in input.itertuples(index=False, name=None)
102
+ )
103
+ else:
104
+ rows = iter(input.to_dict(orient="records"))
105
+ for idx, rec in enumerate(rows):
106
+ # Prefer explicit id column
107
+ cand_id = rec.get("item_id") or rec.get("id") or f"row_{idx}"
108
+ txt = rec.get("text") or rec.get("body") or rec.get("content") or rec.get("description") or ""
109
+ if not txt:
110
+ # fallback: first stringifiable column value
111
+ for v in rec.values():
112
+ if isinstance(v, str) and len(v.strip()) > 20:
113
+ txt = v
114
+ break
115
+ yield InputItem(
116
+ item_id=str(cand_id),
117
+ text=str(txt),
118
+ metadata={k: v for k, v in rec.items() if k not in ("item_id", "id", "text", "body", "content")},
119
+ )
120
+
121
+ raw_items_factory = dataframe_items
122
+ items = raw_items_factory()
123
+ elif isinstance(input, Mapping):
124
+ raise TypeError("input mappings are not records; pass an iterable of InputItem/dict records")
125
+ elif isinstance(input, Iterable):
126
+ # Generators and other one-shot iterables are consumed once by
127
+ # Engine, which keeps ingestion bounded without requiring a
128
+ # second pass or a full in-memory copy.
129
+ items = input
130
+ else:
131
+ raise TypeError("unsupported input type")
132
+ except Exception as e:
133
+ raise ValueError(f"Unable to resolve input '{input}': {e}") from e
134
+
135
+ rid = run_id or f"sdk-{_time.time_ns()}-{uuid.uuid4().hex[:8]}"
136
+ out_path = _P(output) if output is not None else _P(f"runs/{rid}/clean_packet.json")
137
+ selected_profile = None
138
+ profile_revision_id = None
139
+ if profile is not None:
140
+ selected_profile = (
141
+ profile
142
+ if isinstance(profile, IdealCompanyProfile)
143
+ else IdealCompanyProfile.load(profile)
144
+ )
145
+ profile_revision_id = store.save_profile(selected_profile)
146
+ elif use_active_profile:
147
+ profile_revision_id = store.active_profile_revision_id("ideal_company")
148
+ if selected_profile is None and profile_revision_id:
149
+ selected_profile = store.load_profile("ideal_company")
150
+ eng = Engine(task=spec, store=store, policy=policy, profile=selected_profile)
151
+ return eng.run_campaign(
152
+ raw_items=items,
153
+ run_id=rid,
154
+ input_path=input_path_str,
155
+ concurrency=concurrency,
156
+ max_attempts=max_attempts,
157
+ output_packet_path=out_path,
158
+ profile_revision_id=profile_revision_id,
159
+ profile=selected_profile,
160
+ raw_items_factory=raw_items_factory,
161
+ )
162
+
163
+ __all__ = [
164
+ "load_task_spec",
165
+ "IdealCompanyProfile",
166
+ "Engine",
167
+ "RouteCatalog",
168
+ "verify_grounding",
169
+ "CleanPacket",
170
+ "ExtractedItem",
171
+ "InputItem",
172
+ "ModelOutput",
173
+ "QuoteRef",
174
+ "TaskSpec",
175
+ "HarnessStore",
176
+ "load_input_items",
177
+ "iter_input_items",
178
+ "fetch_text",
179
+ "run_discovery",
180
+ "slice_document",
181
+ "pack_items",
182
+ "export_clean_packet",
183
+ "read_packet",
184
+ "process",
185
+ "__version__",
186
+ ]
@@ -0,0 +1,257 @@
1
+ """Multi-source entity bundler.
2
+
3
+ Combines raw evidence hits gathered across web crawls, case studies, vendor partner
4
+ registries, B2B review platforms, community/social footprints, and ATS job postings
5
+ into consolidated, section-tagged composite dossiers per canonical entity.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import csv
10
+ import re
11
+ from collections import defaultdict
12
+ from collections.abc import Iterable, Sequence
13
+ from pathlib import Path
14
+ from typing import Any
15
+ from urllib.parse import urlparse
16
+
17
+ from .models import InputItem
18
+
19
+ # Known directory & platform domains mapped to source categories
20
+ REGISTRY_DOMAINS = (
21
+ "partners.amazonaws.com",
22
+ "snowflake.com",
23
+ "appsource.microsoft.com",
24
+ "cloud.google.com",
25
+ "datadoghq.com",
26
+ "ecosystem.hubspot.com",
27
+ "salesforce.com",
28
+ )
29
+
30
+ REVIEW_DOMAINS = (
31
+ "clutch.co",
32
+ "g2.com",
33
+ "goodfirms.co",
34
+ "themanifest.com",
35
+ "upcity.com",
36
+ )
37
+
38
+ COMMUNITY_DOMAINS = (
39
+ "github.com",
40
+ "youtube.com",
41
+ "substack.com",
42
+ "medium.com",
43
+ "dev.to",
44
+ )
45
+
46
+ ATS_DOMAINS = (
47
+ "jobs.ashbyhq.com",
48
+ "boards.greenhouse.io",
49
+ "jobs.lever.co",
50
+ "apply.workable.com",
51
+ "jobs.",
52
+ )
53
+
54
+ CASE_STUDY_PATH_RE = re.compile(r"/(case-stud|work|customers?|success-stor|clients|portfolio)", re.IGNORECASE)
55
+ PRACTICE_PATH_RE = re.compile(r"/(services?|solutions?|practices?|about|partners?|consulting)", re.IGNORECASE)
56
+
57
+
58
+ def canonicalize_entity_id(identifier_or_url: str) -> str:
59
+ """Extract clean, canonical domain/entity id from a URL or raw identifier."""
60
+ raw = (identifier_or_url or "").strip().lower()
61
+ if not raw:
62
+ return "unknown_entity"
63
+
64
+ # If it contains a URL scheme or looks like a URL
65
+ if "://" in raw or "/" in raw:
66
+ parsed = urlparse(raw if "://" in raw else f"https://{raw}")
67
+ host = parsed.netloc.lower().strip()
68
+ path = parsed.path.strip("/")
69
+
70
+ # Strip common prefixes
71
+ if host.startswith("www."):
72
+ host = host[4:]
73
+
74
+ # Handle ATS URLs where company slug is the first path segment
75
+ if any(ats in host for ats in ("ashbyhq.com", "greenhouse.io", "lever.co", "workable.com")):
76
+ parts = [p for p in path.split("/") if p]
77
+ if parts:
78
+ slug = parts[0].replace("-", "_")
79
+ # Append .com if slug looks like a domain name
80
+ return f"{slug}.com" if "." not in slug else slug
81
+
82
+ # Handle vendor registry paths: e.g. partners.amazonaws.com/partners/trace3
83
+ if "partners.amazonaws.com" in host or "snowflake.com" in host:
84
+ parts = [p for p in path.split("/") if p and p not in ("partners", "en-us", "marketplace")]
85
+ if parts:
86
+ slug = parts[-1].replace("-", "_")
87
+ return f"{slug}.com" if "." not in slug else slug
88
+
89
+ # Handle Clutch/G2 profiles: clutch.co/profile/trace3
90
+ if any(rev in host for rev in ("clutch.co", "g2.com")):
91
+ parts = [p for p in path.split("/") if p and p not in ("profile", "it-services", "products")]
92
+ if parts:
93
+ slug = parts[-1].replace("-", "_")
94
+ return f"{slug}.com" if "." not in slug else slug
95
+
96
+ if host:
97
+ return host
98
+
99
+ # Plain text identifier
100
+ cleaned = re.sub(r"[^a-z0-9_.-]+", "_", raw).strip("_.")
101
+ return cleaned or "unknown_entity"
102
+
103
+
104
+ def classify_source_category(source_uri: str, entity_id: str = "") -> str:
105
+ """Classify a source URL into one of the 6 canonical partner evidence categories."""
106
+ uri = (source_uri or "").strip().lower()
107
+ if not uri:
108
+ return "general_web"
109
+
110
+ # 1. Vendor registries
111
+ if any(d in uri for d in REGISTRY_DOMAINS) and "partner" in uri:
112
+ return "vendor_registry"
113
+
114
+ # 2. Review and audit platforms
115
+ if any(d in uri for d in REVIEW_DOMAINS):
116
+ return "b2b_directory_audit"
117
+
118
+ # 3. Community and social platforms
119
+ if any(d in uri for d in COMMUNITY_DOMAINS):
120
+ return "community_and_social"
121
+
122
+ # 4. ATS / Hiring requisitions
123
+ if any(ats in uri for ats in ATS_DOMAINS):
124
+ return "ats_requisitions"
125
+
126
+ # 5. First-party case studies
127
+ if CASE_STUDY_PATH_RE.search(uri):
128
+ return "first_party_case_study"
129
+
130
+ # 6. First-party practices and services
131
+ if PRACTICE_PATH_RE.search(uri):
132
+ return "first_party_practice"
133
+
134
+ # Fallback to first-party if host matches entity domain
135
+ if entity_id and entity_id in uri:
136
+ return "first_party_practice"
137
+
138
+ return "general_web"
139
+
140
+
141
+ def bundle_records(
142
+ records: Iterable[InputItem | dict[str, Any]],
143
+ *,
144
+ min_sources: int = 1,
145
+ min_categories: int = 1,
146
+ ) -> list[InputItem]:
147
+ """Group multi-channel evidence hits by canonical entity into composite dossiers."""
148
+ grouped: dict[str, list[dict[str, Any]]] = defaultdict(list)
149
+
150
+ for item in records:
151
+ if isinstance(item, InputItem):
152
+ raw_id = item.item_id
153
+ text = item.text
154
+ uri = item.source_uri or ""
155
+ metadata = dict(item.metadata or {})
156
+ else:
157
+ raw_id = str(item.get("item_id") or item.get("domain") or item.get("url") or "")
158
+ text = str(item.get("text") or item.get("research") or "")
159
+ uri = str(item.get("source_uri") or item.get("url") or "")
160
+ metadata = dict(item.get("metadata") or {})
161
+
162
+ canonical_id = canonicalize_entity_id(raw_id or uri)
163
+ category = metadata.get("source_category") or classify_source_category(uri, canonical_id)
164
+
165
+ grouped[canonical_id].append({
166
+ "text": text.strip(),
167
+ "source_uri": uri,
168
+ "source_category": category,
169
+ "metadata": metadata,
170
+ })
171
+
172
+ bundled_items: list[InputItem] = []
173
+
174
+ for entity_id, hits in sorted(grouped.items()):
175
+ if len(hits) < min_sources:
176
+ continue
177
+
178
+ categories = sorted({h["source_category"] for h in hits if h["source_category"]})
179
+ if len(categories) < min_categories:
180
+ continue
181
+
182
+ # Format composite document
183
+ sections: list[str] = [f"# Multi-Source Evidence Dossier: {entity_id}\n"]
184
+ all_uris: list[str] = []
185
+
186
+ for i, hit in enumerate(hits, 1):
187
+ cat = hit["source_category"].upper()
188
+ uri_text = hit["source_uri"] or f"source_{i}"
189
+ all_uris.append(uri_text)
190
+ sections.append(
191
+ f"=== SECTION: {cat} (URI: {uri_text}) ===\n"
192
+ f"{hit['text']}\n"
193
+ )
194
+
195
+ composite_text = "\n".join(sections).strip()
196
+
197
+ bundled_items.append(
198
+ InputItem(
199
+ item_id=entity_id,
200
+ text=composite_text,
201
+ source_uri=all_uris[0] if all_uris else "",
202
+ title=f"Partner Dossier: {entity_id}",
203
+ metadata={
204
+ "entity": entity_id,
205
+ "source_count": len(hits),
206
+ "source_categories": categories,
207
+ "category_count": len(categories),
208
+ "source_uris": all_uris,
209
+ },
210
+ )
211
+ )
212
+
213
+ return bundled_items
214
+
215
+
216
+ def load_and_bundle(
217
+ input_path: str | Path,
218
+ *,
219
+ id_column: str | None = None,
220
+ text_column: str | None = None,
221
+ uri_column: str | None = None,
222
+ min_sources: int = 1,
223
+ min_categories: int = 1,
224
+ ) -> list[InputItem]:
225
+ """Load records from CSV or JSONL and bundle by canonical entity."""
226
+ from .input_data import load_input_items
227
+
228
+ raw_items = load_input_items(
229
+ input_path,
230
+ id_column=id_column,
231
+ text_column=text_column,
232
+ uri_column=uri_column,
233
+ )
234
+ return bundle_records(raw_items, min_sources=min_sources, min_categories=min_categories)
235
+
236
+
237
+ def export_bundled_csv(
238
+ bundled_items: Sequence[InputItem],
239
+ output_path: str | Path,
240
+ ) -> Path:
241
+ """Export bundled items to a CSV file ready for harness-fleet run."""
242
+ out = Path(output_path).expanduser().resolve()
243
+ out.parent.mkdir(parents=True, exist_ok=True)
244
+
245
+ with out.open("w", encoding="utf-8", newline="") as f:
246
+ writer = csv.writer(f)
247
+ writer.writerow(["item_id", "text", "source_uri", "source_count", "source_categories"])
248
+ for item in bundled_items:
249
+ meta = item.metadata or {}
250
+ writer.writerow([
251
+ item.item_id,
252
+ item.text,
253
+ item.source_uri or "",
254
+ meta.get("source_count", 1),
255
+ ",".join(meta.get("source_categories", [])),
256
+ ])
257
+ return out