bookmark-research 0.5.0-beta.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.
- package/.agents/plugins/marketplace.json +20 -0
- package/.codex-plugin/plugin.json +46 -0
- package/LICENSE +674 -0
- package/README.md +128 -0
- package/README.zh.md +126 -0
- package/bin/bookmark-research.js +30 -0
- package/config/providers.json +98 -0
- package/docs/bookmark-research-architecture.md +124 -0
- package/docs/cli-launcher-plan.md +58 -0
- package/docs/details.en.md +272 -0
- package/docs/details.md +93 -0
- package/docs/harness-compatibility.md +304 -0
- package/docs/harness-sources.json +342 -0
- package/docs/host-validation.json +298 -0
- package/docs/install-design-0.2.0.md +57 -0
- package/docs/installation.en.md +197 -0
- package/docs/installation.md +316 -0
- package/docs/instructions.en.md +42 -0
- package/docs/instructions.md +42 -0
- package/docs/mcp-aggregation-0.2.0.md +92 -0
- package/docs/multi-host-installation-plan.md +163 -0
- package/docs/prompt-reference.en.md +104 -0
- package/docs/prompt-reference.md +104 -0
- package/docs/provider-smoke-0.2.0.json +235 -0
- package/docs/release-notes-0.5.0-beta.1.md +33 -0
- package/docs/research-0.2.0.md +140 -0
- package/docs/research-sources-0.2.0.json +9360 -0
- package/docs/user-guide.en.md +74 -0
- package/docs/user-guide.md +74 -0
- package/docs/validation-0.2.0.md +126 -0
- package/docs/validation-0.4.0.md +154 -0
- package/docs/wiki-quality.md +105 -0
- package/docs/wiki-quality.zh.md +105 -0
- package/hosts/claude/runtime.js +13 -0
- package/hosts/codex/delegate.md +14 -0
- package/hosts/codex/prepare.py +61 -0
- package/hosts/dsh/plugin.js +36 -0
- package/hosts/dsh/runtime.js +14 -0
- package/hosts/dsh/workflow-call.py +45 -0
- package/hosts/pi/register-workflow.py +72 -0
- package/hosts/pi/runtime.js +18 -0
- package/hosts/shared/research-call.py +76 -0
- package/hosts/shared/research-flow.js +208 -0
- package/hosts/shared/workflow.json +15 -0
- package/install.sh +310 -0
- package/package.json +12 -0
- package/scripts/build_zip.py +143 -0
- package/scripts/export_bundle.py +496 -0
- package/scripts/host_assets.py +70 -0
- package/scripts/host_clients.py +196 -0
- package/scripts/host_install.py +277 -0
- package/scripts/install.py +593 -0
- package/scripts/launcher.py +186 -0
- package/scripts/verify_fixture.py +220 -0
- package/scripts/verify_live.py +62 -0
- package/scripts/verify_quality.py +87 -0
- package/skills/bookmark-research/SKILL.md +80 -0
- package/skills/bookmark-research/agents/openai.yaml +3 -0
- package/skills/bookmark-research/references/cli.md +150 -0
- package/skills/bookmark-research/references/deep-research.md +164 -0
- package/skills/bookmark-research/references/github-and-sync.md +30 -0
- package/skills/bookmark-research/references/host-workflows.md +101 -0
- package/skills/bookmark-research/references/package-semantics.md +78 -0
- package/skills/bookmark-research/references/research-methods.md +51 -0
- package/skills/bookmark-research/references/research-services.md +72 -0
- package/skills/bookmark-research/references/research-workflow.md +57 -0
- package/skills/bookmark-research/references/settings-and-archive.md +104 -0
- package/skills/bookmark-research/references/source-lifecycle.md +75 -0
- package/skills/bookmark-research/references/wiki-and-evaluation.md +63 -0
- package/skills/bookmark-research/references/zh/cli.md +150 -0
- package/skills/bookmark-research/references/zh/deep-research.md +164 -0
- package/skills/bookmark-research/references/zh/github-and-sync.md +30 -0
- package/skills/bookmark-research/references/zh/host-workflows.md +101 -0
- package/skills/bookmark-research/references/zh/package-semantics.md +78 -0
- package/skills/bookmark-research/references/zh/research-methods.md +51 -0
- package/skills/bookmark-research/references/zh/research-services.md +70 -0
- package/skills/bookmark-research/references/zh/research-workflow.md +57 -0
- package/skills/bookmark-research/references/zh/settings-and-archive.md +104 -0
- package/skills/bookmark-research/references/zh/skill-guide.md +75 -0
- package/skills/bookmark-research/references/zh/source-lifecycle.md +75 -0
- package/skills/bookmark-research/references/zh/wiki-and-evaluation.md +63 -0
- package/src/archive.py +302 -0
- package/src/bookmark_index.py +913 -0
- package/src/cli.py +435 -0
- package/src/credentials.py +82 -0
- package/src/evaluation.py +337 -0
- package/src/jina.py +96 -0
- package/src/mcp_server.py +641 -0
- package/src/onboarding.py +352 -0
- package/src/provider_adapters.py +152 -0
- package/src/readiness.py +283 -0
- package/src/remote_mcp.py +295 -0
- package/src/research.py +1334 -0
- package/src/research_coverage.py +160 -0
- package/src/research_services.py +684 -0
- package/src/routing.py +202 -0
- package/src/search_results.py +244 -0
- package/src/service_http.py +139 -0
- package/src/settings.py +247 -0
- package/src/source_inputs.py +201 -0
- package/src/source_manager.py +348 -0
- package/src/source_watcher.py +68 -0
- package/src/textio.py +9 -0
- package/src/tui.py +586 -0
- package/src/web_search.py +603 -0
- package/src/wiki.py +383 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""The `bookmark-research` command: install, configure, check and update from one entry."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
10
|
+
sys.path.insert(0, str(ROOT / "scripts"))
|
|
11
|
+
sys.path.insert(0, str(ROOT / "src"))
|
|
12
|
+
|
|
13
|
+
USAGE = """usage: bookmark-research [--lang auto|en|zh] [COMMAND] [ARGS...]
|
|
14
|
+
|
|
15
|
+
(no command) Open the menu in a terminal; print status JSON otherwise
|
|
16
|
+
install [HOST...] Install into hosts: codex, claude, pi, dsh (wizard when omitted)
|
|
17
|
+
update [HOST...] Update installed hosts (all when omitted)
|
|
18
|
+
verify [HOST...] Verify installed hosts (all when omitted)
|
|
19
|
+
setup Preferences, services, hidden API-key entry and checks
|
|
20
|
+
status Installed hosts, preferences and key status as JSON
|
|
21
|
+
config show|set View or change preferences (set reads --input FILE or -)
|
|
22
|
+
doctor Check Python and SQLite FTS5
|
|
23
|
+
|
|
24
|
+
Other options are passed through, e.g. install claude dsh --profile web --non-interactive.
|
|
25
|
+
API keys are never accepted as arguments: use setup or environment variables.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def install_home():
|
|
30
|
+
data = Path(os.environ.get("XDG_DATA_HOME", str(Path.home() / ".local/share"))).expanduser()
|
|
31
|
+
if not data.is_absolute():
|
|
32
|
+
data = Path.home() / ".local/share"
|
|
33
|
+
return Path(os.environ.get("BOOKMARK_RESEARCH_INSTALL_DIR", str(data / "bookmark-research/installations"))).expanduser()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def installed_targets(codex="codex"):
|
|
37
|
+
"""Read managed receipts and the Codex registry; never contacts a network service."""
|
|
38
|
+
rows = []
|
|
39
|
+
home = install_home()
|
|
40
|
+
for receipt in sorted(home.glob("*/receipt.json")) if home.is_dir() else []:
|
|
41
|
+
try:
|
|
42
|
+
value = json.loads(receipt.read_text(encoding="utf-8"))
|
|
43
|
+
target = value["target"]
|
|
44
|
+
except (OSError, ValueError, KeyError, TypeError):
|
|
45
|
+
continue
|
|
46
|
+
rows.append({"host": target.get("host"), "scope": target.get("scope"), "project": target.get("project"),
|
|
47
|
+
"profile": target.get("profile"), "version": value.get("version"), "state": value.get("state")})
|
|
48
|
+
try:
|
|
49
|
+
from install import CodexCli, _installed
|
|
50
|
+
import shutil
|
|
51
|
+
if shutil.which(codex):
|
|
52
|
+
plugin = _installed(CodexCli(codex, 30))
|
|
53
|
+
if plugin:
|
|
54
|
+
rows.insert(0, {"host": "codex", "scope": None, "project": None, "profile": None,
|
|
55
|
+
"version": plugin.get("version"), "state": "installed"})
|
|
56
|
+
except (OSError, ValueError, RuntimeError, subprocess.TimeoutExpired):
|
|
57
|
+
pass
|
|
58
|
+
return rows
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def status():
|
|
62
|
+
from credentials import Credentials
|
|
63
|
+
from settings import Settings
|
|
64
|
+
settings = Settings()
|
|
65
|
+
current = settings.load()
|
|
66
|
+
return {"hosts": installed_targets(),
|
|
67
|
+
"preferences": {"depth": current["research"]["depth"], "language": current["research"]["response_language"],
|
|
68
|
+
"search": current["search"]["providers"], "fallback": current["search"]["fallback_providers"],
|
|
69
|
+
"readers": Settings.fetch_providers(current), "archive": current["archive"]["enabled"]},
|
|
70
|
+
"credentials": {row["name"]: row["source"] or "missing" for row in Credentials(settings).describe()["credentials"]},
|
|
71
|
+
"settings_path": str(settings.path)}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _split(arguments):
|
|
75
|
+
hosts, rest = [], []
|
|
76
|
+
for index, value in enumerate(arguments):
|
|
77
|
+
if value.startswith("-"):
|
|
78
|
+
rest = arguments[index:]
|
|
79
|
+
break
|
|
80
|
+
hosts.extend(part for part in value.split(",") if part)
|
|
81
|
+
return hosts, rest
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _install_action(action, arguments, language):
|
|
85
|
+
import install
|
|
86
|
+
hosts, rest = _split(arguments)
|
|
87
|
+
if not hosts and action != "install" and "--host" not in rest:
|
|
88
|
+
targets = installed_targets()
|
|
89
|
+
if not targets:
|
|
90
|
+
print(json.dumps({"error": "Nothing is installed yet; run bookmark-research install"}), file=sys.stderr)
|
|
91
|
+
return 1
|
|
92
|
+
codes = []
|
|
93
|
+
for row in targets:
|
|
94
|
+
extra = ["--host", row["host"]]
|
|
95
|
+
for key in ("scope", "project", "profile"):
|
|
96
|
+
if row.get(key):
|
|
97
|
+
extra += ["--" + key, row[key]]
|
|
98
|
+
codes.append(install.main([action, "--lang", language, *extra, *rest]))
|
|
99
|
+
return max(codes)
|
|
100
|
+
return install.main([action, "--lang", language, *[item for host in hosts for item in ("--host", host)], *rest])
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _cli(arguments, language):
|
|
104
|
+
command = [sys.executable, str(ROOT / "src/cli.py"), *arguments]
|
|
105
|
+
if arguments and arguments[0] == "setup" and "--lang" not in arguments:
|
|
106
|
+
command += ["--lang", language]
|
|
107
|
+
return subprocess.call(command)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def run(arguments, language):
|
|
111
|
+
if not arguments:
|
|
112
|
+
return menu(language)
|
|
113
|
+
command, rest = arguments[0], arguments[1:]
|
|
114
|
+
if command in ("-h", "--help", "help"):
|
|
115
|
+
print(USAGE)
|
|
116
|
+
return 0
|
|
117
|
+
if command in ("install", "update", "verify"):
|
|
118
|
+
return _install_action(command, rest, language)
|
|
119
|
+
if command == "status":
|
|
120
|
+
print(json.dumps(status(), ensure_ascii=False, indent=2))
|
|
121
|
+
return 0
|
|
122
|
+
if command in ("setup", "config", "doctor"):
|
|
123
|
+
return _cli([command, *rest], language)
|
|
124
|
+
print(USAGE, file=sys.stderr)
|
|
125
|
+
return 2
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def menu(language):
|
|
129
|
+
from onboarding import Console
|
|
130
|
+
with Console.open("auto", Console.resolve_language(language)) as console:
|
|
131
|
+
if console is None:
|
|
132
|
+
print(json.dumps(status(), ensure_ascii=False, indent=2))
|
|
133
|
+
return 0
|
|
134
|
+
console.intro("Bookmark Research", "Bookmark Research")
|
|
135
|
+
while True:
|
|
136
|
+
current = status()
|
|
137
|
+
hosts = ", ".join(row["host"] + ("(" + (row["profile"] or row["scope"] or "") + ")" if row["profile"] or row["scope"] else "")
|
|
138
|
+
for row in current["hosts"]) or console.text("none", "无")
|
|
139
|
+
missing = [name.split("_")[0].lower() for name, source in current["credentials"].items() if source == "missing"]
|
|
140
|
+
console.say("Installed: " + hosts, "已安装:" + hosts)
|
|
141
|
+
console.say("Search: " + ", ".join(current["preferences"]["search"]) + (" · keys missing: " + ", ".join(missing) if missing else ""),
|
|
142
|
+
"搜索服务:" + ", ".join(current["preferences"]["search"]) + (" · 未配置密钥:" + ", ".join(missing) if missing else ""))
|
|
143
|
+
choice = console.select("What would you like to do?", "要做什么?", _menu_options(), "setup")
|
|
144
|
+
if choice == "exit":
|
|
145
|
+
console.outro("Bye.", "再见。")
|
|
146
|
+
return 0
|
|
147
|
+
code = {"setup": lambda: _cli(["setup"], console.language),
|
|
148
|
+
"check": lambda: _cli(["setup", "--non-interactive"], console.language),
|
|
149
|
+
"install": lambda: _install_action("install", ["--interactive"], console.language),
|
|
150
|
+
"update": lambda: _install_action("update", [], console.language),
|
|
151
|
+
"status": lambda: print(json.dumps(status(), ensure_ascii=False, indent=2)) or 0}[choice]()
|
|
152
|
+
if code:
|
|
153
|
+
console.warn("Finished with exit code %s." % code, "结束,退出码 %s。" % code)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _menu_options():
|
|
157
|
+
from tui import Option
|
|
158
|
+
return [Option("setup", "Change preferences and API keys", "修改配置与 API Key"),
|
|
159
|
+
Option("check", "Check services", "检查服务是否可用"),
|
|
160
|
+
Option("install", "Install into another host", "安装到其他宿主"),
|
|
161
|
+
Option("update", "Update installed hosts", "更新已安装宿主"),
|
|
162
|
+
Option("status", "Show status JSON", "查看状态(JSON)"),
|
|
163
|
+
Option("exit", "Exit", "退出")]
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def main(argv=None):
|
|
167
|
+
from textio import utf8_stdio
|
|
168
|
+
utf8_stdio()
|
|
169
|
+
arguments = list(sys.argv[1:] if argv is None else argv)
|
|
170
|
+
language = "auto"
|
|
171
|
+
if arguments[:1] == ["--lang"] and len(arguments) > 1:
|
|
172
|
+
language, arguments = arguments[1], arguments[2:]
|
|
173
|
+
elif arguments and arguments[0].startswith("--lang="):
|
|
174
|
+
language, arguments = arguments[0].split("=", 1)[1], arguments[1:]
|
|
175
|
+
if language not in ("auto", "en", "zh"):
|
|
176
|
+
print("--lang must be auto, en or zh", file=sys.stderr)
|
|
177
|
+
return 2
|
|
178
|
+
from install import resolve_language
|
|
179
|
+
try:
|
|
180
|
+
return run(arguments, resolve_language(language))
|
|
181
|
+
except KeyboardInterrupt:
|
|
182
|
+
return 130
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
if __name__ == "__main__":
|
|
186
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Offline validation pack: source fidelity, context and a research lifecycle.
|
|
3
|
+
|
|
4
|
+
Default inputs are synthetic. --package additionally checks a user-supplied
|
|
5
|
+
package without changing it or sending anything to web providers.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import hashlib
|
|
10
|
+
import json
|
|
11
|
+
import sys
|
|
12
|
+
from datetime import datetime, timezone
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
16
|
+
sys.path.insert(0, str(ROOT / "src"))
|
|
17
|
+
from bookmark_index import BookmarkIndex
|
|
18
|
+
from research import ResearchSessions
|
|
19
|
+
from search_results import fuse_results
|
|
20
|
+
from settings import Settings
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def fingerprints(package):
|
|
24
|
+
return {path.relative_to(package).as_posix(): hashlib.sha256(path.read_bytes()).hexdigest()
|
|
25
|
+
for path in sorted(package.rglob("*")) if path.is_file()}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def raw_counts(package):
|
|
29
|
+
urls = []
|
|
30
|
+
sections = 0
|
|
31
|
+
def walk(items):
|
|
32
|
+
for item in items:
|
|
33
|
+
if isinstance(item.get("url"), str) and item["url"]:
|
|
34
|
+
urls.append(item["url"])
|
|
35
|
+
walk(item.get("children", []))
|
|
36
|
+
for path in package.rglob("*.json"):
|
|
37
|
+
value = json.loads(path.read_text(encoding="utf-8"))
|
|
38
|
+
if value.get("format") != "bookmark-canvas-section":
|
|
39
|
+
continue
|
|
40
|
+
sections += 1
|
|
41
|
+
if value.get("fileRole") == "copy-anchor":
|
|
42
|
+
continue
|
|
43
|
+
walk([value["tree"]] if "tree" in value else value.get("items", []))
|
|
44
|
+
return {"bookmarks": len(urls), "unique_urls": len(set(urls)), "sections": sections}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class FixtureProvider:
|
|
48
|
+
"""Recorded fictional responses; never instantiates a network client."""
|
|
49
|
+
|
|
50
|
+
def __init__(self, fixture):
|
|
51
|
+
self.fixture = fixture
|
|
52
|
+
|
|
53
|
+
def search(self, targets, providers, limit_per_target):
|
|
54
|
+
batches = [{"provider": provider, "target": target["target"], "query": target["query"],
|
|
55
|
+
"status": "ok", "retrieved_at": "2026-09-10T00:00:00Z", "results": [
|
|
56
|
+
{"url": url, "title": self.fixture["pages"][url].get("title", "Unavailable fixture"),
|
|
57
|
+
"snippet": "Synthetic search snippet; read the fixture page for evidence."}
|
|
58
|
+
for url in self.fixture["search_results"][target["target"]]]}
|
|
59
|
+
for provider in providers for target in targets]
|
|
60
|
+
return {**fuse_results({"batches": batches, "limit_per_target": limit_per_target}),
|
|
61
|
+
"batches": batches, "usage": {"tool_calls": len(batches)}}
|
|
62
|
+
|
|
63
|
+
def fetch(self, urls, provider, archive, max_characters):
|
|
64
|
+
rows = [{"url": url, **self.fixture["pages"].get(url, {"error": "No synthetic response recorded for this URL"})}
|
|
65
|
+
for url in urls]
|
|
66
|
+
return {"provider": provider, "urls": urls, "tool": "fixture_fetch", "request_arguments": {"urls": urls},
|
|
67
|
+
"retrieved_at": "2026-09-10T00:00:00Z", "requested_max_characters": max_characters,
|
|
68
|
+
"usage": {"tool_calls": 1}, "result": {"structuredContent": {"results": rows}}}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def verify_package(package, output, expected=None, queries=()):
|
|
72
|
+
before = fingerprints(package)
|
|
73
|
+
expected_raw = raw_counts(package)
|
|
74
|
+
with BookmarkIndex(output / "index.sqlite3") as index:
|
|
75
|
+
first = index.sync(package, "validation")
|
|
76
|
+
status = index.status("validation")
|
|
77
|
+
for name, number in {**expected_raw, **(expected or {})}.items():
|
|
78
|
+
if status["counts"][name] != number:
|
|
79
|
+
raise ValueError("Count mismatch for %s: raw/fixture=%s, index=%s" % (name, number, status["counts"][name]))
|
|
80
|
+
second = index.sync(package, "validation")
|
|
81
|
+
if second["changed_files"] != 0:
|
|
82
|
+
raise ValueError("An unchanged package was not recognized incrementally")
|
|
83
|
+
for case in queries:
|
|
84
|
+
options = {key: value for key, value in case.items() if key != "total"}
|
|
85
|
+
if index.search("validation", **options)["total"] != case["total"]:
|
|
86
|
+
raise ValueError("Fixture scope query returned the wrong count")
|
|
87
|
+
context = index.context("validation")
|
|
88
|
+
inventory = index.inventory(["validation"])
|
|
89
|
+
if (inventory["counts"]["unique_urls"] != status["counts"]["unique_urls"] or
|
|
90
|
+
inventory["counts"]["bookmark_instances"] != status["counts"]["bookmarks"]):
|
|
91
|
+
raise ValueError("Complete inventory changed the raw URL or bookmark-instance counts")
|
|
92
|
+
for source_file in inventory["files"]:
|
|
93
|
+
if before.get(source_file["file_path"]) != source_file["sha256"]:
|
|
94
|
+
raise ValueError("Inventory source hash differs from the original file")
|
|
95
|
+
sessions = ResearchSessions(output / "inventory-research", settings=Settings(output / "settings.json"),
|
|
96
|
+
db_path=output / "index.sqlite3")
|
|
97
|
+
frozen = sessions.start("Offline inventory acceptance", [{"id": "q1", "question": "Were all original input IDs preserved?"}],
|
|
98
|
+
source_ids=["validation"], providers=["exa"])
|
|
99
|
+
rows, offset, pages = [], 0, 0
|
|
100
|
+
while offset is not None:
|
|
101
|
+
page = sessions.inventory(frozen["research_id"], offset=offset, limit=100)
|
|
102
|
+
rows.extend(page["items"])
|
|
103
|
+
offset = page["next_offset"]
|
|
104
|
+
pages += 1
|
|
105
|
+
if [row["id"] for row in rows] != [row["id"] for row in inventory["entries"]]:
|
|
106
|
+
raise ValueError("Frozen research inventory pagination lost or reordered inputs")
|
|
107
|
+
manifest_path = Path(frozen["inventory_manifest"])
|
|
108
|
+
if json.loads(manifest_path.read_text(encoding="utf-8")) != inventory:
|
|
109
|
+
raise ValueError("Frozen manifest lost original bookmark metadata or relationships")
|
|
110
|
+
sessions.finish(frozen["research_id"], "Offline inventory inspection only; source pages were not read.",
|
|
111
|
+
status="incomplete", limitations=["This is source-preservation acceptance, not substantive research."])
|
|
112
|
+
if before != fingerprints(package):
|
|
113
|
+
raise ValueError("Source package changed during validation")
|
|
114
|
+
return {"counts": status["counts"], "source_files": len(before), "source_sha256": before,
|
|
115
|
+
"source_unchanged": True, "unchanged_second_sync": True,
|
|
116
|
+
"scope_queries_checked": len(queries), "warnings": first.get("warnings", []),
|
|
117
|
+
"context_nodes": len(context["nodes"]),
|
|
118
|
+
"inventory": {"counts": inventory["counts"], "input_version": inventory["input_version"],
|
|
119
|
+
"manifest_path": str(manifest_path), "manifest_sha256": hashlib.sha256(manifest_path.read_bytes()).hexdigest(),
|
|
120
|
+
"pages_read": pages, "all_ids_preserved": True, "whole_scope_preserved": True,
|
|
121
|
+
"initial_fetch_plan": frozen["initial_fetch_plan"]}}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def verify_research(fixture, output):
|
|
125
|
+
sessions = ResearchSessions(output / "research", settings=Settings(output / "settings.json"),
|
|
126
|
+
engine=FixtureProvider(fixture), db_path=output / "fixture/index.sqlite3")
|
|
127
|
+
with BookmarkIndex(output / "fixture/index.sqlite3") as index:
|
|
128
|
+
rows = index.search("validation", targets=["https://docs.example.test/search"])["results"]
|
|
129
|
+
refs = [{key: row[key] for key in ("source_id", "section_id", "item_id")} for row in rows]
|
|
130
|
+
task = sessions.start(**fixture["brief"], bookmark_refs=refs)
|
|
131
|
+
identifier = task["research_id"]
|
|
132
|
+
inventory = sessions.inventory(identifier)["items"]
|
|
133
|
+
by_url = {row["original_url"]: row["id"] for row in inventory}
|
|
134
|
+
|
|
135
|
+
def review(source, question):
|
|
136
|
+
source_id = source["id"]
|
|
137
|
+
entry = {"kind": "inventory_review", "inventory_id": by_url[source["url"]]}
|
|
138
|
+
if source["body_file"] is None:
|
|
139
|
+
sessions.record(identifier, {**entry, "disposition": "blocked",
|
|
140
|
+
"text": "Synthetic source has no saved page body: " + source["extraction_status"]})
|
|
141
|
+
return None, None
|
|
142
|
+
text = sessions.source(identifier, source_id)["text"]
|
|
143
|
+
sessions.record(identifier, {"kind": "source_review", "source_id": source_id, "verdict": "accepted",
|
|
144
|
+
"text": "Matches the selected synthetic fixture page."})
|
|
145
|
+
quote = text.splitlines()[0]
|
|
146
|
+
claim = sessions.record(identifier, {"kind": "claim", "question_id": question, "statement": quote,
|
|
147
|
+
"citations": [{"source_id": source_id, "quote": quote}]})["recorded"]["id"]
|
|
148
|
+
sessions.record(identifier, {**entry, "disposition": "reviewed", "question_ids": [question],
|
|
149
|
+
"source_ids": [source_id], "claim_ids": [claim], "text": quote})
|
|
150
|
+
return quote, claim
|
|
151
|
+
|
|
152
|
+
for question, page in (("q1", "https://docs.example.test/search"), ("q2", "https://docs.example.test/limits")):
|
|
153
|
+
sessions.search(identifier, question + "-search", [{"question_id": question, "query": fixture["brief"]["questions"][int(question[-1]) - 1]["question"]}])
|
|
154
|
+
fetched = sessions.fetch(identifier, question + "-read", question, [page])
|
|
155
|
+
quote, claim = review(fetched["sources"][0], question)
|
|
156
|
+
sessions.record(identifier, {"kind": "answer", "question_id": question, "answer": quote, "claim_ids": [claim]})
|
|
157
|
+
if question == "q1":
|
|
158
|
+
sessions.record(identifier, {"kind": "gap", "question_id": "q2", "text": "Need the extraction limitations page."})
|
|
159
|
+
remaining = [row["original_url"] for row in sessions.coverage(identifier, filter="unreviewed")["items"]]
|
|
160
|
+
last_batch = sessions.fetch(identifier, "q2-remaining-inputs", "q2", remaining)
|
|
161
|
+
for source in last_batch["sources"]:
|
|
162
|
+
review(source, "q2")
|
|
163
|
+
failure = next(source for source in last_batch["sources"] if source["url"] == "https://docs.example.test/unavailable")
|
|
164
|
+
if last_batch["status"] != "partial" or failure["body_file"] is not None:
|
|
165
|
+
raise ValueError("A failed fixture page was presented as source evidence")
|
|
166
|
+
reopened = ResearchSessions(output / "research", settings=Settings(output / "settings.json"),
|
|
167
|
+
engine=FixtureProvider(fixture))
|
|
168
|
+
replay = reopened.fetch(identifier, "q1-read", "q1", ["https://docs.example.test/search"])
|
|
169
|
+
if not replay["replayed"]:
|
|
170
|
+
raise ValueError("A saved operation was resubmitted")
|
|
171
|
+
coverage = reopened.coverage(identifier)
|
|
172
|
+
if (coverage["total"] != fixture["expected_counts"]["unique_urls"] or
|
|
173
|
+
coverage["metrics"]["accounted_for"]["rate"] != 1 or coverage["completion_ready"]):
|
|
174
|
+
raise ValueError("Failed or missing fixture pages must remain explicit gaps in the complete input scope")
|
|
175
|
+
try:
|
|
176
|
+
reopened.finish(identifier, "This attempt must fail because original pages remain unread.")
|
|
177
|
+
except ValueError as error:
|
|
178
|
+
if "coverage incomplete" not in str(error):
|
|
179
|
+
raise
|
|
180
|
+
else:
|
|
181
|
+
raise ValueError("The coverage gate accepted unread original fixture pages")
|
|
182
|
+
finished = reopened.finish(identifier, "Synthetic contract run: supported answers with explicit unread input gaps.", status="incomplete",
|
|
183
|
+
limitations=["Fictional provider responses test the lifecycle, not real provider quality.",
|
|
184
|
+
"Three original fixture pages have no body and remain blocked; no successful completion is claimed."])
|
|
185
|
+
return {"research_id": identifier, "status": finished["status"], "usage": finished["usage"],
|
|
186
|
+
"bookmark_context_count": len(finished["bookmark_context"]), "sources": len(finished["sources"]),
|
|
187
|
+
"claims": len(finished["claims"]), "artifacts": finished["artifacts"], "network_calls": 0,
|
|
188
|
+
"source_scope": finished["source_scope"], "coverage": coverage["metrics"],
|
|
189
|
+
"completion_gate_rejected_unread_inputs": True}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def main(argv=None):
|
|
193
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
194
|
+
parser.add_argument("--output", required=True, help="New or empty verification directory outside source packages and plugin")
|
|
195
|
+
parser.add_argument("--package", help="Additionally verify a supplied real package read-only")
|
|
196
|
+
args = parser.parse_args(argv)
|
|
197
|
+
try:
|
|
198
|
+
output = Settings.external_path(str(Path(args.output).expanduser().resolve()), "Verification directory")
|
|
199
|
+
if output.exists() and any(output.iterdir()):
|
|
200
|
+
raise ValueError("Use a new or empty output directory")
|
|
201
|
+
fixture_path = ROOT / "tests/fixtures/research-scenario.json"
|
|
202
|
+
if not fixture_path.is_file():
|
|
203
|
+
raise ValueError("Run this from the source checkout or the test pack; production bundles exclude test fixtures")
|
|
204
|
+
fixture = json.loads(fixture_path.read_text(encoding="utf-8"))
|
|
205
|
+
output.mkdir(parents=True, exist_ok=True)
|
|
206
|
+
summary = {"checked_at": datetime.now(timezone.utc).isoformat(), "network_calls": 0,
|
|
207
|
+
"fixture": verify_package(ROOT / "tests/fixtures/canvas", output / "fixture", fixture["expected_counts"], fixture["queries"]),
|
|
208
|
+
"research": verify_research(fixture, output)}
|
|
209
|
+
if args.package:
|
|
210
|
+
summary["provided_package"] = verify_package(Path(args.package).expanduser().resolve(), output / "provided")
|
|
211
|
+
(output / "verification.json").write_text(json.dumps(summary, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
212
|
+
print(json.dumps(summary, ensure_ascii=False, indent=2))
|
|
213
|
+
return 0
|
|
214
|
+
except (ValueError, OSError, RuntimeError) as error:
|
|
215
|
+
print(json.dumps({"error": str(error)}, ensure_ascii=False))
|
|
216
|
+
return 1
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
if __name__ == "__main__":
|
|
220
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Opt-in public-network smoke test; save inspectable provider evidence."""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
|
11
|
+
from web_search import SearchProviders
|
|
12
|
+
from settings import Settings
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def main():
|
|
16
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
17
|
+
parser.add_argument("--output", required=True, help="Evidence directory outside any data package")
|
|
18
|
+
parser.add_argument("--fetch-only", action="store_true", help="Skip search; verify actual extraction and archival")
|
|
19
|
+
parser.add_argument("--fetch-url", action="append", help="Public URL to read; repeat for a bounded batch")
|
|
20
|
+
parser.add_argument("--provider", choices=("exa", "parallel"), default="exa", help="Fetch provider")
|
|
21
|
+
parser.add_argument("--max-characters", type=int, default=12000)
|
|
22
|
+
args = parser.parse_args()
|
|
23
|
+
output = Settings.external_path(str(Path(args.output).expanduser().resolve()), "Verification output directory")
|
|
24
|
+
output.mkdir(parents=True, exist_ok=True)
|
|
25
|
+
settings = Settings(output / "settings.json")
|
|
26
|
+
settings.update({"archive": {"enabled": True, "directory": str(output / "knowledge")}})
|
|
27
|
+
engine = SearchProviders(timeout=30, settings=settings)
|
|
28
|
+
search = {"successful_provider_count": 0, "batches": [], "targets": []} if args.fetch_only else engine.search([
|
|
29
|
+
{"target": "Agent Plugins", "query": "Agent Plugins official specification plugin.json MCP skills"},
|
|
30
|
+
{"target": "DeepSeek Harness", "query": "DeepSeek Harness official plugin bundle MCP client"},
|
|
31
|
+
], limit_per_target=3)
|
|
32
|
+
(output / "live-search.json").write_text(json.dumps(search, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
33
|
+
try:
|
|
34
|
+
fetch = engine.fetch(args.fetch_url or ["https://agent-plugins.org/specification"],
|
|
35
|
+
provider=args.provider, max_characters=args.max_characters)
|
|
36
|
+
fetch_ok = True
|
|
37
|
+
except (RuntimeError, ValueError, OSError) as error:
|
|
38
|
+
fetch = {"error": str(error)}
|
|
39
|
+
fetch_ok = False
|
|
40
|
+
(output / "live-fetch.json").write_text(json.dumps(fetch, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
41
|
+
summary = {
|
|
42
|
+
"checked_at": datetime.now(timezone.utc).isoformat(),
|
|
43
|
+
"search_performed": not args.fetch_only,
|
|
44
|
+
"successful_provider_count": search["successful_provider_count"],
|
|
45
|
+
"batches": [{"provider": row["provider"], "target": row["target"], "status": row["status"],
|
|
46
|
+
"results": len(row["results"]), "error": row.get("error")} for row in search["batches"]],
|
|
47
|
+
"targets": [{"target": row["target"], "status": row["status"], "urls": [r["url"] for r in row["results"]]}
|
|
48
|
+
for row in search["targets"]],
|
|
49
|
+
"fetch_transport_ok": fetch_ok,
|
|
50
|
+
"archive": fetch.get("archive"),
|
|
51
|
+
"fetch_note": "Transport success does not assert origin freshness or per-URL extraction success; inspect live-fetch.json.",
|
|
52
|
+
"evidence_directory": str(output),
|
|
53
|
+
}
|
|
54
|
+
(output / "live-summary.json").write_text(json.dumps(summary, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
55
|
+
print(json.dumps(summary, ensure_ascii=False, indent=2))
|
|
56
|
+
archived = fetch.get("archive", {})
|
|
57
|
+
archive_ok = archived.get("status") == "saved" and all(page["page_body_archived"] for page in archived.get("pages", []))
|
|
58
|
+
return 0 if fetch_ok and archive_ok and all(row["status"] == "ok" and row["results"] for row in search["batches"]) else 1
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
if __name__ == "__main__":
|
|
62
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Run an offline annotated quality example; this does not run any provider."""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import math
|
|
7
|
+
import sys
|
|
8
|
+
import uuid
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
|
13
|
+
from evaluation import evaluate
|
|
14
|
+
from research import ResearchSessions
|
|
15
|
+
from settings import Settings
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def verify_expected(report, expected):
|
|
19
|
+
if report["run_count"] != expected["run_count"] or len(report["groups"]) != expected["group_count"]:
|
|
20
|
+
raise ValueError("Fixture run/group counts differ from the annotated expectation")
|
|
21
|
+
for field in ("answer_f1", "semantic_citation_accuracy"):
|
|
22
|
+
for run in report["runs"]:
|
|
23
|
+
actual = run["metrics"][field]
|
|
24
|
+
if actual is None or not math.isclose(actual, expected[field][run["run_id"]], abs_tol=1e-12):
|
|
25
|
+
raise ValueError("Fixture annotation score mismatch: " + run["run_id"] + " / " + field)
|
|
26
|
+
direct = next(system for group in report["groups"] for system in group["systems"] if system["system_id"] == "fixture-direct")
|
|
27
|
+
if not math.isclose(direct["metrics"]["answer_f1"]["sample_variance"], expected["direct_f1_sample_variance"], abs_tol=1e-12):
|
|
28
|
+
raise ValueError("Fixture sample variance mismatch")
|
|
29
|
+
workflow = next(run for run in report["runs"] if run["run_id"] == "workflow-1")
|
|
30
|
+
if workflow["metrics"]["text_coverage"] != expected["workflow_text_coverage"]:
|
|
31
|
+
raise ValueError("Unavailable sources disappeared from the coverage denominator")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def markdown(report):
|
|
35
|
+
fmt = lambda value: "not scored / unknown" if value is None else "%.4f" % value
|
|
36
|
+
lines = ["# Annotated research evaluation", "", "Kind: **%s**" % report["kind"], "", report["limitations"][0], "",
|
|
37
|
+
"No provider or model is invoked by this script. All semantic scores come from the supplied labels and rubric.", "",
|
|
38
|
+
"| Run | Answer F1 | Semantic citation accuracy | Reviewed scope | Cost (USD) | Latency (s) | Measurement basis |",
|
|
39
|
+
"| --- | ---: | ---: | ---: | ---: | ---: | --- |"]
|
|
40
|
+
for run in report["runs"]:
|
|
41
|
+
metrics = run["metrics"]
|
|
42
|
+
lines.append("| %s | %s | %s | %s | %s | %s | %s |" %
|
|
43
|
+
(run["run_id"], fmt(metrics["answer_f1"]), fmt(metrics["semantic_citation_accuracy"]),
|
|
44
|
+
fmt(metrics["reviewed_coverage"]), fmt(metrics["cost_usd"]), fmt(metrics["latency_seconds"]),
|
|
45
|
+
run["measurements"]["basis"]))
|
|
46
|
+
lines.extend(["", "## Repeat variation and comparability", ""])
|
|
47
|
+
for group in report["groups"]:
|
|
48
|
+
lines.extend(["### %s / %s" % (group["task_id"], group["comparison_group"]), "",
|
|
49
|
+
"Declared controls match: %s. Differences: %s." %
|
|
50
|
+
(group["comparability"]["controlled"], ", ".join(group["comparability"]["differences"]) or "None"), "",
|
|
51
|
+
"| System | Repeats | Mean answer F1 | Sample variance |",
|
|
52
|
+
"| --- | ---: | ---: | ---: |"])
|
|
53
|
+
for system in group["systems"]:
|
|
54
|
+
stats = system["metrics"]["answer_f1"]
|
|
55
|
+
lines.append("| %s | %s | %s | %s |" % (system["system_id"], stats["n"], fmt(stats["mean"]), fmt(stats["sample_variance"])))
|
|
56
|
+
lines.append("")
|
|
57
|
+
lines.extend(["## Interpretation limits", ""] + ["- " + item for item in report["limitations"]])
|
|
58
|
+
lines.extend(["", "The JSON report retains rubric text, evaluator identity, null scores, missing source IDs, control differences and input hashes.", ""])
|
|
59
|
+
return "\n".join(lines)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def main(argv=None):
|
|
63
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
64
|
+
parser.add_argument("--fixture", default=str(Path(__file__).resolve().parents[1] / "tests/fixtures/research-quality.json"),
|
|
65
|
+
help="JSON with suite, runs, judgments and optional known fixture expectations")
|
|
66
|
+
parser.add_argument("--output", help="Artifact directory outside plugins and canvas packages")
|
|
67
|
+
args = parser.parse_args(argv)
|
|
68
|
+
fixture = json.loads(Path(args.fixture).read_text(encoding="utf-8"))
|
|
69
|
+
report = evaluate(fixture["suite"], fixture["runs"], fixture.get("judgments"))
|
|
70
|
+
if "expected" in fixture:
|
|
71
|
+
verify_expected(report, fixture["expected"])
|
|
72
|
+
stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
|
|
73
|
+
output = Settings.external_path(args.output or str(Settings.data_directory() / "runs" / ("quality-" + stamp + "-" + uuid.uuid4().hex[:6])),
|
|
74
|
+
"Evaluation output")
|
|
75
|
+
output.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
76
|
+
artifacts = {"evaluation": str(output / "evaluation.json"), "report": str(output / "evaluation.md"), "inputs": str(output / "inputs.json")}
|
|
77
|
+
ResearchSessions._write(Path(artifacts["evaluation"]), report)
|
|
78
|
+
ResearchSessions._write(Path(artifacts["report"]), markdown(report))
|
|
79
|
+
ResearchSessions._write(Path(artifacts["inputs"]), fixture)
|
|
80
|
+
result = {"passed": True, "verification": "known_fixture_scores" if "expected" in fixture else "supplied_annotations_computed",
|
|
81
|
+
"kind": report["kind"], "network_calls": 0, "provider_quality_claim": False, "artifacts": artifacts}
|
|
82
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
83
|
+
return 0
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
if __name__ == "__main__":
|
|
87
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: bookmark-research
|
|
3
|
+
description: "Research ordinary bookmark URLs or Bookmark Canvas directories, ZIPs and cards: locate bookmarks, check linked pages, compare sources or deliver a scoped research report. Preserves original inputs; does not generate or write back canvas packages."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Bookmark Research
|
|
7
|
+
|
|
8
|
+
[中文阅读版](references/zh/skill-guide.md)
|
|
9
|
+
|
|
10
|
+
Use the user's links, package paths or registered sources. Answer in the requested language, then the saved `research.response_language`; `auto` follows the task's language (English if none is selected). Read `get_settings` when saved preferences are unknown. Pass the resolved language to delegates and saved briefs; preserve original quotations and identifiers. Load references in one language only. Keep source packages unchanged and local notes out of public queries.
|
|
11
|
+
|
|
12
|
+
## Choose the work the user needs
|
|
13
|
+
|
|
14
|
+
| Request | Workflow |
|
|
15
|
+
| --- | --- |
|
|
16
|
+
| Find bookmarks, count items, inspect notes or canvas relationships | Query local metadata. |
|
|
17
|
+
| Check a known link or a few facts | `quick`: read the URL and answer with evidence. |
|
|
18
|
+
| Compare bookmarked tools or investigate an open question | `agentic`: the current host model searches/reads, assesses evidence and follows remaining gaps. |
|
|
19
|
+
| Investigate a complete list/package, retain progress or deliver a verifiable report | `deep`: the same host-led investigation with frozen inputs, evidence records, independent review and coverage checks. |
|
|
20
|
+
|
|
21
|
+
Honor the task's explicit depth, then saved preferences; the table guides `auto`. Depth and scope are separate: a topic comparison covers that topic, while a whole-package request retains all original URLs. Briefly state the scope when useful, then perform the work. A capabilities question alone does not start an investigation. Host-led search and deep research need no professional research API key.
|
|
22
|
+
|
|
23
|
+
## Tools and the quick path
|
|
24
|
+
|
|
25
|
+
Use the actual MCP tool names exposed by the host. If a required tool or parameter is missing from an older catalog, use the bundled CLI: `<plugin-root>/src/cli.py`, where the plugin root is `../..` from this Skill's directory. Use its absolute path. Other CLI operations and their names are in the [CLI reference](references/cli.md).
|
|
26
|
+
|
|
27
|
+
Before each new question that needs web research, call `research_readiness` (CLI `readiness`). Supply the actual `host`, only actually observed host tools, and the intended providers when explicitly restricted. It respects cached/always/manual preferences; do not force repeated probes during one investigation. Offline bookmark queries need no network readiness check. After a connection/auth failure, inspect the affected provider and refresh after fixing it. Preserve an explicitly selected provider; optional API/MCP failures do not block other available routes.
|
|
28
|
+
|
|
29
|
+
Read the returned status precisely: configured credentials, reachable catalogs, successful retrieval and host OAuth are distinct. Missing optional native research MCPs or collaboration do not prevent the host's own research loop. For setup or remediation, use CLI `setup` and [settings guidance](references/settings-and-archive.md). Never ask for keys in chat; the terminal supports hidden entry or environment variables. An actual sample retrieval test consumes provider quota and is opt-in; readiness never starts a professional research job.
|
|
30
|
+
|
|
31
|
+
For a known public URL, call `fetch_web({urls:[url]})`. The equivalent fallback command is:
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
python3 <plugin-root>/src/cli.py fetch-web 'https://example.com/page' --timeout 20
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`--timeout` limits individual provider HTTP requests, not the entire waterfall. Beyond the readiness check, ordinary URL checks need no package import, research session or additional reference. Public GitHub README pages use this same path.
|
|
38
|
+
|
|
39
|
+
`fetch_web.pages` contains one selected extract per URL; each attempt retains status and archive paths. Read the relevant text and cite the original URL. Complete provider envelopes remain in the archive; `raw:true` (CLI `--raw`) returns them when needed. Check archive errors and completeness markers; a provider extract is not proof of a complete or live origin page.
|
|
40
|
+
|
|
41
|
+
## Search, assess, follow up
|
|
42
|
+
|
|
43
|
+
The current host model owns the first pass. Read known URLs directly; use `search_web` to discover sources, then read the relevant pages. Ordinary comparisons can answer directly. Create persistent research records when the user needs retained progress or a report.
|
|
44
|
+
|
|
45
|
+
Omitting providers uses the saved retrieval fallback. Defaults: reading tries Exa, then Parallel + Jina concurrently for unresolved URLs; search starts with Exa + Parallel, then Tavily + keyed Jina for queries without usable results. Explicit providers restrict the call. Preserve successful siblings and inspect unresolved items, skipped providers and remaining budget.
|
|
46
|
+
|
|
47
|
+
A nonempty response does not settle the question. Check page identity, relevant passages, date/version and whether the quotation supports the conclusion. For irrelevant, stale or insufficient text, follow up on that URL/question only: choose another permitted provider, a linked official document, an original-site API/Markdown view, or an available authorized browser. In a research session, record uncertain/rejected source reviews and use a new operation ID for justified new retrieval; do not replay a known inadequate result expecting new text. An alternative page can support an answer but cannot count as reading the original URL.
|
|
48
|
+
|
|
49
|
+
Read [provider access](references/research-workflow.md) when choosing or diagnosing retrieval services; [GitHub boundaries](references/github-and-sync.md) for code, issues, releases, private access or Git synchronization; [research methods](references/research-methods.md) for systematic comparisons, conflicts or benchmarks. A small fact check does not need these full procedures.
|
|
50
|
+
|
|
51
|
+
## Canvas context
|
|
52
|
+
|
|
53
|
+
For Canvas input, first read [package semantics](references/package-semantics.md); reuse it within the session. Consult the original package guide only for new/conflicting fields or a requested convention check.
|
|
54
|
+
|
|
55
|
+
- Find registered sources with `index_status`. Import a new export through `sync_package` in `snapshot` mode; use `live` for a persistent directory identified by the user. Only a confirmed full mirror uses `completeness:complete`; partial exports and single cards use `partial`. Reuse the actual source ID for moved exports of the same canvas. [Source lifecycle](references/source-lifecycle.md) covers migration, recovery and monitoring.
|
|
56
|
+
- Batch search targets in `search_bookmarks`; follow each target's pagination. Matching is literal across metadata, so verify short-name hits against titles/domains. Preserve IDs, URLs and grouping context. Local metadata does not prove online capabilities.
|
|
57
|
+
- Use `get_context` with `item_id` for a bookmark and its ancestors, and for card descriptions, groups and directed edges. A section overview does not enumerate all items. Obtain a folder ID from ancestors before restricting a search.
|
|
58
|
+
- Inspect `source.state`: disclose pending updates; use `refresh:false` on an unavailable/error source only when the user accepts the saved index. Historical snapshots use `source_history`. Research inventories and Wiki pages retain versions; freshness warnings request review, not an automatic reinvestigation.
|
|
59
|
+
|
|
60
|
+
## Deep research and collaboration
|
|
61
|
+
|
|
62
|
+
Read the [deep workflow](references/deep-research.md) when this mode is needed. Start with explicit questions and the complete requested input:
|
|
63
|
+
|
|
64
|
+
- Ordinary bookmarks: `research_start` with `urls:[...]`; duplicate positions remain in the frozen inventory. No Canvas registration is needed.
|
|
65
|
+
- Canvas packages: use indexed `source_ids`; whole-package scope is the default. Only an explicit subset uses `scope_mode:subset` and selected inventory IDs or bookmark references.
|
|
66
|
+
- A public question without an original bookmark list may omit both inputs. URLs mentioned only in the brief are not tracked input.
|
|
67
|
+
|
|
68
|
+
Follow every `research_inventory.next_offset`. Keep original `u-...` inventory IDs separate from saved `sN` evidence IDs. Read original text, review it, record quoted claims and each inventory judgment. Batch ready `research_record.entries` (up to 50) with a stable `batch_id`; use returned IDs in dependent batches. Check coverage differences and continue until complete or budget-limited. Failure notes, bulk exclusions, search snippets and external report citations cannot establish full original-source review.
|
|
69
|
+
|
|
70
|
+
The parent owns synthesis. When delegation is available and permitted, give independent readers/verifiers the scope, questions, output language, relevant instructions and actual MCP/CLI access. Read [host workflows](references/host-workflows.md) for Codex, Claude, Pi or DSH execution details; do not assume child context inheritance. Without delegation, conduct and disclose same-agent review.
|
|
71
|
+
|
|
72
|
+
Professional research is optional support for a concrete subproblem or an explicit user selection. Read [research services](references/research-services.md) before using it. Preserve explicit provider choices and known run IDs; do not replace running or unknown-outcome jobs. `research_route` recommends support but launches no work. A saved API preference does not displace the host's first pass.
|
|
73
|
+
|
|
74
|
+
## Delivery and settings
|
|
75
|
+
|
|
76
|
+
Answer quick tasks directly. Before finishing a report, review the actual short answer against evidence and counterevidence; condensing supported claims must preserve their conditions and limits. Use `research_finish` for reports and source/coverage artifacts: distinguish accounted inputs, usable text, substantive review and answered questions. Use `completed` only when coverage, supported answers and conflict checks pass; otherwise retain an `incomplete` report and gaps. Resume an incomplete archive with a `resume` record. Saved state is not a background agent.
|
|
77
|
+
|
|
78
|
+
Create Wiki pages when the user asks to retain conclusions in a Wiki or knowledge base; read [Wiki and evaluation](references/wiki-and-evaluation.md) for publication, lint and actual labeled quality comparisons. Do not infer semantic correctness from quote/hash checks.
|
|
79
|
+
|
|
80
|
+
Use `get_settings` for configuration questions and `update_settings` for lasting preferences; temporary choices use per-call options. [Settings and archives](references/settings-and-archive.md) describes storage. All archives, indexes and authored outputs stay outside the original package.
|