feed-the-machine 1.5.0 → 1.6.0
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.
- package/LICENSE +21 -21
- package/README.md +170 -170
- package/bin/generate-manifest.mjs +463 -463
- package/bin/install.mjs +491 -491
- package/docs/HOOKS.md +243 -243
- package/docs/INBOX.md +233 -233
- package/ftm/SKILL.md +122 -122
- package/ftm-audit/SKILL.md +623 -541
- package/ftm-audit/references/protocols/PROJECT-PATTERNS.md +91 -91
- package/ftm-audit/references/protocols/RUNTIME-WIRING.md +66 -66
- package/ftm-audit/references/protocols/WIRING-CONTRACTS.md +135 -135
- package/ftm-audit/references/strategies/AUTO-FIX-STRATEGIES.md +69 -69
- package/ftm-audit/references/templates/REPORT-FORMAT.md +96 -96
- package/ftm-audit/scripts/run-knip.sh +23 -23
- package/ftm-audit.yml +2 -2
- package/ftm-brainstorm/SKILL.md +498 -498
- package/ftm-brainstorm/evals/evals.json +100 -100
- package/ftm-brainstorm/evals/promptfoo.yaml +109 -109
- package/ftm-brainstorm/references/agent-prompts.md +224 -224
- package/ftm-brainstorm/references/plan-template.md +121 -121
- package/ftm-brainstorm.yml +2 -2
- package/ftm-browse/SKILL.md +454 -454
- package/ftm-browse/daemon/browser-manager.ts +206 -206
- package/ftm-browse/daemon/bun.lock +30 -30
- package/ftm-browse/daemon/cli.ts +347 -347
- package/ftm-browse/daemon/commands.ts +410 -410
- package/ftm-browse/daemon/main.ts +357 -357
- package/ftm-browse/daemon/package.json +17 -17
- package/ftm-browse/daemon/server.ts +189 -189
- package/ftm-browse/daemon/snapshot.ts +519 -519
- package/ftm-browse/daemon/tsconfig.json +22 -22
- package/ftm-browse.yml +4 -4
- package/ftm-capture/SKILL.md +370 -370
- package/ftm-capture.yml +4 -4
- package/ftm-codex-gate/SKILL.md +361 -361
- package/ftm-codex-gate.yml +2 -2
- package/ftm-config/SKILL.md +345 -345
- package/ftm-config.default.yml +82 -80
- package/ftm-config.yml +2 -2
- package/ftm-council/SKILL.md +416 -416
- package/ftm-council/references/prompts/CLAUDE-INVESTIGATION.md +60 -60
- package/ftm-council/references/prompts/CODEX-INVESTIGATION.md +58 -58
- package/ftm-council/references/prompts/GEMINI-INVESTIGATION.md +58 -58
- package/ftm-council/references/prompts/REBUTTAL-TEMPLATE.md +57 -57
- package/ftm-council/references/protocols/PREREQUISITES.md +47 -47
- package/ftm-council/references/protocols/STEP-0-FRAMING.md +46 -46
- package/ftm-council.yml +2 -2
- package/ftm-dashboard/SKILL.md +163 -163
- package/ftm-dashboard.yml +4 -4
- package/ftm-debug/SKILL.md +1037 -1037
- package/ftm-debug/references/phases/PHASE-0-INTAKE.md +58 -58
- package/ftm-debug/references/phases/PHASE-1-TRIAGE.md +46 -46
- package/ftm-debug/references/phases/PHASE-2-WAR-ROOM-AGENTS.md +279 -279
- package/ftm-debug/references/phases/PHASE-3-TO-6-EXECUTION.md +436 -436
- package/ftm-debug/references/protocols/BLACKBOARD.md +86 -86
- package/ftm-debug/references/protocols/EDGE-CASES.md +103 -103
- package/ftm-debug.yml +2 -2
- package/ftm-diagram/SKILL.md +277 -277
- package/ftm-diagram.yml +2 -2
- package/ftm-executor/SKILL.md +777 -767
- package/ftm-executor/references/STYLE-TEMPLATE.md +73 -73
- package/ftm-executor/references/phases/PHASE-0-VERIFICATION.md +62 -62
- package/ftm-executor/references/phases/PHASE-2-AGENT-ASSEMBLY.md +34 -34
- package/ftm-executor/references/phases/PHASE-3-WORKTREES.md +38 -38
- package/ftm-executor/references/phases/PHASE-4-5-AUDIT.md +72 -72
- package/ftm-executor/references/phases/PHASE-4-DISPATCH.md +66 -66
- package/ftm-executor/references/phases/PHASE-5-5-CODEX-GATE.md +73 -73
- package/ftm-executor/references/protocols/DOCUMENTATION-BOOTSTRAP.md +36 -36
- package/ftm-executor/references/protocols/MODEL-PROFILE.md +59 -44
- package/ftm-executor/references/protocols/PROGRESS-TRACKING.md +66 -66
- package/ftm-executor/runtime/ftm-runtime.mjs +252 -252
- package/ftm-executor/runtime/package.json +8 -8
- package/ftm-executor.yml +2 -2
- package/ftm-git/SKILL.md +441 -441
- package/ftm-git/evals/evals.json +26 -26
- package/ftm-git/evals/promptfoo.yaml +75 -75
- package/ftm-git/hooks/post-commit-experience.sh +92 -92
- package/ftm-git/references/patterns/SECRET-PATTERNS.md +104 -104
- package/ftm-git/references/protocols/REMEDIATION.md +139 -139
- package/ftm-git/scripts/pre-commit-secrets.sh +110 -110
- package/ftm-git.yml +2 -2
- package/ftm-inbox/backend/adapters/_retry.py +64 -64
- package/ftm-inbox/backend/adapters/base.py +230 -230
- package/ftm-inbox/backend/adapters/freshservice.py +104 -104
- package/ftm-inbox/backend/adapters/gmail.py +125 -125
- package/ftm-inbox/backend/adapters/jira.py +136 -136
- package/ftm-inbox/backend/adapters/registry.py +192 -192
- package/ftm-inbox/backend/adapters/slack.py +110 -110
- package/ftm-inbox/backend/db/connection.py +54 -54
- package/ftm-inbox/backend/db/schema.py +78 -78
- package/ftm-inbox/backend/executor/__init__.py +7 -7
- package/ftm-inbox/backend/executor/engine.py +149 -149
- package/ftm-inbox/backend/executor/step_runner.py +98 -98
- package/ftm-inbox/backend/main.py +103 -103
- package/ftm-inbox/backend/models/__init__.py +1 -1
- package/ftm-inbox/backend/models/unified_task.py +36 -36
- package/ftm-inbox/backend/planner/__init__.py +6 -6
- package/ftm-inbox/backend/planner/generator.py +127 -127
- package/ftm-inbox/backend/planner/schema.py +34 -34
- package/ftm-inbox/backend/requirements.txt +5 -5
- package/ftm-inbox/backend/routes/execute.py +186 -186
- package/ftm-inbox/backend/routes/health.py +52 -52
- package/ftm-inbox/backend/routes/inbox.py +68 -68
- package/ftm-inbox/backend/routes/plan.py +271 -271
- package/ftm-inbox/bin/launchagent.mjs +91 -91
- package/ftm-inbox/bin/setup.mjs +188 -188
- package/ftm-inbox/bin/start.sh +10 -10
- package/ftm-inbox/bin/status.sh +17 -17
- package/ftm-inbox/bin/stop.sh +8 -8
- package/ftm-inbox/config.example.yml +55 -55
- package/ftm-inbox/package-lock.json +2898 -2898
- package/ftm-inbox/package.json +26 -26
- package/ftm-inbox/postcss.config.js +6 -6
- package/ftm-inbox/src/app.css +199 -199
- package/ftm-inbox/src/app.html +18 -18
- package/ftm-inbox/src/lib/api.ts +166 -166
- package/ftm-inbox/src/lib/components/ExecutionLog.svelte +81 -81
- package/ftm-inbox/src/lib/components/InboxFeed.svelte +143 -143
- package/ftm-inbox/src/lib/components/PlanStep.svelte +271 -271
- package/ftm-inbox/src/lib/components/PlanView.svelte +206 -206
- package/ftm-inbox/src/lib/components/StreamPanel.svelte +99 -99
- package/ftm-inbox/src/lib/components/TaskCard.svelte +190 -190
- package/ftm-inbox/src/lib/components/ui/EmptyState.svelte +63 -63
- package/ftm-inbox/src/lib/components/ui/KawaiiCard.svelte +86 -86
- package/ftm-inbox/src/lib/components/ui/PillButton.svelte +106 -106
- package/ftm-inbox/src/lib/components/ui/StatusBadge.svelte +67 -67
- package/ftm-inbox/src/lib/components/ui/StreamDrawer.svelte +149 -149
- package/ftm-inbox/src/lib/components/ui/ThemeToggle.svelte +80 -80
- package/ftm-inbox/src/lib/theme.ts +47 -47
- package/ftm-inbox/src/routes/+layout.svelte +76 -76
- package/ftm-inbox/src/routes/+page.svelte +401 -401
- package/ftm-inbox/svelte.config.js +12 -12
- package/ftm-inbox/tailwind.config.ts +63 -63
- package/ftm-inbox/tsconfig.json +13 -13
- package/ftm-inbox/vite.config.ts +6 -6
- package/ftm-intent/SKILL.md +241 -241
- package/ftm-intent.yml +2 -2
- package/ftm-manifest.json +3794 -3794
- package/ftm-map/SKILL.md +291 -291
- package/ftm-map/scripts/db.py +712 -712
- package/ftm-map/scripts/index.py +415 -415
- package/ftm-map/scripts/parser.py +224 -224
- package/ftm-map/scripts/queries/go-tags.scm +20 -20
- package/ftm-map/scripts/queries/javascript-tags.scm +35 -35
- package/ftm-map/scripts/queries/python-tags.scm +31 -31
- package/ftm-map/scripts/queries/ruby-tags.scm +19 -19
- package/ftm-map/scripts/queries/rust-tags.scm +37 -37
- package/ftm-map/scripts/queries/typescript-tags.scm +41 -41
- package/ftm-map/scripts/query.py +301 -301
- package/ftm-map/scripts/ranker.py +377 -377
- package/ftm-map/scripts/requirements.txt +5 -5
- package/ftm-map/scripts/setup-hooks.sh +27 -27
- package/ftm-map/scripts/setup.sh +56 -56
- package/ftm-map/scripts/test_db.py +364 -364
- package/ftm-map/scripts/test_parser.py +174 -174
- package/ftm-map/scripts/test_query.py +183 -183
- package/ftm-map/scripts/test_ranker.py +199 -199
- package/ftm-map/scripts/views.py +591 -591
- package/ftm-map.yml +2 -2
- package/ftm-mind/SKILL.md +1943 -1943
- package/ftm-mind/evals/promptfoo.yaml +142 -142
- package/ftm-mind/references/blackboard-schema.md +328 -328
- package/ftm-mind/references/complexity-guide.md +110 -110
- package/ftm-mind/references/event-registry.md +319 -319
- package/ftm-mind/references/mcp-inventory.md +296 -296
- package/ftm-mind/references/protocols/COMPLEXITY-SIZING.md +72 -72
- package/ftm-mind/references/protocols/MCP-HEURISTICS.md +32 -32
- package/ftm-mind/references/protocols/PLAN-APPROVAL.md +80 -80
- package/ftm-mind/references/reflexion-protocol.md +249 -249
- package/ftm-mind/references/routing/SCENARIOS.md +22 -22
- package/ftm-mind/references/routing-scenarios.md +35 -35
- package/ftm-mind.yml +2 -2
- package/ftm-pause/SKILL.md +395 -395
- package/ftm-pause/references/protocols/SKILL-RESTORE-PROTOCOLS.md +186 -186
- package/ftm-pause/references/protocols/VALIDATION.md +80 -80
- package/ftm-pause.yml +2 -2
- package/ftm-researcher/SKILL.md +275 -275
- package/ftm-researcher/evals/agent-diversity.yaml +17 -17
- package/ftm-researcher/evals/synthesis-quality.yaml +12 -12
- package/ftm-researcher/evals/trigger-accuracy.yaml +39 -39
- package/ftm-researcher/references/adaptive-search.md +116 -116
- package/ftm-researcher/references/agent-prompts.md +193 -193
- package/ftm-researcher/references/council-integration.md +193 -193
- package/ftm-researcher/references/output-format.md +203 -203
- package/ftm-researcher/references/synthesis-pipeline.md +165 -165
- package/ftm-researcher/scripts/score_credibility.py +234 -234
- package/ftm-researcher/scripts/validate_research.py +92 -92
- package/ftm-researcher.yml +2 -2
- package/ftm-resume/SKILL.md +518 -518
- package/ftm-resume/references/protocols/VALIDATION.md +172 -172
- package/ftm-resume.yml +2 -2
- package/ftm-retro/SKILL.md +380 -380
- package/ftm-retro/references/protocols/SCORING-RUBRICS.md +89 -89
- package/ftm-retro/references/templates/REPORT-FORMAT.md +109 -109
- package/ftm-retro.yml +2 -2
- package/ftm-routine/SKILL.md +170 -170
- package/ftm-routine.yml +4 -4
- package/ftm-state/blackboard/capabilities.json +5 -5
- package/ftm-state/blackboard/capabilities.schema.json +27 -27
- package/ftm-state/blackboard/context.json +23 -23
- package/ftm-state/blackboard/experiences/index.json +9 -9
- package/ftm-state/blackboard/patterns.json +6 -6
- package/ftm-state/schemas/context.schema.json +130 -130
- package/ftm-state/schemas/experience-index.schema.json +77 -77
- package/ftm-state/schemas/experience.schema.json +78 -78
- package/ftm-state/schemas/patterns.schema.json +44 -44
- package/ftm-upgrade/SKILL.md +194 -194
- package/ftm-upgrade/scripts/check-version.sh +76 -76
- package/ftm-upgrade/scripts/upgrade.sh +143 -143
- package/ftm-upgrade.yml +2 -2
- package/ftm-verify.yml +2 -2
- package/ftm.yml +2 -2
- package/hooks/ftm-blackboard-enforcer.sh +93 -93
- package/hooks/ftm-discovery-reminder.sh +90 -90
- package/hooks/ftm-drafts-gate.sh +61 -61
- package/hooks/ftm-event-logger.mjs +107 -107
- package/hooks/ftm-map-autodetect.sh +79 -79
- package/hooks/ftm-pending-sync-check.sh +22 -22
- package/hooks/ftm-plan-gate.sh +92 -92
- package/hooks/ftm-post-commit-trigger.sh +57 -57
- package/hooks/settings-template.json +81 -81
- package/install.sh +363 -363
- package/package.json +84 -84
- package/uninstall.sh +25 -25
|
@@ -1,234 +1,234 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
Source credibility scoring for ftm-researcher findings.
|
|
4
|
-
|
|
5
|
-
Scoring dimensions:
|
|
6
|
-
- Source type weight (35%): primary > peer_reviewed > official_docs > news > blog > forum
|
|
7
|
-
- Recency (20%): decay based on age for fast-moving topics
|
|
8
|
-
- Expertise signals (25%): domain authority, author credentials
|
|
9
|
-
- Bias detection (20%): sensationalism penalties, balanced language bonuses
|
|
10
|
-
|
|
11
|
-
Additional flags:
|
|
12
|
-
- Corroboration bonus: +0.15 if independently found by 2+ agents from different source types
|
|
13
|
-
- Circular sourcing: flag if multiple sources cite the same original
|
|
14
|
-
"""
|
|
15
|
-
import json
|
|
16
|
-
import sys
|
|
17
|
-
import re
|
|
18
|
-
from datetime import datetime
|
|
19
|
-
from urllib.parse import urlparse
|
|
20
|
-
|
|
21
|
-
# Source type base weights
|
|
22
|
-
SOURCE_WEIGHTS = {
|
|
23
|
-
"primary": 1.0,
|
|
24
|
-
"peer_reviewed": 0.9,
|
|
25
|
-
"official_docs": 0.85,
|
|
26
|
-
"code_repo": 0.8,
|
|
27
|
-
"qa_site": 0.65,
|
|
28
|
-
"news": 0.6,
|
|
29
|
-
"blog": 0.4,
|
|
30
|
-
"forum": 0.25,
|
|
31
|
-
"codebase": 0.95, # local codebase findings are high-trust
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
# High-authority domains
|
|
35
|
-
HIGH_AUTHORITY = {
|
|
36
|
-
"arxiv.org", "nature.com", "science.org", "acm.org", "ieee.org",
|
|
37
|
-
"github.com", "docs.python.org", "developer.mozilla.org",
|
|
38
|
-
"platform.openai.com", "docs.anthropic.com", "cloud.google.com",
|
|
39
|
-
"aws.amazon.com", "learn.microsoft.com",
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
MODERATE_AUTHORITY = {
|
|
43
|
-
"stackoverflow.com", "stackexchange.com", "reddit.com",
|
|
44
|
-
"news.ycombinator.com", "techcrunch.com", "arstechnica.com",
|
|
45
|
-
"thenewstack.io", "infoq.com", "dev.to",
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
# Sensationalism indicators
|
|
49
|
-
SENSATIONAL_PATTERNS = [
|
|
50
|
-
r"you won't believe", r"shocking", r"mind-blowing", r"game.?changer",
|
|
51
|
-
r"revolutionary", r"incredible", r"amazing breakthrough",
|
|
52
|
-
]
|
|
53
|
-
|
|
54
|
-
# Balanced language indicators
|
|
55
|
-
BALANCED_PATTERNS = [
|
|
56
|
-
r"however", r"on the other hand", r"trade-?off", r"limitation",
|
|
57
|
-
r"caveat", r"although", r"despite", r"conversely",
|
|
58
|
-
]
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def score_source_type(finding: dict) -> float:
|
|
62
|
-
return SOURCE_WEIGHTS.get(finding.get("source_type", "blog"), 0.4)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def score_recency(finding: dict, fast_moving: bool = True) -> float:
|
|
66
|
-
"""Score based on source recency. Extracts year from URL or metadata."""
|
|
67
|
-
url = finding.get("source_url", "")
|
|
68
|
-
evidence = finding.get("evidence", "")
|
|
69
|
-
current_year = datetime.now().year
|
|
70
|
-
|
|
71
|
-
# Try to extract year from URL (common in blog/paper URLs)
|
|
72
|
-
year_match = re.search(r'/(20[12]\d)/', url)
|
|
73
|
-
if not year_match:
|
|
74
|
-
# Try evidence text for year mentions
|
|
75
|
-
year_match = re.search(r'\b(20[12]\d)\b', evidence)
|
|
76
|
-
|
|
77
|
-
if year_match:
|
|
78
|
-
source_year = int(year_match.group(1))
|
|
79
|
-
age = current_year - source_year
|
|
80
|
-
if fast_moving:
|
|
81
|
-
# Aggressive decay for fast-moving topics (tech, AI, etc.)
|
|
82
|
-
decay_map = {0: 1.0, 1: 0.85, 2: 0.65, 3: 0.45, 4: 0.30}
|
|
83
|
-
return decay_map.get(age, 0.2)
|
|
84
|
-
else:
|
|
85
|
-
# Gentle decay for stable topics
|
|
86
|
-
decay_map = {0: 1.0, 1: 0.95, 2: 0.85, 3: 0.75, 4: 0.65, 5: 0.55}
|
|
87
|
-
return decay_map.get(age, 0.4)
|
|
88
|
-
|
|
89
|
-
# No date info — return neutral
|
|
90
|
-
return 0.7
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def score_domain_authority(finding: dict) -> float:
|
|
94
|
-
url = finding.get("source_url", "")
|
|
95
|
-
if not url:
|
|
96
|
-
if finding.get("source_type") == "codebase":
|
|
97
|
-
return 0.95
|
|
98
|
-
return 0.5
|
|
99
|
-
|
|
100
|
-
try:
|
|
101
|
-
domain = urlparse(url).netloc.lower()
|
|
102
|
-
# Strip www.
|
|
103
|
-
domain = domain.removeprefix("www.")
|
|
104
|
-
except Exception:
|
|
105
|
-
return 0.5
|
|
106
|
-
|
|
107
|
-
if domain in HIGH_AUTHORITY:
|
|
108
|
-
return 0.9
|
|
109
|
-
if domain in MODERATE_AUTHORITY:
|
|
110
|
-
return 0.7
|
|
111
|
-
# Check for .edu, .gov
|
|
112
|
-
if domain.endswith(".edu") or domain.endswith(".gov"):
|
|
113
|
-
return 0.85
|
|
114
|
-
return 0.55
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
def score_bias(finding: dict) -> float:
|
|
118
|
-
text = finding.get("evidence", "") + " " + finding.get("claim", "")
|
|
119
|
-
text_lower = text.lower()
|
|
120
|
-
|
|
121
|
-
score = 0.7 # baseline
|
|
122
|
-
|
|
123
|
-
# Penalize sensationalism
|
|
124
|
-
for pattern in SENSATIONAL_PATTERNS:
|
|
125
|
-
if re.search(pattern, text_lower):
|
|
126
|
-
score -= 0.1
|
|
127
|
-
|
|
128
|
-
# Bonus for balanced language
|
|
129
|
-
for pattern in BALANCED_PATTERNS:
|
|
130
|
-
if re.search(pattern, text_lower):
|
|
131
|
-
score += 0.05
|
|
132
|
-
|
|
133
|
-
return max(0.1, min(1.0, score))
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
def detect_circular_sourcing(findings: list) -> list:
|
|
137
|
-
"""Flag findings where multiple sources trace to the same original."""
|
|
138
|
-
url_groups = {}
|
|
139
|
-
for i, f in enumerate(findings):
|
|
140
|
-
url = f.get("source_url", "")
|
|
141
|
-
if url:
|
|
142
|
-
domain = urlparse(url).netloc.lower().removeprefix("www.")
|
|
143
|
-
claim_key = f.get("claim", "")[:50]
|
|
144
|
-
key = f"{domain}:{claim_key}"
|
|
145
|
-
url_groups.setdefault(key, []).append(i)
|
|
146
|
-
|
|
147
|
-
circular_indices = set()
|
|
148
|
-
for key, indices in url_groups.items():
|
|
149
|
-
if len(indices) > 1:
|
|
150
|
-
for idx in indices:
|
|
151
|
-
circular_indices.add(idx)
|
|
152
|
-
|
|
153
|
-
return list(circular_indices)
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
def score_findings(findings: list) -> list:
|
|
157
|
-
circular = detect_circular_sourcing(findings)
|
|
158
|
-
|
|
159
|
-
# Count agent agreement per claim (simplified: exact claim match)
|
|
160
|
-
claim_agents = {}
|
|
161
|
-
for f in findings:
|
|
162
|
-
claim = f.get("claim", "")
|
|
163
|
-
agent = f.get("agent_role", "unknown")
|
|
164
|
-
source_type = f.get("source_type", "")
|
|
165
|
-
claim_agents.setdefault(claim, {"agents": set(), "source_types": set()})
|
|
166
|
-
claim_agents[claim]["agents"].add(agent)
|
|
167
|
-
claim_agents[claim]["source_types"].add(source_type)
|
|
168
|
-
|
|
169
|
-
scored = []
|
|
170
|
-
for i, f in enumerate(findings):
|
|
171
|
-
type_score = score_source_type(f)
|
|
172
|
-
recency_score = score_recency(f)
|
|
173
|
-
authority_score = score_domain_authority(f)
|
|
174
|
-
bias_score = score_bias(f)
|
|
175
|
-
|
|
176
|
-
# Weighted composite
|
|
177
|
-
composite = (
|
|
178
|
-
type_score * 0.35 +
|
|
179
|
-
recency_score * 0.20 +
|
|
180
|
-
authority_score * 0.25 +
|
|
181
|
-
bias_score * 0.20
|
|
182
|
-
)
|
|
183
|
-
|
|
184
|
-
# Corroboration bonus
|
|
185
|
-
claim = f.get("claim", "")
|
|
186
|
-
if claim in claim_agents:
|
|
187
|
-
info = claim_agents[claim]
|
|
188
|
-
if len(info["agents"]) >= 2 and len(info["source_types"]) >= 2:
|
|
189
|
-
composite += 0.15
|
|
190
|
-
|
|
191
|
-
# Circular sourcing penalty
|
|
192
|
-
is_circular = i in circular
|
|
193
|
-
if is_circular:
|
|
194
|
-
composite -= 0.2
|
|
195
|
-
|
|
196
|
-
composite = max(0.0, min(1.0, composite))
|
|
197
|
-
|
|
198
|
-
scored_finding = {
|
|
199
|
-
**f,
|
|
200
|
-
"credibility_score": round(composite, 3),
|
|
201
|
-
"score_breakdown": {
|
|
202
|
-
"source_type": round(type_score, 3),
|
|
203
|
-
"recency": round(recency_score, 3),
|
|
204
|
-
"domain_authority": round(authority_score, 3),
|
|
205
|
-
"bias": round(bias_score, 3),
|
|
206
|
-
},
|
|
207
|
-
"circular_sourcing": is_circular,
|
|
208
|
-
"corroborated": claim in claim_agents and len(claim_agents[claim]["agents"]) >= 2,
|
|
209
|
-
"trust_level": (
|
|
210
|
-
"high" if composite >= 0.75 else
|
|
211
|
-
"moderate" if composite >= 0.55 else
|
|
212
|
-
"low" if composite >= 0.35 else
|
|
213
|
-
"verify"
|
|
214
|
-
),
|
|
215
|
-
}
|
|
216
|
-
scored.append(scored_finding)
|
|
217
|
-
|
|
218
|
-
return sorted(scored, key=lambda x: x["credibility_score"], reverse=True)
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
def main():
|
|
222
|
-
if len(sys.argv) < 2:
|
|
223
|
-
print("Usage: score_credibility.py <findings.json>", file=sys.stderr)
|
|
224
|
-
sys.exit(1)
|
|
225
|
-
|
|
226
|
-
with open(sys.argv[1]) as f:
|
|
227
|
-
findings = json.load(f)
|
|
228
|
-
|
|
229
|
-
scored = score_findings(findings)
|
|
230
|
-
print(json.dumps(scored, indent=2))
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
if __name__ == "__main__":
|
|
234
|
-
main()
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Source credibility scoring for ftm-researcher findings.
|
|
4
|
+
|
|
5
|
+
Scoring dimensions:
|
|
6
|
+
- Source type weight (35%): primary > peer_reviewed > official_docs > news > blog > forum
|
|
7
|
+
- Recency (20%): decay based on age for fast-moving topics
|
|
8
|
+
- Expertise signals (25%): domain authority, author credentials
|
|
9
|
+
- Bias detection (20%): sensationalism penalties, balanced language bonuses
|
|
10
|
+
|
|
11
|
+
Additional flags:
|
|
12
|
+
- Corroboration bonus: +0.15 if independently found by 2+ agents from different source types
|
|
13
|
+
- Circular sourcing: flag if multiple sources cite the same original
|
|
14
|
+
"""
|
|
15
|
+
import json
|
|
16
|
+
import sys
|
|
17
|
+
import re
|
|
18
|
+
from datetime import datetime
|
|
19
|
+
from urllib.parse import urlparse
|
|
20
|
+
|
|
21
|
+
# Source type base weights
|
|
22
|
+
SOURCE_WEIGHTS = {
|
|
23
|
+
"primary": 1.0,
|
|
24
|
+
"peer_reviewed": 0.9,
|
|
25
|
+
"official_docs": 0.85,
|
|
26
|
+
"code_repo": 0.8,
|
|
27
|
+
"qa_site": 0.65,
|
|
28
|
+
"news": 0.6,
|
|
29
|
+
"blog": 0.4,
|
|
30
|
+
"forum": 0.25,
|
|
31
|
+
"codebase": 0.95, # local codebase findings are high-trust
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# High-authority domains
|
|
35
|
+
HIGH_AUTHORITY = {
|
|
36
|
+
"arxiv.org", "nature.com", "science.org", "acm.org", "ieee.org",
|
|
37
|
+
"github.com", "docs.python.org", "developer.mozilla.org",
|
|
38
|
+
"platform.openai.com", "docs.anthropic.com", "cloud.google.com",
|
|
39
|
+
"aws.amazon.com", "learn.microsoft.com",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
MODERATE_AUTHORITY = {
|
|
43
|
+
"stackoverflow.com", "stackexchange.com", "reddit.com",
|
|
44
|
+
"news.ycombinator.com", "techcrunch.com", "arstechnica.com",
|
|
45
|
+
"thenewstack.io", "infoq.com", "dev.to",
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# Sensationalism indicators
|
|
49
|
+
SENSATIONAL_PATTERNS = [
|
|
50
|
+
r"you won't believe", r"shocking", r"mind-blowing", r"game.?changer",
|
|
51
|
+
r"revolutionary", r"incredible", r"amazing breakthrough",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
# Balanced language indicators
|
|
55
|
+
BALANCED_PATTERNS = [
|
|
56
|
+
r"however", r"on the other hand", r"trade-?off", r"limitation",
|
|
57
|
+
r"caveat", r"although", r"despite", r"conversely",
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def score_source_type(finding: dict) -> float:
|
|
62
|
+
return SOURCE_WEIGHTS.get(finding.get("source_type", "blog"), 0.4)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def score_recency(finding: dict, fast_moving: bool = True) -> float:
|
|
66
|
+
"""Score based on source recency. Extracts year from URL or metadata."""
|
|
67
|
+
url = finding.get("source_url", "")
|
|
68
|
+
evidence = finding.get("evidence", "")
|
|
69
|
+
current_year = datetime.now().year
|
|
70
|
+
|
|
71
|
+
# Try to extract year from URL (common in blog/paper URLs)
|
|
72
|
+
year_match = re.search(r'/(20[12]\d)/', url)
|
|
73
|
+
if not year_match:
|
|
74
|
+
# Try evidence text for year mentions
|
|
75
|
+
year_match = re.search(r'\b(20[12]\d)\b', evidence)
|
|
76
|
+
|
|
77
|
+
if year_match:
|
|
78
|
+
source_year = int(year_match.group(1))
|
|
79
|
+
age = current_year - source_year
|
|
80
|
+
if fast_moving:
|
|
81
|
+
# Aggressive decay for fast-moving topics (tech, AI, etc.)
|
|
82
|
+
decay_map = {0: 1.0, 1: 0.85, 2: 0.65, 3: 0.45, 4: 0.30}
|
|
83
|
+
return decay_map.get(age, 0.2)
|
|
84
|
+
else:
|
|
85
|
+
# Gentle decay for stable topics
|
|
86
|
+
decay_map = {0: 1.0, 1: 0.95, 2: 0.85, 3: 0.75, 4: 0.65, 5: 0.55}
|
|
87
|
+
return decay_map.get(age, 0.4)
|
|
88
|
+
|
|
89
|
+
# No date info — return neutral
|
|
90
|
+
return 0.7
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def score_domain_authority(finding: dict) -> float:
|
|
94
|
+
url = finding.get("source_url", "")
|
|
95
|
+
if not url:
|
|
96
|
+
if finding.get("source_type") == "codebase":
|
|
97
|
+
return 0.95
|
|
98
|
+
return 0.5
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
domain = urlparse(url).netloc.lower()
|
|
102
|
+
# Strip www.
|
|
103
|
+
domain = domain.removeprefix("www.")
|
|
104
|
+
except Exception:
|
|
105
|
+
return 0.5
|
|
106
|
+
|
|
107
|
+
if domain in HIGH_AUTHORITY:
|
|
108
|
+
return 0.9
|
|
109
|
+
if domain in MODERATE_AUTHORITY:
|
|
110
|
+
return 0.7
|
|
111
|
+
# Check for .edu, .gov
|
|
112
|
+
if domain.endswith(".edu") or domain.endswith(".gov"):
|
|
113
|
+
return 0.85
|
|
114
|
+
return 0.55
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def score_bias(finding: dict) -> float:
|
|
118
|
+
text = finding.get("evidence", "") + " " + finding.get("claim", "")
|
|
119
|
+
text_lower = text.lower()
|
|
120
|
+
|
|
121
|
+
score = 0.7 # baseline
|
|
122
|
+
|
|
123
|
+
# Penalize sensationalism
|
|
124
|
+
for pattern in SENSATIONAL_PATTERNS:
|
|
125
|
+
if re.search(pattern, text_lower):
|
|
126
|
+
score -= 0.1
|
|
127
|
+
|
|
128
|
+
# Bonus for balanced language
|
|
129
|
+
for pattern in BALANCED_PATTERNS:
|
|
130
|
+
if re.search(pattern, text_lower):
|
|
131
|
+
score += 0.05
|
|
132
|
+
|
|
133
|
+
return max(0.1, min(1.0, score))
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def detect_circular_sourcing(findings: list) -> list:
|
|
137
|
+
"""Flag findings where multiple sources trace to the same original."""
|
|
138
|
+
url_groups = {}
|
|
139
|
+
for i, f in enumerate(findings):
|
|
140
|
+
url = f.get("source_url", "")
|
|
141
|
+
if url:
|
|
142
|
+
domain = urlparse(url).netloc.lower().removeprefix("www.")
|
|
143
|
+
claim_key = f.get("claim", "")[:50]
|
|
144
|
+
key = f"{domain}:{claim_key}"
|
|
145
|
+
url_groups.setdefault(key, []).append(i)
|
|
146
|
+
|
|
147
|
+
circular_indices = set()
|
|
148
|
+
for key, indices in url_groups.items():
|
|
149
|
+
if len(indices) > 1:
|
|
150
|
+
for idx in indices:
|
|
151
|
+
circular_indices.add(idx)
|
|
152
|
+
|
|
153
|
+
return list(circular_indices)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def score_findings(findings: list) -> list:
|
|
157
|
+
circular = detect_circular_sourcing(findings)
|
|
158
|
+
|
|
159
|
+
# Count agent agreement per claim (simplified: exact claim match)
|
|
160
|
+
claim_agents = {}
|
|
161
|
+
for f in findings:
|
|
162
|
+
claim = f.get("claim", "")
|
|
163
|
+
agent = f.get("agent_role", "unknown")
|
|
164
|
+
source_type = f.get("source_type", "")
|
|
165
|
+
claim_agents.setdefault(claim, {"agents": set(), "source_types": set()})
|
|
166
|
+
claim_agents[claim]["agents"].add(agent)
|
|
167
|
+
claim_agents[claim]["source_types"].add(source_type)
|
|
168
|
+
|
|
169
|
+
scored = []
|
|
170
|
+
for i, f in enumerate(findings):
|
|
171
|
+
type_score = score_source_type(f)
|
|
172
|
+
recency_score = score_recency(f)
|
|
173
|
+
authority_score = score_domain_authority(f)
|
|
174
|
+
bias_score = score_bias(f)
|
|
175
|
+
|
|
176
|
+
# Weighted composite
|
|
177
|
+
composite = (
|
|
178
|
+
type_score * 0.35 +
|
|
179
|
+
recency_score * 0.20 +
|
|
180
|
+
authority_score * 0.25 +
|
|
181
|
+
bias_score * 0.20
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# Corroboration bonus
|
|
185
|
+
claim = f.get("claim", "")
|
|
186
|
+
if claim in claim_agents:
|
|
187
|
+
info = claim_agents[claim]
|
|
188
|
+
if len(info["agents"]) >= 2 and len(info["source_types"]) >= 2:
|
|
189
|
+
composite += 0.15
|
|
190
|
+
|
|
191
|
+
# Circular sourcing penalty
|
|
192
|
+
is_circular = i in circular
|
|
193
|
+
if is_circular:
|
|
194
|
+
composite -= 0.2
|
|
195
|
+
|
|
196
|
+
composite = max(0.0, min(1.0, composite))
|
|
197
|
+
|
|
198
|
+
scored_finding = {
|
|
199
|
+
**f,
|
|
200
|
+
"credibility_score": round(composite, 3),
|
|
201
|
+
"score_breakdown": {
|
|
202
|
+
"source_type": round(type_score, 3),
|
|
203
|
+
"recency": round(recency_score, 3),
|
|
204
|
+
"domain_authority": round(authority_score, 3),
|
|
205
|
+
"bias": round(bias_score, 3),
|
|
206
|
+
},
|
|
207
|
+
"circular_sourcing": is_circular,
|
|
208
|
+
"corroborated": claim in claim_agents and len(claim_agents[claim]["agents"]) >= 2,
|
|
209
|
+
"trust_level": (
|
|
210
|
+
"high" if composite >= 0.75 else
|
|
211
|
+
"moderate" if composite >= 0.55 else
|
|
212
|
+
"low" if composite >= 0.35 else
|
|
213
|
+
"verify"
|
|
214
|
+
),
|
|
215
|
+
}
|
|
216
|
+
scored.append(scored_finding)
|
|
217
|
+
|
|
218
|
+
return sorted(scored, key=lambda x: x["credibility_score"], reverse=True)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def main():
|
|
222
|
+
if len(sys.argv) < 2:
|
|
223
|
+
print("Usage: score_credibility.py <findings.json>", file=sys.stderr)
|
|
224
|
+
sys.exit(1)
|
|
225
|
+
|
|
226
|
+
with open(sys.argv[1]) as f:
|
|
227
|
+
findings = json.load(f)
|
|
228
|
+
|
|
229
|
+
scored = score_findings(findings)
|
|
230
|
+
print(json.dumps(scored, indent=2))
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
if __name__ == "__main__":
|
|
234
|
+
main()
|
|
@@ -1,92 +1,92 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
Validates ftm-researcher output for completeness and quality.
|
|
4
|
-
|
|
5
|
-
Checks:
|
|
6
|
-
1. All required fields present in each finding
|
|
7
|
-
2. Source URLs are non-empty for non-codebase findings
|
|
8
|
-
3. Confidence scores in valid range
|
|
9
|
-
4. Disagreement map has all 4 tiers
|
|
10
|
-
5. No placeholder text (TBD, TODO, FIXME)
|
|
11
|
-
6. Minimum finding count per mode (quick: 3, standard: 10, deep: 15)
|
|
12
|
-
7. Source diversity: at least 3 different source types represented
|
|
13
|
-
8. No duplicate claims (exact match)
|
|
14
|
-
"""
|
|
15
|
-
import json
|
|
16
|
-
import sys
|
|
17
|
-
|
|
18
|
-
REQUIRED_FINDING_FIELDS = ["claim", "source_type", "confidence", "agent_role"]
|
|
19
|
-
REQUIRED_MAP_TIERS = ["consensus", "contested", "unique_insights", "refuted"]
|
|
20
|
-
PLACEHOLDER_PATTERNS = ["TBD", "TODO", "FIXME", "placeholder", "lorem ipsum"]
|
|
21
|
-
MIN_FINDINGS = {"quick": 3, "standard": 10, "deep": 15}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def validate(output: dict) -> dict:
|
|
25
|
-
errors = []
|
|
26
|
-
warnings = []
|
|
27
|
-
|
|
28
|
-
mode = output.get("mode", "standard")
|
|
29
|
-
findings = output.get("findings", [])
|
|
30
|
-
disagreement_map = output.get("disagreement_map", {})
|
|
31
|
-
|
|
32
|
-
# Check minimum findings
|
|
33
|
-
min_count = MIN_FINDINGS.get(mode, 10)
|
|
34
|
-
if len(findings) < min_count:
|
|
35
|
-
warnings.append(f"Only {len(findings)} findings for {mode} mode (expected >= {min_count})")
|
|
36
|
-
|
|
37
|
-
# Check required fields
|
|
38
|
-
for i, f in enumerate(findings):
|
|
39
|
-
for field in REQUIRED_FINDING_FIELDS:
|
|
40
|
-
if field not in f or not f[field]:
|
|
41
|
-
errors.append(f"Finding {i}: missing required field '{field}'")
|
|
42
|
-
|
|
43
|
-
# Source URL required for non-codebase
|
|
44
|
-
if f.get("source_type") != "codebase" and not f.get("source_url"):
|
|
45
|
-
warnings.append(f"Finding {i}: no source_url for {f.get('source_type')} source")
|
|
46
|
-
|
|
47
|
-
# Confidence range
|
|
48
|
-
conf = f.get("confidence", 0)
|
|
49
|
-
if not (0.0 <= conf <= 1.0):
|
|
50
|
-
errors.append(f"Finding {i}: confidence {conf} out of range [0, 1]")
|
|
51
|
-
|
|
52
|
-
# Placeholder detection
|
|
53
|
-
text = json.dumps(f).lower()
|
|
54
|
-
for p in PLACEHOLDER_PATTERNS:
|
|
55
|
-
if p.lower() in text:
|
|
56
|
-
errors.append(f"Finding {i}: contains placeholder text '{p}'")
|
|
57
|
-
|
|
58
|
-
# Source diversity
|
|
59
|
-
source_types = set(f.get("source_type", "") for f in findings)
|
|
60
|
-
if len(source_types) < 3:
|
|
61
|
-
warnings.append(f"Only {len(source_types)} source types (expected >= 3)")
|
|
62
|
-
|
|
63
|
-
# Duplicate detection
|
|
64
|
-
claims = [f.get("claim", "") for f in findings]
|
|
65
|
-
dupes = [c for c in claims if claims.count(c) > 1]
|
|
66
|
-
if dupes:
|
|
67
|
-
errors.append(f"Duplicate claims found: {set(dupes)}")
|
|
68
|
-
|
|
69
|
-
# Disagreement map tiers
|
|
70
|
-
if mode in ("standard", "deep"):
|
|
71
|
-
for tier in REQUIRED_MAP_TIERS:
|
|
72
|
-
if tier not in disagreement_map:
|
|
73
|
-
errors.append(f"Disagreement map missing tier: {tier}")
|
|
74
|
-
|
|
75
|
-
return {"errors": errors, "warnings": warnings, "valid": len(errors) == 0}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def main():
|
|
79
|
-
if len(sys.argv) < 2:
|
|
80
|
-
print("Usage: validate_research.py <output.json>", file=sys.stderr)
|
|
81
|
-
sys.exit(1)
|
|
82
|
-
|
|
83
|
-
with open(sys.argv[1]) as f:
|
|
84
|
-
output = json.load(f)
|
|
85
|
-
|
|
86
|
-
result = validate(output)
|
|
87
|
-
print(json.dumps(result, indent=2))
|
|
88
|
-
sys.exit(0 if result["valid"] else 1)
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if __name__ == "__main__":
|
|
92
|
-
main()
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Validates ftm-researcher output for completeness and quality.
|
|
4
|
+
|
|
5
|
+
Checks:
|
|
6
|
+
1. All required fields present in each finding
|
|
7
|
+
2. Source URLs are non-empty for non-codebase findings
|
|
8
|
+
3. Confidence scores in valid range
|
|
9
|
+
4. Disagreement map has all 4 tiers
|
|
10
|
+
5. No placeholder text (TBD, TODO, FIXME)
|
|
11
|
+
6. Minimum finding count per mode (quick: 3, standard: 10, deep: 15)
|
|
12
|
+
7. Source diversity: at least 3 different source types represented
|
|
13
|
+
8. No duplicate claims (exact match)
|
|
14
|
+
"""
|
|
15
|
+
import json
|
|
16
|
+
import sys
|
|
17
|
+
|
|
18
|
+
REQUIRED_FINDING_FIELDS = ["claim", "source_type", "confidence", "agent_role"]
|
|
19
|
+
REQUIRED_MAP_TIERS = ["consensus", "contested", "unique_insights", "refuted"]
|
|
20
|
+
PLACEHOLDER_PATTERNS = ["TBD", "TODO", "FIXME", "placeholder", "lorem ipsum"]
|
|
21
|
+
MIN_FINDINGS = {"quick": 3, "standard": 10, "deep": 15}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def validate(output: dict) -> dict:
|
|
25
|
+
errors = []
|
|
26
|
+
warnings = []
|
|
27
|
+
|
|
28
|
+
mode = output.get("mode", "standard")
|
|
29
|
+
findings = output.get("findings", [])
|
|
30
|
+
disagreement_map = output.get("disagreement_map", {})
|
|
31
|
+
|
|
32
|
+
# Check minimum findings
|
|
33
|
+
min_count = MIN_FINDINGS.get(mode, 10)
|
|
34
|
+
if len(findings) < min_count:
|
|
35
|
+
warnings.append(f"Only {len(findings)} findings for {mode} mode (expected >= {min_count})")
|
|
36
|
+
|
|
37
|
+
# Check required fields
|
|
38
|
+
for i, f in enumerate(findings):
|
|
39
|
+
for field in REQUIRED_FINDING_FIELDS:
|
|
40
|
+
if field not in f or not f[field]:
|
|
41
|
+
errors.append(f"Finding {i}: missing required field '{field}'")
|
|
42
|
+
|
|
43
|
+
# Source URL required for non-codebase
|
|
44
|
+
if f.get("source_type") != "codebase" and not f.get("source_url"):
|
|
45
|
+
warnings.append(f"Finding {i}: no source_url for {f.get('source_type')} source")
|
|
46
|
+
|
|
47
|
+
# Confidence range
|
|
48
|
+
conf = f.get("confidence", 0)
|
|
49
|
+
if not (0.0 <= conf <= 1.0):
|
|
50
|
+
errors.append(f"Finding {i}: confidence {conf} out of range [0, 1]")
|
|
51
|
+
|
|
52
|
+
# Placeholder detection
|
|
53
|
+
text = json.dumps(f).lower()
|
|
54
|
+
for p in PLACEHOLDER_PATTERNS:
|
|
55
|
+
if p.lower() in text:
|
|
56
|
+
errors.append(f"Finding {i}: contains placeholder text '{p}'")
|
|
57
|
+
|
|
58
|
+
# Source diversity
|
|
59
|
+
source_types = set(f.get("source_type", "") for f in findings)
|
|
60
|
+
if len(source_types) < 3:
|
|
61
|
+
warnings.append(f"Only {len(source_types)} source types (expected >= 3)")
|
|
62
|
+
|
|
63
|
+
# Duplicate detection
|
|
64
|
+
claims = [f.get("claim", "") for f in findings]
|
|
65
|
+
dupes = [c for c in claims if claims.count(c) > 1]
|
|
66
|
+
if dupes:
|
|
67
|
+
errors.append(f"Duplicate claims found: {set(dupes)}")
|
|
68
|
+
|
|
69
|
+
# Disagreement map tiers
|
|
70
|
+
if mode in ("standard", "deep"):
|
|
71
|
+
for tier in REQUIRED_MAP_TIERS:
|
|
72
|
+
if tier not in disagreement_map:
|
|
73
|
+
errors.append(f"Disagreement map missing tier: {tier}")
|
|
74
|
+
|
|
75
|
+
return {"errors": errors, "warnings": warnings, "valid": len(errors) == 0}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def main():
|
|
79
|
+
if len(sys.argv) < 2:
|
|
80
|
+
print("Usage: validate_research.py <output.json>", file=sys.stderr)
|
|
81
|
+
sys.exit(1)
|
|
82
|
+
|
|
83
|
+
with open(sys.argv[1]) as f:
|
|
84
|
+
output = json.load(f)
|
|
85
|
+
|
|
86
|
+
result = validate(output)
|
|
87
|
+
print(json.dumps(result, indent=2))
|
|
88
|
+
sys.exit(0 if result["valid"] else 1)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
if __name__ == "__main__":
|
|
92
|
+
main()
|
package/ftm-researcher.yml
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
name: ftm-researcher
|
|
2
|
-
description: Deep parallel research engine with 7 domain-specialized finder agents, adversarial review via ftm-council, adaptive wave-based search, and codebase awareness. Use when the user wants thorough research on any topic — "research X", "find out about Y", "what's the state of the art on Z", "compare approaches to W", "deep dive into X", "look into Y". Also invoked by ftm-brainstorm for its research sprints. Triggers on "research", "investigate", "deep dive", "state of the art", "compare", "find examples of", "what's out there for", "how do others handle", "find me evidence", "look into". For idea exploration and brainstorming, use ftm-brainstorm instead (which calls ftm-researcher internally for research).
|
|
1
|
+
name: ftm-researcher
|
|
2
|
+
description: Deep parallel research engine with 7 domain-specialized finder agents, adversarial review via ftm-council, adaptive wave-based search, and codebase awareness. Use when the user wants thorough research on any topic — "research X", "find out about Y", "what's the state of the art on Z", "compare approaches to W", "deep dive into X", "look into Y". Also invoked by ftm-brainstorm for its research sprints. Triggers on "research", "investigate", "deep dive", "state of the art", "compare", "find examples of", "what's out there for", "how do others handle", "find me evidence", "look into". For idea exploration and brainstorming, use ftm-brainstorm instead (which calls ftm-researcher internally for research).
|