feed-the-machine 1.6.1 → 1.7.1

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 (272) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +262 -170
  3. package/bin/__pycache__/tasks_db.cpython-314.pyc +0 -0
  4. package/bin/brain.py +1340 -0
  5. package/bin/convert_claude_skills_to_codex.py +490 -0
  6. package/bin/generate-manifest.mjs +463 -463
  7. package/bin/harden_codex_skills.py +141 -0
  8. package/bin/install.mjs +491 -491
  9. package/bin/migrate-eng-buddy-data.py +875 -0
  10. package/bin/playbook_engine/__init__.py +1 -0
  11. package/bin/playbook_engine/conftest.py +8 -0
  12. package/bin/playbook_engine/extractor.py +33 -0
  13. package/bin/playbook_engine/manager.py +102 -0
  14. package/bin/playbook_engine/models.py +84 -0
  15. package/bin/playbook_engine/registry.py +35 -0
  16. package/bin/playbook_engine/test_extractor.py +72 -0
  17. package/bin/playbook_engine/test_integration.py +129 -0
  18. package/bin/playbook_engine/test_manager.py +85 -0
  19. package/bin/playbook_engine/test_models.py +166 -0
  20. package/bin/playbook_engine/test_registry.py +67 -0
  21. package/bin/playbook_engine/test_tracer.py +86 -0
  22. package/bin/playbook_engine/tracer.py +93 -0
  23. package/bin/tasks_db.py +456 -0
  24. package/docs/HOOKS.md +243 -243
  25. package/docs/INBOX.md +233 -233
  26. package/ftm/SKILL.md +125 -122
  27. package/ftm-audit/SKILL.md +673 -623
  28. package/ftm-audit/references/protocols/PROJECT-PATTERNS.md +91 -91
  29. package/ftm-audit/references/protocols/RUNTIME-WIRING.md +66 -66
  30. package/ftm-audit/references/protocols/WIRING-CONTRACTS.md +135 -135
  31. package/ftm-audit/references/strategies/AUTO-FIX-STRATEGIES.md +69 -69
  32. package/ftm-audit/references/templates/REPORT-FORMAT.md +96 -96
  33. package/ftm-audit/scripts/run-knip.sh +23 -23
  34. package/ftm-audit.yml +2 -2
  35. package/ftm-brainstorm/SKILL.md +1003 -498
  36. package/ftm-brainstorm/evals/evals.json +180 -100
  37. package/ftm-brainstorm/evals/promptfoo.yaml +109 -109
  38. package/ftm-brainstorm/references/agent-prompts.md +552 -224
  39. package/ftm-brainstorm/references/plan-template.md +209 -121
  40. package/ftm-brainstorm.yml +2 -2
  41. package/ftm-browse/SKILL.md +454 -454
  42. package/ftm-browse/daemon/browser-manager.ts +206 -206
  43. package/ftm-browse/daemon/bun.lock +30 -30
  44. package/ftm-browse/daemon/cli.ts +347 -347
  45. package/ftm-browse/daemon/commands.ts +410 -410
  46. package/ftm-browse/daemon/main.ts +357 -357
  47. package/ftm-browse/daemon/package.json +17 -17
  48. package/ftm-browse/daemon/server.ts +189 -189
  49. package/ftm-browse/daemon/snapshot.ts +519 -519
  50. package/ftm-browse/daemon/tsconfig.json +22 -22
  51. package/ftm-browse.yml +4 -4
  52. package/ftm-capture/SKILL.md +370 -370
  53. package/ftm-capture.yml +4 -4
  54. package/ftm-codex-gate/SKILL.md +361 -361
  55. package/ftm-codex-gate.yml +2 -2
  56. package/ftm-config/SKILL.md +422 -345
  57. package/ftm-config.default.yml +125 -82
  58. package/ftm-config.yml +44 -2
  59. package/ftm-council/SKILL.md +416 -416
  60. package/ftm-council/references/prompts/CLAUDE-INVESTIGATION.md +60 -60
  61. package/ftm-council/references/prompts/CODEX-INVESTIGATION.md +58 -58
  62. package/ftm-council/references/prompts/GEMINI-INVESTIGATION.md +58 -58
  63. package/ftm-council/references/prompts/REBUTTAL-TEMPLATE.md +57 -57
  64. package/ftm-council/references/protocols/PREREQUISITES.md +47 -47
  65. package/ftm-council/references/protocols/STEP-0-FRAMING.md +46 -46
  66. package/ftm-council-chat.yml +2 -0
  67. package/ftm-council.yml +2 -2
  68. package/ftm-dashboard/SKILL.md +163 -163
  69. package/ftm-dashboard.yml +4 -4
  70. package/ftm-debug/SKILL.md +1037 -1037
  71. package/ftm-debug/references/phases/PHASE-0-INTAKE.md +58 -58
  72. package/ftm-debug/references/phases/PHASE-1-TRIAGE.md +46 -46
  73. package/ftm-debug/references/phases/PHASE-2-WAR-ROOM-AGENTS.md +279 -279
  74. package/ftm-debug/references/phases/PHASE-3-TO-6-EXECUTION.md +436 -436
  75. package/ftm-debug/references/protocols/BLACKBOARD.md +86 -86
  76. package/ftm-debug/references/protocols/EDGE-CASES.md +103 -103
  77. package/ftm-debug.yml +2 -2
  78. package/ftm-diagram/SKILL.md +277 -277
  79. package/ftm-diagram.yml +2 -2
  80. package/ftm-executor/SKILL.md +777 -777
  81. package/ftm-executor/references/STYLE-TEMPLATE.md +73 -73
  82. package/ftm-executor/references/phases/PHASE-0-VERIFICATION.md +62 -62
  83. package/ftm-executor/references/phases/PHASE-2-AGENT-ASSEMBLY.md +34 -34
  84. package/ftm-executor/references/phases/PHASE-3-WORKTREES.md +38 -38
  85. package/ftm-executor/references/phases/PHASE-4-5-AUDIT.md +81 -72
  86. package/ftm-executor/references/phases/PHASE-4-DISPATCH.md +66 -66
  87. package/ftm-executor/references/phases/PHASE-5-5-CODEX-GATE.md +73 -73
  88. package/ftm-executor/references/protocols/DOCUMENTATION-BOOTSTRAP.md +36 -36
  89. package/ftm-executor/references/protocols/MODEL-PROFILE.md +59 -59
  90. package/ftm-executor/references/protocols/PROGRESS-TRACKING.md +66 -66
  91. package/ftm-executor/runtime/ftm-runtime.mjs +252 -252
  92. package/ftm-executor/runtime/package.json +8 -8
  93. package/ftm-executor.yml +2 -2
  94. package/ftm-git/SKILL.md +441 -441
  95. package/ftm-git/evals/evals.json +26 -26
  96. package/ftm-git/evals/promptfoo.yaml +75 -75
  97. package/ftm-git/hooks/post-commit-experience.sh +92 -92
  98. package/ftm-git/references/patterns/SECRET-PATTERNS.md +104 -104
  99. package/ftm-git/references/protocols/REMEDIATION.md +139 -139
  100. package/ftm-git/scripts/pre-commit-secrets.sh +110 -110
  101. package/ftm-git.yml +2 -2
  102. package/ftm-inbox/backend/__pycache__/main.cpython-314.pyc +0 -0
  103. package/ftm-inbox/backend/adapters/_retry.py +64 -64
  104. package/ftm-inbox/backend/adapters/base.py +230 -230
  105. package/ftm-inbox/backend/adapters/freshservice.py +104 -104
  106. package/ftm-inbox/backend/adapters/gmail.py +125 -125
  107. package/ftm-inbox/backend/adapters/jira.py +136 -136
  108. package/ftm-inbox/backend/adapters/registry.py +192 -192
  109. package/ftm-inbox/backend/adapters/slack.py +110 -110
  110. package/ftm-inbox/backend/db/connection.py +54 -54
  111. package/ftm-inbox/backend/db/schema.py +78 -78
  112. package/ftm-inbox/backend/executor/__init__.py +7 -7
  113. package/ftm-inbox/backend/executor/engine.py +149 -149
  114. package/ftm-inbox/backend/executor/step_runner.py +98 -98
  115. package/ftm-inbox/backend/main.py +103 -103
  116. package/ftm-inbox/backend/models/__init__.py +1 -1
  117. package/ftm-inbox/backend/models/unified_task.py +36 -36
  118. package/ftm-inbox/backend/planner/__init__.py +6 -6
  119. package/ftm-inbox/backend/planner/__pycache__/__init__.cpython-314.pyc +0 -0
  120. package/ftm-inbox/backend/planner/__pycache__/generator.cpython-314.pyc +0 -0
  121. package/ftm-inbox/backend/planner/__pycache__/schema.cpython-314.pyc +0 -0
  122. package/ftm-inbox/backend/planner/generator.py +127 -127
  123. package/ftm-inbox/backend/planner/schema.py +34 -34
  124. package/ftm-inbox/backend/requirements.txt +5 -5
  125. package/ftm-inbox/backend/routes/__pycache__/plan.cpython-314.pyc +0 -0
  126. package/ftm-inbox/backend/routes/execute.py +186 -186
  127. package/ftm-inbox/backend/routes/health.py +52 -52
  128. package/ftm-inbox/backend/routes/inbox.py +68 -68
  129. package/ftm-inbox/backend/routes/plan.py +271 -271
  130. package/ftm-inbox/bin/launchagent.mjs +91 -91
  131. package/ftm-inbox/bin/setup.mjs +188 -188
  132. package/ftm-inbox/bin/start.sh +10 -10
  133. package/ftm-inbox/bin/status.sh +17 -17
  134. package/ftm-inbox/bin/stop.sh +8 -8
  135. package/ftm-inbox/config.example.yml +55 -55
  136. package/ftm-inbox/package-lock.json +2898 -2898
  137. package/ftm-inbox/package.json +26 -26
  138. package/ftm-inbox/postcss.config.js +6 -6
  139. package/ftm-inbox/src/app.css +199 -199
  140. package/ftm-inbox/src/app.html +18 -18
  141. package/ftm-inbox/src/lib/api.ts +166 -166
  142. package/ftm-inbox/src/lib/components/ExecutionLog.svelte +81 -81
  143. package/ftm-inbox/src/lib/components/InboxFeed.svelte +143 -143
  144. package/ftm-inbox/src/lib/components/PlanStep.svelte +271 -271
  145. package/ftm-inbox/src/lib/components/PlanView.svelte +206 -206
  146. package/ftm-inbox/src/lib/components/StreamPanel.svelte +99 -99
  147. package/ftm-inbox/src/lib/components/TaskCard.svelte +190 -190
  148. package/ftm-inbox/src/lib/components/ui/EmptyState.svelte +63 -63
  149. package/ftm-inbox/src/lib/components/ui/KawaiiCard.svelte +86 -86
  150. package/ftm-inbox/src/lib/components/ui/PillButton.svelte +106 -106
  151. package/ftm-inbox/src/lib/components/ui/StatusBadge.svelte +67 -67
  152. package/ftm-inbox/src/lib/components/ui/StreamDrawer.svelte +149 -149
  153. package/ftm-inbox/src/lib/components/ui/ThemeToggle.svelte +80 -80
  154. package/ftm-inbox/src/lib/theme.ts +47 -47
  155. package/ftm-inbox/src/routes/+layout.svelte +76 -76
  156. package/ftm-inbox/src/routes/+page.svelte +401 -401
  157. package/ftm-inbox/svelte.config.js +12 -12
  158. package/ftm-inbox/tailwind.config.ts +63 -63
  159. package/ftm-inbox/tsconfig.json +13 -13
  160. package/ftm-inbox/vite.config.ts +6 -6
  161. package/ftm-intent/SKILL.md +241 -241
  162. package/ftm-intent.yml +2 -2
  163. package/ftm-manifest.json +3794 -3794
  164. package/ftm-map/SKILL.md +291 -291
  165. package/ftm-map/scripts/db.py +712 -712
  166. package/ftm-map/scripts/index.py +415 -415
  167. package/ftm-map/scripts/parser.py +224 -224
  168. package/ftm-map/scripts/queries/go-tags.scm +20 -20
  169. package/ftm-map/scripts/queries/javascript-tags.scm +35 -35
  170. package/ftm-map/scripts/queries/python-tags.scm +31 -31
  171. package/ftm-map/scripts/queries/ruby-tags.scm +19 -19
  172. package/ftm-map/scripts/queries/rust-tags.scm +37 -37
  173. package/ftm-map/scripts/queries/typescript-tags.scm +41 -41
  174. package/ftm-map/scripts/query.py +301 -301
  175. package/ftm-map/scripts/ranker.py +377 -377
  176. package/ftm-map/scripts/requirements.txt +5 -5
  177. package/ftm-map/scripts/setup-hooks.sh +27 -27
  178. package/ftm-map/scripts/setup.sh +56 -56
  179. package/ftm-map/scripts/test_db.py +364 -364
  180. package/ftm-map/scripts/test_parser.py +174 -174
  181. package/ftm-map/scripts/test_query.py +183 -183
  182. package/ftm-map/scripts/test_ranker.py +199 -199
  183. package/ftm-map/scripts/views.py +591 -591
  184. package/ftm-map.yml +2 -2
  185. package/ftm-mind/SKILL.md +201 -1943
  186. package/ftm-mind/evals/promptfoo.yaml +142 -142
  187. package/ftm-mind/references/blackboard-protocol.md +110 -0
  188. package/ftm-mind/references/blackboard-schema.md +328 -328
  189. package/ftm-mind/references/complexity-guide.md +110 -110
  190. package/ftm-mind/references/complexity-sizing.md +138 -0
  191. package/ftm-mind/references/decide-act-protocol.md +172 -0
  192. package/ftm-mind/references/direct-execution.md +51 -0
  193. package/ftm-mind/references/environment-discovery.md +77 -0
  194. package/ftm-mind/references/event-registry.md +319 -319
  195. package/ftm-mind/references/mcp-inventory.md +300 -296
  196. package/ftm-mind/references/ops-routing.md +47 -0
  197. package/ftm-mind/references/orient-protocol.md +234 -0
  198. package/ftm-mind/references/personality.md +40 -0
  199. package/ftm-mind/references/protocols/COMPLEXITY-SIZING.md +72 -72
  200. package/ftm-mind/references/protocols/MCP-HEURISTICS.md +32 -32
  201. package/ftm-mind/references/protocols/PLAN-APPROVAL.md +80 -80
  202. package/ftm-mind/references/reflexion-protocol.md +249 -249
  203. package/ftm-mind/references/routing/SCENARIOS.md +22 -22
  204. package/ftm-mind/references/routing-scenarios.md +35 -35
  205. package/ftm-mind.yml +2 -2
  206. package/ftm-ops.yml +4 -0
  207. package/ftm-pause/SKILL.md +395 -395
  208. package/ftm-pause/references/protocols/SKILL-RESTORE-PROTOCOLS.md +186 -186
  209. package/ftm-pause/references/protocols/VALIDATION.md +80 -80
  210. package/ftm-pause.yml +2 -2
  211. package/ftm-researcher/SKILL.md +275 -275
  212. package/ftm-researcher/evals/agent-diversity.yaml +17 -17
  213. package/ftm-researcher/evals/synthesis-quality.yaml +12 -12
  214. package/ftm-researcher/evals/trigger-accuracy.yaml +39 -39
  215. package/ftm-researcher/references/adaptive-search.md +116 -116
  216. package/ftm-researcher/references/agent-prompts.md +193 -193
  217. package/ftm-researcher/references/council-integration.md +193 -193
  218. package/ftm-researcher/references/output-format.md +203 -203
  219. package/ftm-researcher/references/synthesis-pipeline.md +165 -165
  220. package/ftm-researcher/scripts/score_credibility.py +234 -234
  221. package/ftm-researcher/scripts/validate_research.py +92 -92
  222. package/ftm-researcher.yml +2 -2
  223. package/ftm-resume/SKILL.md +518 -518
  224. package/ftm-resume/references/protocols/VALIDATION.md +172 -172
  225. package/ftm-resume.yml +2 -2
  226. package/ftm-retro/SKILL.md +380 -380
  227. package/ftm-retro/references/protocols/SCORING-RUBRICS.md +89 -89
  228. package/ftm-retro/references/templates/REPORT-FORMAT.md +109 -109
  229. package/ftm-retro.yml +2 -2
  230. package/ftm-routine/SKILL.md +170 -170
  231. package/ftm-routine.yml +4 -4
  232. package/ftm-state/blackboard/capabilities.json +5 -5
  233. package/ftm-state/blackboard/capabilities.schema.json +27 -27
  234. package/ftm-state/blackboard/context.json +37 -23
  235. package/ftm-state/blackboard/experiences/doom-statusline-fix.json +26 -0
  236. package/ftm-state/blackboard/experiences/hackathon-pages-site.json +26 -0
  237. package/ftm-state/blackboard/experiences/hindsight-sso-kickoff.json +42 -0
  238. package/ftm-state/blackboard/experiences/index.json +58 -9
  239. package/ftm-state/blackboard/experiences/learning-ragnarok-api-access.json +23 -0
  240. package/ftm-state/blackboard/experiences/nordlayer-members-auto-assign.json +26 -0
  241. package/ftm-state/blackboard/experiences/saml2aws-stale-session-fix.json +41 -0
  242. package/ftm-state/blackboard/patterns.json +6 -6
  243. package/ftm-state/schemas/context.schema.json +130 -130
  244. package/ftm-state/schemas/experience-index.schema.json +77 -77
  245. package/ftm-state/schemas/experience.schema.json +78 -78
  246. package/ftm-state/schemas/patterns.schema.json +44 -44
  247. package/ftm-upgrade/SKILL.md +194 -194
  248. package/ftm-upgrade/scripts/check-version.sh +76 -76
  249. package/ftm-upgrade/scripts/upgrade.sh +143 -143
  250. package/ftm-upgrade.yml +2 -2
  251. package/ftm-verify.yml +2 -2
  252. package/ftm.yml +2 -2
  253. package/hooks/ftm-auto-log.sh +137 -0
  254. package/hooks/ftm-blackboard-enforcer.sh +93 -93
  255. package/hooks/ftm-discovery-reminder.sh +90 -90
  256. package/hooks/ftm-drafts-gate.sh +61 -61
  257. package/hooks/ftm-event-logger.mjs +107 -107
  258. package/hooks/ftm-install-hooks.sh +240 -0
  259. package/hooks/ftm-learning-capture.sh +117 -0
  260. package/hooks/ftm-map-autodetect.sh +79 -79
  261. package/hooks/ftm-pending-sync-check.sh +22 -22
  262. package/hooks/ftm-plan-gate.sh +92 -92
  263. package/hooks/ftm-post-commit-trigger.sh +57 -57
  264. package/hooks/ftm-post-compaction.sh +138 -0
  265. package/hooks/ftm-pre-compaction.sh +147 -0
  266. package/hooks/ftm-session-end.sh +52 -0
  267. package/hooks/ftm-session-snapshot.sh +213 -0
  268. package/hooks/ftm-task-loader.sh +100 -0
  269. package/hooks/settings-template.json +91 -81
  270. package/install.sh +363 -363
  271. package/package.json +84 -84
  272. 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()
@@ -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).